Puzzled by the ColumnSorterMixin... and by the Notebook...

Can you send a runnable demo? I was just dealing with the same issue
yesterday and it was because I had forgotten to set a unique value for
the call to SetItemData. I originally thought that might be the case
in your SortedList as well, but looking through refreshList it does

seem that index will always have a value because it always either hits
the colNum==0 or the else condition. (Unless data has no elements,
but then you’d get an error because index wouldn’t be defined…)

Could it be that you need to use the GetSecondarySortValues method?

Rob

It gets weirder and weirder. It turns out that the difference isn’t actually between the two classes at all, but in how I was deploying them (yes, I realize I should have figured that out long before!) :frowning:

My apps that use the SortedCheckList are all on panels - i.e., I have a panel on the left with buttons and options, and the right panel contains my list. My (one big) app that uses the SortedList actually creates 10 pages on a Notebook, each containing a SortedList.

So when I started to make a demo to send, I began with the Notebook app, and I was just going to create one page using the SortedList and one using the SortedCheckList - lo and behold, neither of them works! I re-jiggered it to create two panels instead, and they both work…well, sorta. Now, when I click on the last column header (“V”), I get the following error:

Traceback (most recent call last):
File “C:\Python25\lib\site-packages\wx-2.8-msw-unicode\wx\lib\mixins\listctrl.py”, line 118, in __OnColClick
self._colSortFlag[col] = int(not self._colSortFlag[col])

IndexError: list index out of range

which I fixed by editing the ColumnSorterMixin and changing
def SetColumnCount(self, newNumColumns):
self._colSortFlag = [0] * (newNumColumns)
self._col = -1

to
def SetColumnCount(self, newNumColumns):
self._colSortFlag = [0] * (newNumColumns + 1)
self._col = -1
at line 69 of listctrl.py.

So now I have a few more questions…

  • What the heck is the difference between a notebook page and a panel? (I realize there are LOTS of differences, but you know what I mean.)

  • Has anybody else run into the last-column error?

  • Does my change to the ColumnSorterMixin break anybody’s code? If not, should I submit a bug report? (I find it much more likely that I screwed up somehow than that I found a bug in mature code, but stranger things have happened…)

Here’s my demo - to switch back and forth between notebook and panel, comment or un-comment “Option 1” or “Option 2” in myFrame…

import wx, string, sys, os, copy
from types import *

from wx.lib.mixins.listctrl import ColumnSorterMixin, CheckListCtrlMixin, ListCtrlAutoWidthMixin

DemoList = [[“Column1”, “Column2”, “Col3”, “4”, “V”],
[“How”, “are”, “we”, “doing”, “here?”],

        ["Things", "aren't", "looking", "too", "good."],
        ["But", "they", "could", "get", "better."],

        ["I", "hope", "this", "list", "works"],
        ["How", "are", "you", "doing", "there?"]]

SortList = copy.deepcopy(DemoList)
ChkList = copy.deepcopy(DemoList)

class SortedList(wx.ListCtrl, ColumnSorterMixin, ListCtrlAutoWidthMixin):
‘’’
Modularize management of columns and headers. Unfortunately this adds some constraints:

    - If the data structure is a dictionary:
        - it must have a numeric index
        - key 0 must correspond to the header record
    - If the data structure is a list:
        - the first item in the list must be the header record
