<article>


Wed 06 Nov 2024 (Last Updated 12 Nov 2024)

WxPython Data Views: Tips, Tricks and Pitfalls

WxPython documentation is somewhat lacking, theres a lot of obscure parts of the API that the docs dont elaborate on enough, no fleshed out samples in the demo folder of the sources (nor the separate samples folder)[1(1)] and no quantity of googlefu to save you. One of these nooks in WxPython is the DataView family of classes. In this document is some Tips, Tricks and Pitfalls ive learned while dealing with the bloody things

ObjectToItem uses the id function

When using PyDataViewModel they use the bulletin id function, meaning your inputted objects for ItemToObject/ObjectToItem, need to be the same instance, defining __eq__ or whatever other pythonic™ abstraction wont cut it. From what I believe using id() is considered black magic wizardry in python land, so theres that solace i guess. Instead simply override the ItemToObject/ObjectToItem functions with

def ItemToObject(self, item):
    # Replace item.GetID() with whatever object hashing you want to use
    return self.Mapper[int(item.GetID())]
def ObjectToItem(self, obj: data.Categories.CategoryRef):
    self.Mapper[obj.CatID] = obj
    return wxd.DataViewItem(obj.CatID)

(Debugging this stuff can be a nightmare!)

Excessive calls to Get* Functions

In a DataViewModel WxPython will excessively call all Get overridden functions inside it, especially when resizing. If you are doing database lookups within those functions or something, then you’ll have a very slow experience, even for python standards! Simply wrap all your intensive Get functions as well as IsContainer with the functools.cache decorator, though remember not to wrap you Set functions too, at first it works…until it doesn’t functools.cache is a highly underrated feature of python, as well as memorization in general, ill likely do another one of these when Im more versed in it.

Adding container columns with content

Despite being in the docs2, you (i) will have probably missed it, you can enable container columns by overriding HasContainerColumns in your DateViewModel. Additionally there is also IsCompatibleVariantType3, which should allow types other than string to be returned from a custom renderer. However too much of my code base serializes and deserializes to strings for me to bother with, so thats homework for the those following at home i guess.

Keep your DataViewItem Ids close to the onscreen presentation

In an ideal world, your ids will be a 1:1 mapping of whats on screen, with for example each Id being the next row starting from 0. Alas things are never perfect.

Rendering a control in DataViewCustomRenderer

I was about to tell you that rendering a plain old WxPython control inside of a DataViewCustomRenderer was impossible, again no docs or samples on this matter, plus the “““stateless”“” nature of Render makes it quite hard to implement stateful controls. However I spent an afternoon night figuring it out and came back with this rather janky solution. Ive only tested this on wxGTK+ (with a custom theme lul), so here be dragons. Additionally you may need to modify the offsets I used to get it centered in the cell

class _ChoiceRenderer(wxd.DataViewCustomRenderer):
    def __init__(self, parent, listctrl, choices):
        wxd.DataViewCustomRenderer.__init__(self)
        self.listctrl = listctrl # Could replace with self.GetView,
                                 # but segfaulted last time I tried
        self.parent = parent
        self.Choices = choices
        self.ControlNameList = []
    def GetValue(self):
        print(self.CurElm)
        return str(self.value)
    def GetSize(self):
        return wx.Size(200, 30)
    def Render(self, cell:wx.Rect, dc:wx.DC, state):
        self.CellHeight = cell.height
        if not hasattr(self,str(cell.y)):
            TmpR = wx.Choice(self.parent, choices=self.Choices)
            TmpR.__setattr__("Row", cell.Position.y // self.CellHeight)
            TmpR.Bind(wx.EVT_CHOICE, self.OnChoice)
            self.__setattr__(str(cell.y), TmpR)
            self.ControlNameList.append(str(cell.y))
        self.CurElm:wx.Choice = self.__getattribute__(str(cell.y))
        self.CurElm.Show()
        self.CurElm.SetSelection(self.value)
        self.CurElm.Position = wx.Point(cell.Position.x+1, 
            cell.Position.y+self.listctrl.Position.y+cell.height-3)
        self.CurElm.SetSize(cell.width, cell.height)
        return True

    def OnChoice(self,evt):
       print("ONCHOICE", evt.GetEventObject().Row)
       # It doesn't seem possible to call back into the rest of
       # DataViewModel here, call into you underlying API i guess?
    def UnRender(self):
        for e in self.ControlNameList:
            self.__getattribute__(e).Hide()
    def HasEditorCtrl(self):
        return False


# In your business code
self.Bind(wxd.EVT_DATAVIEW_ITEM_COLLAPSED, self.OnCollapsed)
def OnCollapsed(self, evt):
    <YourChoiceRenderer>.UnRender()

  1. 1↩︎

  2. https://docs.wxpython.org/wx.dataview.DataViewModel.html#wx.dataview.DataViewModel.HasContainerColumns↩︎

  3. https://docs.wxpython.org/wx.dataview.DataViewRenderer.html#wx.dataview.DataViewRenderer.IsCompatibleVariantType↩︎