Welcome, guest | Sign In | My Account | Store | Cart

This recipe shows a way of using ColumnSorterMixin with virtual lists (wx.ListCtrl defined with the wx.LC_VIRTUAL flag). The sample code pretty much follows the wx.listctrl demo (provided with wxPython). The interesting bit is the TestVirtualList class and the musicdata dictionary (altogether lines 42 to 257). Remainder is provided to run the TestVirtualList class (or others from the wxdemo) standalone.

The idea is to combine an itemDataMap dictionary with an index table which defines the order in which the dictionary items are displayed. The actual display is handled by the virtual list using only the OnGetItemText, OnGetItemImage and OnGetItemAttr methods.

First of all, the 3 methods OnGetItemText OnGetItemImage and OnGetItemAttr must be declared. Following the ListCtrl demo and in addition to the self.itemDataMap dictionary, a self.itemIndexMap table defines the items order.

ColumnSorterMixin uses the SortItems, GetListrCtrl and GetSortImages methods which are redefined here. GetListCtrl now returns self (to keep the mixin happy). GetSortImages is pretty much the same as in the ListCtrl demo, only it uses the art provider (needs better looking arrows, maybe). SortItems handles the sorting. It gets the column clicked from the mixin _col variable and the sorting order (ascending or descending) from the _colSortFlag table.

Python, 254 lines
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
import wx
import wx.lib.mixins.listctrl  as  listmix

import sys
import time
        
#----------------------------------------------------------------------
class Log:
    r"""\brief Needed by the wxdemos.
    The log output is redirected to the status bar of the containing frame.
    """

    def WriteText(self,text_string):
        self.write(text_string)

    def write(self,text_string):
        wx.GetApp().GetTopWindow().SetStatusText(text_string)

#----------------------------------------------------------------------
# The panel you want to test (TestVirtualList)
#----------------------------------------------------------------------

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"),
13: ("Sinead O'Connor", "Nothing Compares 2 U", "Rock"),
14: ("Stevie B.", "Because I Love You", "Rock"),
15: ("Taylor Dayne", "Love Will Lead You Back", "Rock"),
16: ("The Bangles", "Eternal Flame", "Rock"),
17: ("Wilson Phillips", "Release Me", "Rock"),
18: ("Billy Joel", "Blonde Over Blue", "Rock"),
19: ("Billy Joel", "Famous Last Words", "Rock"),
20: ("Billy Joel", "Lullabye (Goodnight, My Angel)", "Rock"),
21: ("Billy Joel", "The River Of Dreams", "Rock"),
22: ("Billy Joel", "Two Thousand Years", "Rock"),
23: ("Janet Jackson", "Alright", "Rock"),
24: ("Janet Jackson", "Black Cat", "Rock"),
25: ("Janet Jackson", "Come Back To Me", "Rock"),
26: ("Janet Jackson", "Escapade", "Rock"),
27: ("Janet Jackson", "Love Will Never Do (Without You)", "Rock"),
28: ("Janet Jackson", "Miss You Much", "Rock"),
29: ("Janet Jackson", "Rhythm Nation", "Rock"),
30: ("Janet Jackson", "State Of The World", "Rock"),
31: ("Janet Jackson", "The Knowledge", "Rock"),
32: ("Spyro Gyra", "End of Romanticism", "Jazz"),
33: ("Spyro Gyra", "Heliopolis", "Jazz"),
34: ("Spyro Gyra", "Jubilee", "Jazz"),
35: ("Spyro Gyra", "Little Linda", "Jazz"),
36: ("Spyro Gyra", "Morning Dance", "Jazz"),
37: ("Spyro Gyra", "Song for Lorraine", "Jazz"),
38: ("Yes", "Owner Of A Lonely Heart", "Rock"),
39: ("Yes", "Rhythm Of Love", "Rock"),
40: ("Cusco", "Dream Catcher", "New Age"),
41: ("Cusco", "Geronimos Laughter", "New Age"),
42: ("Cusco", "Ghost Dance", "New Age"),
43: ("Blue Man Group", "Drumbone", "New Age"),
44: ("Blue Man Group", "Endless Column", "New Age"),
45: ("Blue Man Group", "Klein Mandelbrot", "New Age"),
46: ("Kenny G", "Silhouette", "Jazz"),
47: ("Sade", "Smooth Operator", "Jazz"),
48: ("David Arkenstone", "Papillon (On The Wings Of The Butterfly)", "New Age"),
49: ("David Arkenstone", "Stepping Stars", "New Age"),
50: ("David Arkenstone", "Carnation Lily Lily Rose", "New Age"),
51: ("David Lanz", "Behind The Waterfall", "New Age"),
52: ("David Lanz", "Cristofori's Dream", "New Age"),
53: ("David Lanz", "Heartsounds", "New Age"),
54: ("David Lanz", "Leaves on the Seine", "New Age"),
}

