<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 IsCompatibleVariantType
3, 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):
__init__(self)
wxd.DataViewCustomRenderer.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)):
= wx.Choice(self.parent, choices=self.Choices)
TmpR __setattr__("Row", cell.Position.y // self.CellHeight)
TmpR.self.OnChoice)
TmpR.Bind(wx.EVT_CHOICE, 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,
+self.listctrl.Position.y+cell.height-3)
cell.Position.yself.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()