Closed mprosperi closed 4 years ago
If it worked on Windows in 4.0.7 then I suspect that it was an accident. It should be throwing an error due to the mismatch in data types for the column renderer and the data provided. On OSX with 4.0.7 I get:
wx._core.wxAssertionError: C++ assertion "Assert failure" failed at /Users/robind/projects/bb2/dist-osx-py36/build/ext/wxWidgets/src/osx/cocoa/dataview.mm(2813) in MacRender(): Text renderer cannot render value because of wrong value type; value type: double
There are some options. The first is to simply convert the value to/from strings in your GetValue
and SetValue
as needed.
The other option is to create your own renderer class derived from DataViewCustomRenderer
that handles floating point values, and give an instance of it to the DataViewColumn
that you add to the dataview control.
Using the customer renderer doesn't work on wxpy4.1 but works with 4.0.7. Looks like the value is not set in the renderer for floats (you can try the code below). So the only solution is casting to str in the data view model. To me it look like a workaround because the bool, datetime and int columns are managed without explicitly converting to str
#!/usr/bin/env python
import wx
import wx.dataview as dv
class Song(object):
def __init__(self, id, artist, title, genre):
self.id = id
self.artist = artist
self.title = title
self.genre = genre
self.like = False
self.nr = 3.3
self.date = wx.DateTime().FromDMY(15,4,2020)
class Genre(object):
def __init__(self, name):
self.name = name
self.songs = []
class MyCustomRenderer(dv.DataViewCustomRenderer):
def __init__(self, log, *args, **kw):
dv.DataViewCustomRenderer.__init__(self, *args, **kw)
self.log = log
self.value = None
self.EnableEllipsize(wx.ELLIPSIZE_END)
def SetValue(self, value):
#self.log.write('SetValue: %s' % value)
self.value = value
return True
def GetValue(self):
## self.log.write('GetValue: {}'.format(value))
return self.value
def GetSize(self):
# Return the size needed to display the value. The renderer
# has a helper function we can use for measuring text that is
# aware of any custom attributes that may have been set for
# this item.
try:
value = '%0.2f' % self.value
except:
value = self.value if self.value else ""
size = self.GetTextExtent(value)
size += (2,2)
#self.log.write('GetSize("{}"): {}'.format(value, size))
return size
def Render(self, rect, dc, state):
#if state != 0:
# self.log.write('Render: %s, %d' % (rect, state))
if not state & dv.DATAVIEW_CELL_SELECTED:
# we'll draw a shaded background to see if the rect correctly
# fills the cell
dc.SetBrush(wx.Brush('#ffd0d0'))
dc.SetPen(wx.TRANSPARENT_PEN)
rect.Deflate(1, 1)
dc.DrawRoundedRectangle(rect, 2)
# And then finish up with this helper function that draws the
# text for us, dealing with alignment, font and color
# attributes, etc.
try:
value = '%0.2f' % self.value
except:
value = self.value if self.value else ""
self.RenderText(value,
0, # x-offset
rect,
dc,
state # wxDataViewCellRenderState flags
)
return True
def ActivateCell(self, rect, model, item, col, mouseEvent):
## self.log.write("ActivateCell")
return False
# The HasEditorCtrl, CreateEditorCtrl and GetValueFromEditorCtrl
# methods need to be implemented if this renderer is going to
# support in-place editing of the cell value, otherwise they can
# be omitted.
#
# NOTE: This is well supported only in the DVC implementation on Windows,
# so this sample will not turn on the editable mode for the custom
# rendered column, see below.
def HasEditorCtrl(self):
## self.log.write('HasEditorCtrl')
return True
def CreateEditorCtrl(self, parent, labelRect, value):
## self.log.write('CreateEditorCtrl: %s' % labelRect)
ctrl = wx.TextCtrl(parent,
value=value,
pos=labelRect.Position,
size=labelRect.Size)
# select the text and put the caret at the end
ctrl.SetInsertionPointEnd()
ctrl.SelectAll()
return ctrl
def GetValueFromEditorCtrl(self, editor):
## self.log.write('GetValueFromEditorCtrl: %s' % editor)
value = editor.GetValue()
return value
# The LeftClick and Activate methods serve as notifications
# letting you know that the user has either clicked or
# double-clicked on an item. Implementing them in your renderer
# is optional.
def LeftClick(self, pos, cellRect, model, item, col):
## self.log.write('LeftClick')
return False
def Activate(self, cellRect, model, item, col):
## self.log.write('Activate')
return False
class MyTreeListModel(dv.PyDataViewModel):
def __init__(self, data, log):
dv.PyDataViewModel.__init__(self)
self.data = data
self.log = log
self.UseWeakRefs(True)
def GetColumnCount(self):
return 7
def GetColumnType(self, col):
mapper = { 0 : 'string',
1 : 'string',
2 : 'string',
3 : 'string', # the real value is an int, but the renderer should convert it okay
4 : 'datetime',
5 : 'bool',
6: 'float'
}
return mapper[col]
def GetChildren(self, parent, children):
if not parent:
for genre in self.data:
children.append(self.ObjectToItem(genre))
return len(self.data)
node = self.ItemToObject(parent)
if isinstance(node, Genre):
for song in node.songs:
children.append(self.ObjectToItem(song))
return len(node.songs)
return 0
def IsContainer(self, item):
# The hidden root is a container
if not item:
return True
# and in this model the genre objects are containers
node = self.ItemToObject(item)
if isinstance(node, Genre):
return True
# but everything else (the song objects) are not
return False
def GetParent(self, item):
if not item:
return dv.NullDataViewItem
node = self.ItemToObject(item)
if isinstance(node, Genre):
return dv.NullDataViewItem
elif isinstance(node, Song):
for g in self.data:
if g.name == node.genre:
return self.ObjectToItem(g)
def GetValue(self, item, col):
node = self.ItemToObject(item)
if isinstance(node, Genre):
mapper = { 0 : node.name,
1 : "",
2 : "",
3 : "",
4 : wx.DateTime.FromTimeT(0), # TODO: There should be some way to indicate a null value...
5 : False,
6:3.3
}
return mapper[col]
elif isinstance(node, Song):
mapper = { 0 : node.genre,
1 : node.artist,
2 : node.title,
3 : node.id,
4 : node.date,
5 : node.like,
6:3.3
}
return mapper[col]
else:
raise RuntimeError("unknown node type")
def SetValue(self, value, item, col):
node = self.ItemToObject(item)
if isinstance(node, Song):
if col == 1:
node.artist = value
elif col == 2:
node.title = value
elif col == 3:
node.id = value
elif col == 4:
node.date = value
elif col == 5:
node.like = value
elif col==6:
node.nr= value
return True
#----------------------------------------------------------------------
class TestPanel(wx.Panel):
def __init__(self, parent, log, data=None, model=None):
self.log = log
wx.Panel.__init__(self, parent, -1)
# Create a dataview control
self.dvc = dv.DataViewCtrl(self,
style=wx.BORDER_THEME
| dv.DV_ROW_LINES # nice alternating bg colors
#| dv.DV_HORIZ_RULES
| dv.DV_VERT_RULES
| dv.DV_MULTIPLE
)
self.model = MyTreeListModel(data, log)
newModel = True # it's a new instance so we need to decref it below
self.dvc.AssociateModel(self.model)
if newModel:
self.model.DecRef()
self.dvc.AppendTextColumn("Genre", 0, width=80)
c1 = self.dvc.AppendTextColumn("Artist", 1, width=170, mode=dv.DATAVIEW_CELL_EDITABLE)
c2 = self.dvc.AppendTextColumn("Title", 2, width=260, mode=dv.DATAVIEW_CELL_EDITABLE)
c3 = self.dvc.AppendDateColumn('Acquired', 4, width=100, mode=dv.DATAVIEW_CELL_ACTIVATABLE)
c4 = self.dvc.AppendToggleColumn('Like', 5, width=40, mode=dv.DATAVIEW_CELL_ACTIVATABLE)
renderer = MyCustomRenderer(self.log)
col = dv.DataViewColumn("Nr", renderer, 6, width=260)
col.Alignment = wx.ALIGN_LEFT
self.dvc.AppendColumn(col)
## c5 = self.dvc.AppendTextColumn('Nr', 6, width=40, mode=dv.DATAVIEW_CELL_ACTIVATABLE)
c6 = self.dvc.AppendTextColumn("id", 3, width=40, mode=dv.DATAVIEW_CELL_EDITABLE)
c6.Alignment = wx.ALIGN_RIGHT
self.Sizer = wx.BoxSizer(wx.VERTICAL)
self.Sizer.Add(self.dvc, 1, wx.EXPAND)
#----------------------------------------------------------------------
def runTest(frame, log=None):
# Reuse the music data in the ListCtrl sample, and put it in a
# hierarchical structure so we can show it as a tree
musicdata = {
1 : ("Bad English", "The Price Of Love", "Rock"),
2 : ("DNA featuring Suzanne Vega", "Tom's Diner", "Rock"),
3 : ("George Michael", "Praying For Time", "Rock"),
4 : ("Gloria Estefan", "Here We Are", "Rock"),
5 : ("Linda Ronstadt", "Don't Know Much", "Rock"),
6 : ("Michael Bolton", "How Am I Supposed To Live Without You", "Blues"),
7 : ("Paul Young", "Oh Girl", "Rock"),
8 : ("Paula Abdul", "Opposites Attract", "Rock"),
9 : ("Richard Marx", "Should've Known Better", "Rock"),
10: ("Rod Stewart", "Forever Young", "Rock"),
11: ("Roxette", "Dangerous", "Rock"),
12: ("Sheena Easton", "The Lover In Me", "Rock"),}
# our data structure will be a collection of Genres, each of which is a
# collection of Songs
data = dict()
for key, val in musicdata.items():
song = Song(str(key), val[0], val[1], val[2])
genre = data.get(song.genre)
if genre is None:
genre = Genre(song.genre)
data[song.genre] = genre
genre.songs.append(song)
data = data.values()
# Finally create the test window
win = TestPanel(frame, log, data=data)
return win
if __name__ == '__main__':
import wx
class MyFrame(wx.Frame):
def __init__(self, parent, title):
wx.Frame.__init__(self, parent, title=title, size=(800,300))
self.control = runTest(self) #wx.TextCtrl(self, style=wx.TE_MULTILINE)
self.Show(True)
app = wx.App(False)
frame = MyFrame(None, 'Sample')
app.MainLoop()
Using the customer renderer doesn't work on wxpy4.1
You need to set the varianttype in your renderer to "double". The default value is "string" so that's why you're still getting the wrong type errors.
class MyCustomRenderer(dv.DataViewCustomRenderer):
def __init__(self, log, *args, **kw):
kw['varianttype'] = 'double'
dv.DataViewCustomRenderer.__init__(self, *args, **kw)
self.log = log
self.value = None
self.EnableEllipsize(wx.ELLIPSIZE_END)
To me it look like a workaround because the bool, datetime and int columns are managed without explicitly converting to str
Because there are existing renderer classes for them in the library, and convenience methods in DVC for adding columns using the built-in types of renderers. Or, in the case of int
the underlying C++ wxVariant class is able to convert it to a string. For anything else you need to create your own renderer class. I suspect that there is no renderer or auto-convert for double
types because there are many ways to represent a floating point value as a string and it was unclear what the best way to do it would be, and so it's left up to the application developer instead.
Operating system: win10 wxPython version & source: wxPython-4.1.0a1.dev4626+8e2627e8-cp37-cp37m-win_amd64.whl Python version & source: 3.7.2 stock
Description of the problem:
I've added a float column ('nr') to a reduced version of a dvc demo sample. With wxpy4.0.7 the values are shown while with 4.1a they are not