class TestVirtualList(wx.ListCtrl, listmix.ListCtrlAutoWidthMixin, listmix.ColumnSorterMixin):
    def __init__(self, parent,log):
        wx.ListCtrl.__init__( self, parent, -1, style=wx.LC_REPORT|wx.LC_VIRTUAL|wx.LC_HRULES|wx.LC_VRULES)
        self.log=log

        #adding some art
        self.il = wx.ImageList(16, 16)
        a={"sm_up":"GO_UP","sm_dn":"GO_DOWN","w_idx":"WARNING","e_idx":"ERROR","i_idx":"QUESTION"}
        for k,v in a.items():
            s="self.%s= self.il.Add(wx.ArtProvider_GetBitmap(wx.ART_%s,wx.ART_TOOLBAR,(16,16)))" % (k,v)
            exec(s)
        self.SetImageList(self.il, wx.IMAGE_LIST_SMALL)

        #adding some attributes (colourful background for each item rows)
        self.attr1 = wx.ListItemAttr()
        self.attr1.SetBackgroundColour("yellow")
        self.attr2 = wx.ListItemAttr()
        self.attr2.SetBackgroundColour("light blue")
        self.attr3 = wx.ListItemAttr()
        self.attr3.SetBackgroundColour("purple")

        #building the columns
        self.InsertColumn(0, "Artist")
        self.InsertColumn(1, "Title")
        self.InsertColumn(2, "Genre")
        self.SetColumnWidth(0, 150)
        self.SetColumnWidth(1, 220)
        self.SetColumnWidth(2, 100)

        #These two should probably be passed to init more cleanly
        #setting the numbers of items = number of elements in the dictionary
        self.itemDataMap = musicdata
        self.itemIndexMap = musicdata.keys()
        self.SetItemCount(len(musicdata))
        
        #mixins
        listmix.ListCtrlAutoWidthMixin.__init__(self)
        listmix.ColumnSorterMixin.__init__(self, 3)

        #sort by genre (column 2), A->Z ascending order (1)
        self.SortListItems(2, 1)

        #events
        self.Bind(wx.EVT_LIST_ITEM_SELECTED, self.OnItemSelected)
        self.Bind(wx.EVT_LIST_ITEM_ACTIVATED, self.OnItemActivated)
        self.Bind(wx.EVT_LIST_ITEM_DESELECTED, self.OnItemDeselected)
        self.Bind(wx.EVT_LIST_COL_CLICK, self.OnColClick)

    def OnColClick(self,event):
        event.Skip()

    def OnItemSelected(self, event):
        self.currentItem = event.m_itemIndex
        self.log.WriteText('OnItemSelected: "%s", "%s", "%s", "%s"\n' %
                           (self.currentItem,
                            self.GetItemText(self.currentItem),
                            self.getColumnText(self.currentItem, 1),
                            self.getColumnText(self.currentItem, 2)))

    def OnItemActivated(self, event):
        self.currentItem = event.m_itemIndex
        self.log.WriteText("OnItemActivated: %s\nTopItem: %s\n" %
                           (self.GetItemText(self.currentItem), self.GetTopItem()))

    def getColumnText(self, index, col):
        item = self.GetItem(index, col)
        return item.GetText()

    def OnItemDeselected(self, evt):
        self.log.WriteText("OnItemDeselected: %s" % evt.m_itemIndex)


    #---------------------------------------------------
    # These methods are callbacks for implementing the
    # "virtualness" of the list...

    def OnGetItemText(self, item, col):
        index=self.itemIndexMap[item]
        s = self.itemDataMap[index][col]
        return s

    def OnGetItemImage(self, item):
        index=self.itemIndexMap[item]
        genre=self.itemDataMap[index][2]

        if genre=="Rock":
            return self.w_idx
        elif genre=="Jazz":
            return self.e_idx
        elif genre=="New Age":
            return self.i_idx
        else:
            return -1

    def OnGetItemAttr(self, item):
        index=self.itemIndexMap[item]
        genre=self.itemDataMap[index][2]

        if genre=="Rock":
            return self.attr2
        elif genre=="Jazz":
            return self.attr1
        elif genre=="New Age":
            return self.attr3
        else:
            return None

    #---------------------------------------------------
    # Matt C, 2006/02/22
    # Here's a better SortItems() method --
    # the ColumnSorterMixin.__ColumnSorter() method already handles the ascending/descending,
    # and it knows to sort on another column if the chosen columns have the same value.

    def SortItems(self,sorter=cmp):
        items = list(self.itemDataMap.keys())
        items.sort(sorter)
        self.itemIndexMap = items
        
        # redraw the list
        self.Refresh()

    # Used by the ColumnSorterMixin, see wx/lib/mixins/listctrl.py
    def GetListCtrl(self):
        return self

    # Used by the ColumnSorterMixin, see wx/lib/mixins/listctrl.py
    def GetSortImages(self):
        return (self.sm_dn, self.sm_up)

    #XXX Looks okay to remove this one (was present in the original demo)
    #def getColumnText(self, index, col):
    #    item = self.GetItem(index, col)
    #    return item.GetText()

