What is missing from my subclass of propgrid.PGProperty?

I modified the example from propgrid.PGProperty - creating-custom-properties.
The wiki class did not function, as it a) raised a TypeError (see below[1]), b) the value 44 was not displayed in the property grid.

With my modification, my “MiniApp” below [2]

  • does not display the value 44 and crashes when I close the gui window (MainLoop exception [3])
  • -or-
  • displays the value 44, but only if I set a break point right after super().init() in MyProperty and interrupt the interpreter by that manner.

Can someone tell me, what I am missing? Why is the value display if put a breakpoint in the script? Why do I get that exception [2]? I have implemented GetValueAsString and still raise the assertion error.

I should not have to implement my own wxVariant type, or do I have to? The documentation (also found at link above) is uncertain:
```
Since wx.propgrid.PGProperty derives from wx.Object, you can use standard DECLARE_DYNAMIC_CLASS and IMPLEMENT_DYNAMIC_CLASS macros. From the above example they were omitted for sake of simplicity, and besides, they are only really needed if you need to use RTTI with your property class. You can change the ‘value type’ of a property by simply assigning different type of variant with SetValue. It is mandatory to implement VariantData class for all data types used as property values. You can use macros declared in wx.propgrid.PropertyGrid headers. For instance: […WHERE???]

# NOTE: wxVariants are handled internally in wxPython. Conversions are
# implicitly done for those types that wxVariant already knows about, and
# the Raw PyObject is used for those that it doesn’t know about.

```

[1]:

TypeError: descriptor '__init__' requires a 'sip.simplewrapper' object but received a 'str'

[2]

Python313\site-packages\wx\core.py", line 2262, in MainLoop
    rv = wx.PyApp.MainLoop(self)
wx._core.wxAssertionError: C++ assertion ""GetChildCount() > 0"" failed at ..\..\src\propgrid\property.cpp(1016) in wxPGProperty::ValueToString(): If user property does not have any children, it must override GetValueAsString

[3] My snippet. Raises [2].

import wx
import wx.propgrid

class Alpha:
    def __init__(self, val: int = None, str_val: str = None):
        if val is None:
            self.value = int(str_val)
        else:
            self.value = val

    @property
    def Value(self) -> int:
        return self.value

    def to_str(self) -> str:
        return str(self.value)


class MyProperty(wx.propgrid.PGProperty):
    def __init__(self, label, name, value : Alpha):
        super().__init__(label, name)

        self.m_value = value
       # self.SetValue(value) # tried as alternative
        

    def DoGetEditorClass(self):
        return wx.propgrid.PGTextCtrlEditor

    def ValueToString(self, value, argFlags=0):
        return value.to_str()

    def StringToValue(self, text, argFlags=0):
        return True, Alpha(str_val=text)

    def GetValueAsString(self, argFlags=0):
        return self.m_value.to_str()

class MiniFrame(wx.Frame):
    def __init__(self):
        super().__init__(None, title="PGProperty")
        pan = wx.Panel(self)
        siz = wx.BoxSizer(wx.VERTICAL)
        pan.SetSizer(siz)

        ppgm = wx.propgrid.PropertyGridManager(pan, style=wx.propgrid.PG_TOOLBAR | wx.propgrid.PGMAN_DEFAULT_STYLE)
        siz.Add(ppgm, 1, wx.EXPAND)

        ppg = ppgm.AddPage("1")
        ppg.Append(MyProperty("c", "c", Alpha(44)))

        self.SetSize(wx.Size(300, 200))
        self.Fit()

class MiniApp(wx.App):
    def OnInit(self):
        try:
            self.frm = MiniFrame()
            self.frm.CenterOnScreen()
            self.frm.Show()
        except Exception as e:
            import traceback as tb
            wx.MessageBox("\n".join(tb.format_exception(e)))
            exit()
        return True

if __name__ == "__main__":
    app = MiniApp()
    app.MainLoop()

Addendum: In a nutshell, I would like to know if it is even possible to subclass the PGProperty baseclass at the current state (wxPython 4.2.2, Python 3.13.2) - not unlike this question about subclassing a GridCellEditor: infinite-recursion-subclassing-gridcellnumbereditor.

try this (straight from the docu) :sweat:

import wx
import wx.propgrid