One drawback of making column creation automatic is that column width and style are set to defaults.
Future versions may add more control...
'''
def __init__(self, parent, inThing, log=None):

    wx.ListCtrl.__init__(self, parent, -1, pos=wx.DefaultPosition, size=wx.DefaultSize, style=wx.LC_REPORT
                             > wx.BORDER_NONE
                             > wx.WANTS_CHARS

                             )
    ListCtrlAutoWidthMixin.__init__(self)
    self.log = log

    header = []
    if isinstance(inThing, DictType): # inThing is a dictionary
        if inThing.has_key(0):
            header = inThing[0]
            del inThing[0]
        self.thing = inThing
    elif isinstance(inThing, ListType): #inThing is a list, so make a dictionary out of it

        header = inThing[0]
        del inThing[0]
        self.thing  = {}
        for num, item in enumerate(inThing):
            self.thing[num+1] = item   # needs to be 1-based, not 0-based

           
    ColumnSorterMixin.__init__(self, len(self.thing)) # number of columns equals width of header record
    self.itemDataMap = self.thing
    self.Widths = []
    for colNum, colData in enumerate(header):

        self.InsertColumn(colNum, colData)
        self.Widths.append(len(colData))
    self.refreshList(init=True)
    dc = wx.ScreenDC()
    for colNum, colWidth in enumerate(self.Widths

):
width, h = dc.GetTextExtent(‘N’*colWidth) # capital W is too wide, capital I too narrow
self.SetColumnWidth(colNum, width)

def getColumnText(self, index, col):

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

def refreshList(self, init = False):
    for key, data in self.thing.items():
        for colNum, colData in enumerate(data):

            if init:  # first time only - otherwise just update existing items
                self.Widths[colNum] = max(self.Widths[colNum], len(colData), 5) # get max length for each column, at least 5

                if (colNum==0):
                    index = self.InsertStringItem(sys.maxint, data[0])
                    self.tmpFont = self.GetItemFont(index)
            else:
                index = key -1

            self.SetStringItem(index, colNum, colData)
        self.SetItemData(index, key)

def GetListCtrl(self):
    return self

class SortedCheckList(wx.ListCtrl, CheckListCtrlMixin, ColumnSorterMixin, ListCtrlAutoWidthMixin):

'''
Combines the CheckListCtrl and ColumnSorter mixins for the ListCtrl widget.
Also modularizes management of columns and headers.  Unfortunately this adds some constraints:
    - If the data structure is a dictionary:
        - it must have a numeric index
        - key 0 must correspond to the header record
    - If the data structure is a list:
        - the first item in the list must be the header record
