wxWidgets / Phoenix

wxPython's Project Phoenix. A new implementation of wxPython, better, stronger, faster than he was before.
http://wxpython.org/
2.33k stars 516 forks source link

dataviewctrl doesn't show floats in wxpy4.1 #1597

Closed mprosperi closed 4 years ago

mprosperi commented 4 years ago

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

#!/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 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)
        c5 = self.dvc.AppendTextColumn('Nr',   6, width=40, mode=dv.DATAVIEW_CELL_ACTIVATABLE)

        c5 = self.dvc.AppendTextColumn("id", 3, width=40,  mode=dv.DATAVIEW_CELL_EDITABLE)
        c5.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()
RobinD42 commented 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.

mprosperi commented 4 years ago

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()
RobinD42 commented 4 years ago

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)
RobinD42 commented 4 years ago

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.