class Alpha:
    def __init__(self, val: int = None, str_val: str = None):
        if val is None:
            self.value = int(str_val)
        else:
            self.value = val

    @property
    def Value(self) -> int:
        return self.value

    def to_str(self) -> str:
        return str(self.value)


class MyProperty(wx.propgrid.PGProperty):

    def __init__(self, label=wx.propgrid.PG_LABEL, name=wx.propgrid.PG_LABEL, value=0):
        wx.propgrid.PGProperty.__init__(self, label, name)
        self.my_value = int(value)

    def DoGetEditorClass(self):
        """
        Determines what editor should be used for this property type. This
        is one way to specify one of the stock editors.
        """
        return wxpg.PropertyGridInterface.GetEditorByName("TextCtrl")

    def ValueToString(self, value, flags):
        """
        Convert the given property value to a string.
        """
        return str(value)

    def StringToValue(self, st, flags):
        """
        Convert a string to the correct type for the property.

        If failed, return False or (False, None). If success, return tuple
        (True, newValue).
        """
        try:
            val = int(st)
            return (True, val)
        except (ValueError, TypeError):
            pass
        except:
            raise
        return (False, None)

class MiniFrame(wx.Frame):
    def __init__(self):
        super().__init__(None, title="PGProperty")
        pan = wx.Panel(self)
        siz = wx.BoxSizer(wx.VERTICAL)
        pan.SetSizer(siz)

        ppgm = wx.propgrid.PropertyGridManager(pan, style=wx.propgrid.PG_TOOLBAR | wx.propgrid.PGMAN_DEFAULT_STYLE)
        siz.Add(ppgm, 1, wx.EXPAND)

        ppg = ppgm.AddPage("1")
        ppg.Append(MyProperty("c", "c", 44))

        self.SetSize(wx.Size(300, 200))
        self.Fit()

class MiniApp(wx.App):
    def OnInit(self):
        try:
            self.frm = MiniFrame()
            self.frm.CenterOnScreen()
            self.frm.Show()
        except Exception as e:
            import traceback as tb
            wx.MessageBox("\n".join(tb.format_exception(e)))
            exit()
        return True

if __name__ == "__main__":
    app = MiniApp()
    app.MainLoop()

I modified the example from propgrid.PGProperty - creating-custom-properties.

Been there, done that. I copied and ran your snippet as is to rule out mistakes on my side. The propgrid either displays nothing or a greyed out value, if I set a break point in __init__:

MyProperty_as_example_in_wiki

Either the base class is bugged or the documentation is wrong. Or I am a complete moron…

Here is my current example (and yes, I also ran your bare example - the behavior is identical):


import wx
import wx.propgrid

# set to 0, if you want to see the program raise a TypeError (on close)
# set a breakpoint (marked with other comment in __init__) to make the 
# property display the value without crashing on close
MY_VALUE = 1 

class MyProperty(wx.propgrid.PGProperty):

    def __init__(self, label=wx.propgrid.PG_LABEL, name=wx.propgrid.PG_LABEL, value=0):
        wx.propgrid.PGProperty.__init__(self, label, name)

        if MY_VALUE == 1:   # set breakpoint here
            self.my_value = int(value)
        else:
            self.SetValue(int(value))

    def DoGetEditorClass(self):
        """
        Determines what editor should be used for this property type. This
        is one way to specify one of the stock editors.
        """
        return wx.propgrid.PropertyGridInterface.GetEditorByName("TextCtrl")

    def ValueToString(self, value, flags):
        """
        Convert the given property value to a string.
        """
        print("valtostr: " + str(value))
        return str(value)

    def StringToValue(self, st, flags):
        """
        Convert a string to the correct type for the property.

        If failed, return False or (False, None). If success, return tuple
        (True, newValue).
        """
        try:
            print("strtoval: " + st)
            val = int(st)
            return (True, val)
        except (ValueError, TypeError):
            pass
        except:
            raise
        return (False, -1)
    
    def GetValueAsString(self, argFlags=0):
        if MY_VALUE == 1:
            print("GetValueAsStr: " + str(self.my_value))
            return str(self.my_value)
        else:
            print("GetValueAsStr: " + str(self.GetValue()))
            return str(self.GetValue())