One drawback of making column creation automatic is that column width and style are set to defaults.
Future versions may add more control...
'''
def __init__(self, parent, inThing, log=None):

    self.log = log
    header = []
    if isinstance(inThing, DictType): # inThing is a dictionary
        if inThing.has_key(0):
            header = inThing[0]
            del inThing[0]

        self.thing = inThing
    elif isinstance(inThing, ListType):
        header = inThing[0]
        del inThing[0]
        self.thing  = {}
        for num, item in enumerate(inThing):

            self.thing[num+1] = item   # needs to be 1-based, not 0-based
           
    wx.ListCtrl.__init__(self, parent, -1, style=wx.LC_REPORT)
    CheckListCtrlMixin.__init__(self)
    ListCtrlAutoWidthMixin.__init__(self)

    ColumnSorterMixin.__init__(self, len(self.thing))
    self.itemDataMap = self.thing
    Widths = []
    for colNum, colData in enumerate(header):
        self.InsertColumn(colNum, colData)

        Widths.append(len(colData))
    for key, data in self.thing.items():
        for colNum, colData in enumerate(data):
            Widths[colNum] = max(Widths[colNum], len(str(colData)), 5) # get max length for each column, at least 5

            if (colNum==0):
                index = self.InsertStringItem(sys.maxint, data[0])
                tmpFont = self.GetItemFont(index)
            else:
                self.SetStringItem

(index, colNum, str(colData))
self.SetItemData(index, key)
dc = wx.ScreenDC()
dc.SetFont(tmpFont)
for colNum, colWidth in enumerate(Widths):
width, h = dc.GetTextExtent
(‘N’*colWidth) # capital W is too wide, capital I too narrow
self.SetColumnWidth(colNum, width)

    # for wxMSW
    self.Bind(wx.EVT_COMMAND_RIGHT_CLICK, self.OnRightClick

)
# for wxGTK
self.Bind(wx.EVT_RIGHT_UP, self.OnRightClick)

def GetListCtrl(self):
    return self

def OnRightClick(self, event):
    # only do this part the first time so the events are only bound once

    if not hasattr(self, "popupID1"):
        self.popupID1 = wx.NewId()
        self.popupID2 = wx.NewId()
        self.popupID3 = wx.NewId()
        self.popupID4 = wx.NewId

()
self.Bind(wx.EVT_MENU, self.OnPopupOne, id=self.popupID1)
self.Bind(wx.EVT_MENU, self.OnPopupTwo, id=self.popupID2)
self.Bind(wx.EVT_MENU, self.OnPopupThree, id=self.popupID3
)
self.Bind(wx.EVT_MENU, self.OnPopupFour, id=self.popupID4)

    # make a menu
    menu = wx.Menu()
    # add some items
    menu.Append(self.popupID1, "Select highlighted items")

    menu.Append(self.popupID2, "Deselect highlighted items")
    menu.Append(self.popupID3, "Select all")
    menu.Append(self.popupID4, "Deselect all")

    # Popup the menu.  If an item is selected then its handler

    # will be called before PopupMenu returns.
    self.PopupMenu(menu)
    menu.Destroy()

def OnPopupOne(self, event):
    index = self.GetFirstSelected()
    while index != -1:

        self.CheckItem(index)
        index = self.GetNextSelected(index)

def OnPopupTwo(self, event):
    index = self.GetFirstSelected()
    while index != -1:
        self.CheckItem

(index, False)
index = self.GetNextSelected(index)

def OnPopupThree(self, event):
    num = self.GetItemCount()
    for i in range(num):
        self.CheckItem(i)

def OnPopupFour(self, event):

    num = self.GetItemCount()
    for i in range(num):
        self.CheckItem(i, False)

class Log(object):
“”"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)

class myNB(
wx.Notebook):

···

On Dec 18, 2007 8:50 AM, Rob McMullen rob.mcmullen@gmail.com wrote:
#----------------------------------------------------------------------

The guts of the main operation…

#----------------------------------------------------------------------
def init(self, parent, id, log):

    wx.Notebook.__init__(self, parent, id, size=wx.DefaultSize, style=wx.BK_DEFAULT)
    self.log = log
    pageCheck = SortedCheckList(self,ChkList)
    self.AddPage(pageCheck, "SortedCheckList")

    pageSort = SortedList(self,SortList)
    self.AddPage(pageSort, "SortedList")
    self.Show(True)

class myFrame(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()

    #====Option 1======
    self.win = myNB(self, -1, log)

    #====Option 2======
    """
    panel = wx.Panel(self, -1)
    hbox = wx.BoxSizer(wx.HORIZONTAL)
    lvbox = wx.BoxSizer(wx.VERTICAL)
    rvbox = wx.BoxSizer

(wx.VERTICAL)
leftPanel = wx.Panel(panel, -1)
rightPanel = wx.Panel(panel, -1)
self.Llist = SortedCheckList(leftPanel, SortList)
self.Rlist = SortedList(rightPanel, ChkList)

    leftPanel.SetSizer(lvbox)
    lvbox.Add(self.Llist, 1, wx.EXPAND | wx.TOP, 3)
    rightPanel.SetSizer(rvbox)
    rvbox.Add(self.Rlist, 1, wx.EXPAND | wx.TOP, 3)
    hbox.Add(leftPanel, 1, wx.EXPAND)
    hbox.Add(rightPanel, 1, wx.EXPAND)
    panel.SetSizer(hbox)
    self.Centre()
    self.Show(True)
    """
    #====end Option 2======

class MyApp(
wx.App):
def OnInit(self):
f = myFrame(None, -1, “Stripped-down Demo of SortedList”,wx.Size(700,600))
f.Show()
return True

#===============================================================================================

Where it all begins…

#===============================================================================================
def main(argv=None):
if argv is None:
argv = sys.argv
app = MyApp(0)

app.MainLoop()

if name == ‘main’:
main()

Thank you! I’ll try that as soon as I get a chance.

···

On Dec 19, 2007 3:34 PM, Robin Dunn robin@alldunn.com wrote:

Marc Tompkins wrote:

I have pocked with the source code of ColumnSorterMixin, and what
happens (for an unknown reason to me) when you use a notebook the
column click event is sent *twice*, actually erasing the effect of
sorting. The very first click (which still send the column click event
twice) changes the sort flag from undefined to ascending and suddenly
to descending (because of the 2 events). After that, no matter how you
click, the sorting order can only be descending, as the sort flag is
reset by the twice called event.

Well, that certainly jibes with what I’m seeing. Unfortunately MSW is
the platform my users use…
Any ideas on how I can work around this? I’m very fond of the look of
the Notebook, and it certainly couldn’t be any easier to work with. I’d

hate to have to switch away from it just to make column sorting work,
but sorting is very important for my application…

If you happen to remember, at approximately what point along the

event-processing chain do single click events turn into doubles? Most
of my ideas are along the lines of slipping in an extra event - turning
two into three - but obviously turning two events into four would be

worse than useless…

ColumnSorterMixin binds the EVT_LIST_COL_CLICK event and calls evt.Skip
just in case you also want to catch it. I bet that the Skip is what is
allowing the 2nd event to be sent, so try binding that event to your own

method and don’t call evt.Skip() there, so the event processing system
will not keep looking for an event handler.


Robin Dunn
Software Craftsman

http://wxPython.org
Java give you jitters? Relax with wxPython!


To unsubscribe, e-mail:
wxPython-users-unsubscribe@lists.wxwidgets.org
For additional commands, e-mail: wxPython-users-help@lists.wxwidgets.org


www.fsrtechnologies.com