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.
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!
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.
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.
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.
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