class MiniFrame(wx.Frame):
    def __init__(self):
        super().__init__(None, title="PGProperty")
        pan = wx.Panel(self)
        siz = wx.BoxSizer(wx.VERTICAL)
        pan.SetSizer(siz)

        ppgm = wx.propgrid.PropertyGridManager(pan, style=wx.propgrid.PG_TOOLBAR | wx.propgrid.PGMAN_DEFAULT_STYLE)
        siz.Add(ppgm, 1, wx.EXPAND)

        ppg = ppgm.AddPage("1")
        ppg.Append(wx.propgrid.IntProperty("IntProperty", "b", 44))
        ppg.Append(MyProperty("MyProperty", "c", 44))

        self.SetSize(wx.Size(300, 200))
        self.Fit()

class MiniApp(wx.App):
    def OnInit(self):
        try:
            self.frm = MiniFrame()
            self.frm.CenterOnScreen()
            self.frm.Show()
        except Exception as e:
            import traceback as tb
            wx.MessageBox("\n".join(tb.format_exception(e)))
            exit()
        return True

if __name__ == "__main__":
    app = MiniApp()
    app.MainLoop()

If I use SetValue() - as the wiki recommends along the way - then I get either a TypeError due to GetValueAsStr() - which I have implemented though - or the value is displayed correctly - but only if I set a breakpoint:

MyProperty_as_wiki_reccomends_set_value

The snippet is the same as above with MY_VALUE set to Zero (0)

I have spent some time investigating this problem, but have not found a convincing answer.

I noticed that the PropertyGrid example in the wxPython Demo includes a custom PyObjectPropertyValue class that is used by a PyObjectProperty class.

I made a standalone version of the example and modified line 773 to pass in an initial PyObjectPropertyValue object. When I run that code it does appear to work and there is no AssertionError raised if I edit the value and then close the application.

I then tried to create a cut down version that only includes the PyObjectPropertyValue and PyObjectProperty classes. The editing seems to work OK, but it then raises an AssertionError when the app is closed.

Here is the standalone version of the example from the wxPython Demo:
PropertyGrid.py (35.3 KB)

Here is the cut down version:
python_object_custom_property_1.py (2.2 KB)

I think the standalone example shows you should be able to do what you want, but I haven’t been able to get it to work in isolation.

Edit: here is a screen dump of the standalone version, showing the relevant item being edited:

PropertyGrid.py runs fine. The cut down version raises the assertion as you said - not always though…I have managed to close the window without an error a few times (this might depend on the build speed - the longer, the less likely to raise an error?).

I also pasted PyObjectProperty and PyObjectPropertyValue in my snippet above and appended just one PyObjectProperty item. There are no errors on change or on close :unamused:

Going though the lines, I found a difference. Where I use the PropertyGridPage (returned by PropertyGridManager.AddPage()) to append properties, the sample uses Append() of the PropertyGridManager.

        ppg = ppgm.AddPage("1")

        #no err, sometimes even no AssertionError on close
        ppgm.Append(PyObjectProperty("Append(Manager)", "manager_append", PyObjectPropertyValue("1-2-3-4")))
        # error, is ppg not a page object??
        ppg.Append(PyObjectProperty("Append(Page)", "page_append", PyObjectPropertyValue("5-6-7-8")))

Note that default property classes (e.g. StringProperty) added with PropertyGridPage.Append() do not cause any error.

Whatever causes this must be triggered especially when the property grid page is referenced by AddPage(). However, since this happens in your cutdown example as well, the real cause might be hidden internally in the PropertyGridManager.

well, initial display is no problem but there seems to be a lot more to investigate :hot_face:

import wx
import wx.propgrid as prgr

class MyProperty(prgr.PGProperty):

    def __init__(self, label=prgr.PG_LABEL, name=prgr.PG_LABEL, value=0):
        prgr.PGProperty.__init__(self, label, name)
        self.m_value = value

    def DoGetEditorClass(self):
        """
        Determines what editor should be used for this property type. This
        is one way to specify one of the stock editors.
        """
        return prgr.PropertyGridInterface.GetEditorByName("TextCtrl")

    def ValueToString(self, value, flags):
        """
        Convert the given property value to a string.
        """
        return str(value)

    def StringToValue(self, st, flags):
        """
        Convert a string to the correct type for the property.

        If failed, return False or (False, None). If success, return tuple
        (True, newValue).
        """
        try:
            val = int(st)
            return (True, val)
        except (ValueError, TypeError):
            pass
        except:
            raise
        return (False, None)

