Search code examples
colorswxpythonrowselectedlistctrl

wxPython wxListCtrl selected row color


I want to have certain rows selected color be red instead of the standard color (blue on windows) so that I can indicate status. Anyone know if this is possible in wxPython?


Solution

  • In order to do what you want, i.e. have a different selection color when certain items are selected, you will need to drop into win32. Fortunately, it is not too hard to do that in python. It does however make your code platform dependent. I tried it out today in a small program. If the Genre is not "Rock" I make the selection orange. Here are some screenshots.

    Rock Items Selected alt text

    Mixed Items Selected. Notice the RnB and Blues are selected with Orange alt text http://img258.imageshack.us/img258/1307/soshot2.jpg

    Here is the code. It looks scary at first but if you know any win32, its not that bad. I make use of the pywin32 package and the std ctypes libraries. I had to define some of the SDK constants as they were not available in the win32con module.

    import sys
    import wx
    import wx.lib.mixins.listctrl  as  listmix
    
    import win32api
    import win32gui
    import win32con
    import win32gui_struct
    import commctrl
    import ctypes
    from ctypes.wintypes import BOOL, HWND, RECT, UINT, DWORD, HDC, DWORD, LPARAM, COLORREF
    
    LVM_FIRST = 0x1000
    LVM_GETSUBITEMRECT=(LVM_FIRST + 56)
    LVIR_BOUNDS             =0
    LVIR_ICON               =1
    LVIR_LABEL              =2
    LVIR_SELECTBOUNDS       =3
    DEFAULT_GUI_FONT    =17
    
    #LPNMHDR
    class NMHDR(ctypes.Structure):
        pass
    INT = ctypes.c_int
    NMHDR._fields_ = [('hwndFrom', HWND), ('idFrom', UINT), ('code', INT)]
    LPNMHDR = ctypes.POINTER(NMHDR)
    
    #LPNMCUSTOMDRAW
    class NMCUSTOMDRAW(ctypes.Structure):
        pass
    NMCUSTOMDRAW._fields_ = [('hdr', NMHDR), ('dwDrawStage', DWORD), ('hdc', ctypes.c_int), 
                             ('rc', RECT), ('dwItemSpec', DWORD), ('uItemState', UINT),
                             ('lItemlParam', LPARAM)]
    LPNMCUSTOMDRAW = ctypes.POINTER(NMCUSTOMDRAW)
    
    #LPNMLVCUSTOMDRAW
    class NMLVCUSTOMDRAW(ctypes.Structure):
        pass
    NMLVCUSTOMDRAW._fields_ = [('nmcd', NMCUSTOMDRAW), 
                               ('clrText', COLORREF),
                               ('clrTextBk', COLORREF),
                               ('iSubItem', ctypes.c_int),
                               ('dwItemType', DWORD),
                               ('clrFace', COLORREF),
                               ('iIconEffect', ctypes.c_int),
                               ('iIconPhase', ctypes.c_int),
                               ('iPartId', ctypes.c_int),
                               ('iStateId', ctypes.c_int),                           
                               ('rcText', RECT),
                               ('uAlign', UINT)
                               ]
    LPNMLVCUSTOMDRAW = ctypes.POINTER(NMLVCUSTOMDRAW)
    
    
    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 : ("Bobby Brown", "My Prerogative", "RnB"),
    }
    
    
    
    
    class MyListCtrl(wx.ListCtrl, listmix.ListCtrlAutoWidthMixin):
        def __init__(self, parent, ID, pos=wx.DefaultPosition,
                     size=wx.DefaultSize, style=0):
            wx.ListCtrl.__init__(self, parent, ID, pos, size, style)
            listmix.ListCtrlAutoWidthMixin.__init__(self)
    
        def ShouldCustomDraw(self, row):
            if self.IsSelected(row):
                listitem = self.GetItem(row, 2)
                genre = listitem.GetText()
    
                return genre != "Rock"
    
    
        def CustomDraw(self, lpcd):        
            if lpcd.contents.nmcd.dwDrawStage == commctrl.CDDS_PREPAINT:
                return (True, commctrl.CDRF_NOTIFYITEMDRAW)
    
            if lpcd.contents.nmcd.dwDrawStage == commctrl.CDDS_ITEMPREPAINT:                
                if self.ShouldCustomDraw(lpcd.contents.nmcd.dwItemSpec):
                    #do custom drawing for non Rock selected rows
                    #paint the selection background
                    color = win32api.RGB(255, 127, 0) #orange
                    brush = win32gui.CreateSolidBrush(color)   
                    r = lpcd.contents.nmcd.rc
                    win32gui.FillRect(int(lpcd.contents.nmcd.hdc),  (r.left+4, r.top, r.right, r.bottom), brush)                
                    win32gui.DeleteObject(brush)
                    return (True, commctrl.CDRF_NOTIFYSUBITEMDRAW)                    
    
            if lpcd.contents.nmcd.dwDrawStage == commctrl.CDDS_ITEMPREPAINT|commctrl.CDDS_SUBITEM:                
                row = lpcd.contents.nmcd.dwItemSpec
                col = lpcd.contents.iSubItem
                item = self.GetItem(row, col)
                text = item.GetText()
                #paint the text
                rc = RECT()
                rc.top = col
                if col > 0:
                    rc.left = LVIR_BOUNDS
                else:
                    rc.left = LVIR_LABEL
                success = win32api.SendMessage(self.Handle, LVM_GETSUBITEMRECT, row, ctypes.addressof(rc))
                if col > 0:
                    rc.left += 5
                else:
                    rc.left += 2
                rc.top += 2
    
                if success:                
                    oldColor = win32gui.SetTextColor(lpcd.contents.nmcd.hdc, win32gui.GetSysColor(win32con.COLOR_HIGHLIGHTTEXT))                
                    win32gui.DrawText(lpcd.contents.nmcd.hdc, text, len(text), (rc.left, rc.top, rc.right, rc.bottom), win32con.DT_LEFT|win32con.DT_VCENTER)
                    win32gui.SetTextColor(lpcd.contents.nmcd.hdc, oldColor)                                
    
                return (True, commctrl.CDRF_SKIPDEFAULT)
    
    
            # don't need custom drawing
            return (True, commctrl.CDRF_DODEFAULT)
    
    
    class MyFrame(wx.Frame):
        def __init__(self, *args, **kwds):
            wx.Frame.__init__(self, *args, **kwds)
            self._sizer = wx.BoxSizer(wx.VERTICAL)
            tID = wx.NewId()
            self._ctl = MyListCtrl(self, tID,
                                     style=wx.LC_REPORT 
                                     #| wx.BORDER_SUNKEN
                                     | wx.BORDER_NONE
                                     | wx.LC_EDIT_LABELS
                                     | wx.LC_SORT_ASCENDING
                                     #| wx.LC_NO_HEADER
                                     #| wx.LC_VRULES
                                     #| wx.LC_HRULES
                                     #| wx.LC_SINGLE_SEL
                                     )
            self._sizer.Add(self._ctl, 1, wx.EXPAND, 3)
            self.PopulateList()
    
            self.oldWndProc = win32gui.SetWindowLong(self.GetHandle(), win32con.GWL_WNDPROC, self.MyWndProc)
    
    
        def MyWndProc(self, hWnd, msg, wParam, lParam):
    
            if msg == win32con.WM_NOTIFY:
                hwndFrom, idFrom, code = win32gui_struct.UnpackWMNOTIFY(lParam)
                if code == commctrl.NM_CUSTOMDRAW and hwndFrom == self._ctl.Handle:                
                    lpcd = ctypes.cast(lParam, LPNMLVCUSTOMDRAW)
                    retProc, retCode = self._ctl.CustomDraw(lpcd)
    
                    if retProc:
                        return retCode
    
    
            # Restore the old WndProc.  Notice the use of wxin32api
            # instead of win32gui here.  This is to avoid an error due to
            # not passing a callable object.
            if msg == win32con.WM_DESTROY:
                win32api.SetWindowLong(self.GetHandle(),
                                  win32con.GWL_WNDPROC,
                                  self.oldWndProc)
    
            # Pass all messages (in this case, yours may be different) on
            # to the original WndProc
            return win32gui.CallWindowProc(self.oldWndProc,
                                      hWnd, msg, wParam, lParam)
    
        def PopulateList(self):
            self._ctl.InsertColumn(0, "Artist")
            self._ctl.InsertColumn(1, "Title")
            self._ctl.InsertColumn(2, "Genre")
    
            items = musicdata.items()
    
            for key, data in items:            
                index = self._ctl.InsertStringItem(sys.maxint, data[0])
                self._ctl.SetStringItem(index, 1, data[1])
                self._ctl.SetStringItem(index, 2, data[2])
                self._ctl.SetItemData(index, key)
    
    
            self._ctl.SetColumnWidth(0, wx.LIST_AUTOSIZE)
            self._ctl.SetColumnWidth(1, wx.LIST_AUTOSIZE)
            self._ctl.SetColumnWidth(2, 100)
    
            self.currentItem = 0
    
    class MyApp(wx.App):
        def OnInit(self):
            frame = MyFrame(None, -1, 'wxListCtrl StackOverflow')
            frame.Show()
            self.SetTopWindow(frame)
            return 1
    
    if __name__ == "__main__":
        app = MyApp(0)
        app.MainLoop()