#----------------------------------------------------------------------
# The main window
#----------------------------------------------------------------------
# This is where you populate the frame with a panel from the demo.
#  original line in runTest (in the demo source):
#    win = TestPanel(nb, log)
#  this is changed to:
#    self.win=TestPanel(self,log)
#----------------------------------------------------------------------

class TestFrame(wx.Frame):

    def __init__(self, parent, id, title, size, style = wx.DEFAULT_FRAME_STYLE ):

        wx.Frame.__init__(self, parent, id, title, size=size, style=style)

        self.CreateStatusBar(1)

        log=Log()

        self.win = TestVirtualList(self, log)

def main(argv=None):
    if argv is None:
        argv = sys.argv

    # Command line arguments of the script to be run are preserved by the
    # hotswap.py wrapper but hotswap.py and its options are removed that
    # sys.argv looks as if no wrapper was present.
    #print "argv:", `argv`

    #some applications might require image handlers
    #wx.InitAllImageHandlers()

    app = wx.PySimpleApp()
    f = TestFrame(None, -1, "ColumnSorterMixin used with a Virtual ListCtrl",wx.Size(500,300))
    f.Show()
    app.MainLoop()

if __name__ == '__main__':
    main()

In my opinion, virtual ListCtrl are more powerful than normal lists in the sense that only a few methods used in conjunction with the data stored in itemDataMap, entirely define the behaviour of the ListCtrl. If the content of itemDataMap is changed (and maybe the item count), the actual display is handled automatically after a Refresh() call.

Optimisation: Thanks to Matt C for the new SortItems function!

5 comments

David S 18 years, 9 months ago  # | flag

One wx problem. This works perfectly except for 1 problem I have, which is, I think, with wxPython itself. The problem is that the column header remains obscured as if the graphic was still there after sorting on another column. I have tried numerous workarounds involving wx.NullBitmap, my own image lists with 1 pixel wide images, etc. No luck. Otherwise it works great and seems faster than using the list control alone.

dict not required. The data can be any sequence: all that is required is that the itemDataMap support the __get__ method with an integer argument, which obviously applies to any sequence. This simplifies the data input quite a bit.

Tage Shadow 18 years, 7 months ago  # | flag

Thank you. This code has been very helpful! I appreciate that you shared it with all of us. =)

@David S

The problem with the column header lies in wxPython. More specifically, it lies in the function that is used when clearing the "arrow" image from the previously sorted column.

Before you click on a column to sort it, the columns are not obscured yet because they are not set to show an image -yet-. After you click it, it sets that column to allow an image and then loads the image you specified into it. When you sort another column afterwards, the function that is used to clear that image only sets the image to nothing. It does not tell the program you don't need space there for an image anymore; therefore, the program still thinks an image is being used there and leaves space open for it.

You'll have to override this function in your listctrl class.

It's called: ClearColumnImage(self, col)

I overrided it with nearly the same code it uses--except, without the code that tells it to save space for an image. The function is in the _controls.py file.

Matt C 18 years, 2 months ago  # | flag

A better SortItems method. Here's a better SortItems() method -- the ColumnSorterMixin.__ColumnSorter() method already handles the ascending/descending, and it knows to sort on another column if the chosen columns have the same value.

def SortItems(self,sorter=cmp):
    items = list(self.itemDataMap.keys())
    items.sort(sorter)
    self.itemIndexMap = items

    # redraw the list
    self.Refresh()
Egor Zindy (author) 17 years, 9 months ago  # | flag

Code update... Thanks Matt,

I swapped my SortItem function for yours (nearly 6 months after you submitted it, sorry!). I really like what you did!

Cheers, Egor