class MiniFrame(wx.Frame):
    def __init__(self):
        super().__init__(None, title="PGProperty")
        pan = wx.Panel(self)
        siz = wx.BoxSizer(wx.VERTICAL)
        pan.SetSizer(siz)

        ppgm = prgr.PropertyGridManager(pan, style=prgr.PG_TOOLBAR | prgr.PGMAN_DEFAULT_STYLE)
        siz.Add(ppgm, 1, wx.EXPAND)

        ppg = ppgm.AddPage("1")
        ppg.Append(prgr.IntProperty("IntProperty", "b", 44))
        p = MyProperty("MyProperty", "c", 44)
        ppg.Append(p)

        self.SetSize(wx.Size(300, 200))
        self.CenterOnScreen()
        self.Show()
        ppg.RefreshProperty(p)

app = wx.App()
MiniFrame()
app.MainLoop()

I have to think about it, try some analog in C++. Sadly, I have no deep insight in the deeper layers of the bindings. Maybe, I should check out the git source.

For now, the best solution is to use Append() of the PropertyGridManager, not of the PropergyGridPage. The only, major downside is that you may not be able to add/remove pages easily. If you only need one page, or a static grid that is built once, then that is a final, satisfying solution.

well, I just wonder what such a grid is used for and why you are not subclassing one of the given properties :woozy_face:
but here is some more to play (before getting drowned in C++) :smiling_face_with_three_hearts:

import wx
import wx.propgrid as prgr

class MyProperty(prgr.PGProperty):

    def __init__(self, label=prgr.PG_LABEL, name=prgr.PG_LABEL, value=0):
        super().__init__(label, name)
        self.m_value = value

    def DoGetEditorClass(self):
        """
        Determines what editor should be used for this property type. This
        is one way to specify one of the stock editors.
        """
        print(f'editor {self.m_value}')
        return prgr.PropertyGridInterface.GetEditorByName("TextCtrl")

    def ValueToString(self, value, flags):
        """
        Convert the given property value to a string.
        """
        print(f'vts {value} {flags}')
        return str(value)

    def StringToValue(self, st, flags):
        """
        Convert a string to the correct type for the property.

        If failed, return False or (False, None). If success, return tuple
        (True, newValue).
        """
        print(f'vts {st} {flags}')
        try:
            val = int(st)
            return (True, val)
        except (ValueError, TypeError):
            pass
        except:
            raise
        return (False, None)

class MiniFrame(wx.Frame):
    def __init__(self):
        super().__init__(None, title="PGProperty")
        pan = wx.Panel(self)
        siz = wx.BoxSizer(wx.VERTICAL)
        pan.SetSizer(siz)

        ppgm = prgr.PropertyGridManager(pan, style=prgr.PG_TOOLBAR | prgr.PGMAN_DEFAULT_STYLE)
        siz.Add(ppgm, 1, wx.EXPAND)

        ppg = ppgm.AddPage("1")
        ppg.Append(prgr.IntProperty("IntProperty", "b", 44))
        self.p = MyProperty("MyProperty", "c", 45)
        ppg.Append(self.p)

        self.SetSize(wx.Size(300, 200))
        self.CenterOnScreen()
        self.Show()
        def evt_destroy(evt):
            self.p.DeleteChildren()
        self.Bind(wx.EVT_WINDOW_DESTROY, evt_destroy)

app = wx.App()
MiniFrame()
app.MainLoop()

The original idea was to use my generic classes as m_value container. Now, that you say it, I migh as well subclass from the defined properties. :face_with_hand_over_mouth:

class MyStrProp(wx.propgrid.StringProperty):
   def __init__(self, in : SubclassGeneric):
     super().__init__(in.to_str(...))

  def ValueToString(self, value, flags):
    return val.to_str(...)

  def StringToValue(self, st, flags):
     try:
         return (True, val.from_str(st))
     except (ValueError, TypeError):
         return (False, None)
     except:
         raise

Eventually, I wanted to custom properties with combinations of widget elements, and use PGProperty as base.