wxOLVTooltips

Recently I was trying to figure out how to add tooltips to each item in an ObjectListView widget in wxPython on Windows. The wxPython wiki has an example that uses PyWin32, but I didn’t want to go that route. So I asked on the wxPython Google Group and got an interesting answer. They had actually used one of my old articles to build their solution for me. I have cleaned it up a little bit and decided it was worth sharing with my readers:

import wx
from ObjectListView import ObjectListView, ColumnDefn
 
########################################################################
class Book(object):
    """
    Model of the Book object
 
    Contains the following attributes:
    'ISBN', 'Author', 'Manufacturer', 'Title'
    """
    #----------------------------------------------------------------------
    def __init__(self, title, author, isbn, mfg):
        self.isbn = isbn
        self.author = author
        self.mfg = mfg
        self.title = title
 
 
########################################################################
class MainPanel(wx.Panel):
    #----------------------------------------------------------------------
    def __init__(self, parent):
        wx.Panel.__init__(self, parent=parent, id=wx.ID_ANY)
        self.products = [Book("wxPython in Action", "Robin Dunn",
                              "1932394621", "Manning"),
                         Book("Hello World", "Warren and Carter Sande",
                              "1933988495", "Manning")
                         ]
 
        self.dataOlv = ObjectListView(self, wx.ID_ANY, style=wx.LC_REPORT|wx.SUNKEN_BORDER)
        self.setBooks()
 
        # Allow the cell values to be edited when double-clicked
        self.dataOlv.cellEditMode = ObjectListView.CELLEDIT_SINGLECLICK
 
        # create an update button
        updateBtn = wx.Button(self, wx.ID_ANY, "Update OLV")
        updateBtn.Bind(wx.EVT_BUTTON, self.updateControl)
 
        # Create some sizers
        mainSizer = wx.BoxSizer(wx.VERTICAL)        
 
        mainSizer.Add(self.dataOlv, 1, wx.ALL|wx.EXPAND, 5)
        mainSizer.Add(updateBtn, 0, wx.ALL|wx.CENTER, 5)
        self.SetSizer(mainSizer)
 
        self.dataOlv.Bind(wx.EVT_LIST_ITEM_SELECTED, self.onSetToolTip)
 
    #----------------------------------------------------------------------
    def updateControl(self, event):
        """
        Update the control
        """
        print "updating..."
        #product_dict = [{"title":"Core Python Programming", "author":"Wesley Chun",
                         #"isbn":"0132269937", "mfg":"Prentice Hall"},
                        #{"title":"Python Programming for the Absolute Beginner",
                         #"author":"Michael Dawson", "isbn":"1598631128",
                         #"mfg":"Course Technology"},
                        #{"title":"Learning Python", "author":"Mark Lutz",
                         #"isbn":"0596513984", "mfg":"O'Reilly"}
                        #]
 
        product_list = [Book("Core Python Programming", "Wesley Chun",
                             "0132269937", "Prentice Hall"),
                        Book("Python Programming for the Absolute Beginner",
                             "Michael Dawson", "1598631128", "Course Technology"),
                        Book("Learning Python", "Mark Lutz", "0596513984",
                             "O'Reilly")
                        ]
 
        data = self.products + product_list
        self.dataOlv.SetObjects(data)
 
    #----------------------------------------------------------------------
    def setBooks(self, data=None):
        """
        Sets the book data for the OLV object
        """
        self.dataOlv.SetColumns([
            ColumnDefn("Title", "left", 220, "title"),
            ColumnDefn("Author", "left", 200, "author"),
            ColumnDefn("ISBN", "right", 100, "isbn"),
            ColumnDefn("Mfg", "left", 180, "mfg")
        ])
 
        self.dataOlv.SetObjects(self.products)
 
    #----------------------------------------------------------------------
    def onSetToolTip(self, event):
        """
        Set the tool tip on the selected row
        """
        item = self.dataOlv.GetSelectedObject()
        tooltip = "%s is a good writer!" % item.author
        event.GetEventObject().SetToolTipString(tooltip)
        event.Skip()
 
########################################################################
class MainFrame(wx.Frame):
    #----------------------------------------------------------------------
    def __init__(self):
        wx.Frame.__init__(self, parent=None, id=wx.ID_ANY, 
                          title="ObjectListView Demo", size=(800,600))
        panel = MainPanel(self)
 
########################################################################
class GenApp(wx.App):
 
    #----------------------------------------------------------------------
    def __init__(self, redirect=False, filename=None):
        wx.App.__init__(self, redirect, filename)
 
    #----------------------------------------------------------------------
    def OnInit(self):
        # create frame here
        frame = MainFrame()
        frame.Show()
        return True
 
#----------------------------------------------------------------------
def main():
    """
    Run the demo
    """
    app = GenApp()
    app.MainLoop()
 
if __name__ == "__main__":
    main()

All I needed to do was add a binding to wx.EVT_LIST_ITEM_SELECTED. Then in my event handler I needed to grab the event object and set its tooltip string. What I would really like to do is find a way to copy this grid recipe so that I can just mouse over items and have the tooltip change, but it doesn’t look like ObjectListView / ListCtrl has the methods I would need to translate mouse coordinates to a column / row. Regardless, the solution given does work as advertised. Thanks a lot, Erxin!

Update: One of my astute readers noticed an error in my code where when I hit the update button, it added a dictionary to the ObjectListView widget. While you can add a dictionary, this broke the onSetToolTip method as some of the items that were added were no longer Book instances. So I have updated the code to add the extra items as Book instances and commented out the dictionary example.

Update (2013/12/30)

After playing around with a StackOverflow answer I found, I discovered that I can dynamically update the tooltip, although it’s still not quite what I’d like. But first, here’s the updated code example:

import wx
from ObjectListView import ObjectListView, ColumnDefn
 
########################################################################
class Book(object):
    """
    Model of the Book object
 
    Contains the following attributes:
    'ISBN', 'Author', 'Manufacturer', 'Title'
    """
    #----------------------------------------------------------------------
    def __init__(self, title, author, isbn, mfg):
        self.isbn = isbn
        self.author = author
        self.mfg = mfg
        self.title = title
 
 
########################################################################
class MainPanel(wx.Panel):
    #----------------------------------------------------------------------
    def __init__(self, parent):
        wx.Panel.__init__(self, parent=parent, id=wx.ID_ANY)
        self.products = [Book("wxPython in Action", "Robin Dunn",
                              "1932394621", "Manning"),
                         Book("Hello World", "Warren and Carter Sande",
                              "1933988495", "Manning")
                         ]
 
        self.dataOlv = ObjectListView(self, wx.ID_ANY, 
                                      style=wx.LC_REPORT|wx.SUNKEN_BORDER)
        self.dataOlv.Bind(wx.EVT_MOTION, self.updateTooltip)
        self.setBooks()
 
        # Allow the cell values to be edited when double-clicked
        self.dataOlv.cellEditMode = ObjectListView.CELLEDIT_SINGLECLICK
 
        # create an update button
        updateBtn = wx.Button(self, wx.ID_ANY, "Update OLV")
        updateBtn.Bind(wx.EVT_BUTTON, self.updateControl)
 
        # Create some sizers
        mainSizer = wx.BoxSizer(wx.VERTICAL)        
 
        mainSizer.Add(self.dataOlv, 1, wx.ALL|wx.EXPAND, 5)
        mainSizer.Add(updateBtn, 0, wx.ALL|wx.CENTER, 5)
        self.SetSizer(mainSizer)
 
    #----------------------------------------------------------------------
    def updateControl(self, event):
        """
        Update the control
        """
        print "updating..."
        #product_dict = [{"title":"Core Python Programming", "author":"Wesley Chun",
                         #"isbn":"0132269937", "mfg":"Prentice Hall"},
                        #{"title":"Python Programming for the Absolute Beginner",
                         #"author":"Michael Dawson", "isbn":"1598631128",
                         #"mfg":"Course Technology"},
                        #{"title":"Learning Python", "author":"Mark Lutz",
                         #"isbn":"0596513984", "mfg":"O'Reilly"}
                        #]
 
        product_list = [Book("Core Python Programming", "Wesley Chun",
                             "0132269937", "Prentice Hall"),
                        Book("Python Programming for the Absolute Beginner",
                             "Michael Dawson", "1598631128", "Course Technology"),
                        Book("Learning Python", "Mark Lutz", "0596513984",
                             "O'Reilly")
                        ]
 
        data = self.products + product_list
        self.dataOlv.SetObjects(data)
 
    #----------------------------------------------------------------------
    def setBooks(self, data=None):
        """
        Sets the book data for the OLV object
        """
        self.dataOlv.SetColumns([
            ColumnDefn("Title", "left", 220, "title"),
            ColumnDefn("Author", "left", 200, "author"),
            ColumnDefn("ISBN", "right", 100, "isbn"),
            ColumnDefn("Mfg", "left", 180, "mfg")
        ])
 
        self.dataOlv.SetObjects(self.products)
 
    #----------------------------------------------------------------------
    def updateTooltip(self, event):
        """
        Update the tooltip!
        """
        pos = wx.GetMousePosition()
        mouse_pos = self.dataOlv.ScreenToClient(pos)
        item_index, flag = self.dataOlv.HitTest(mouse_pos)
        print flag
        if flag == wx.LIST_HITTEST_ONITEMLABEL:
            msg = "%s is a good book!" % self.dataOlv.GetItemText(item_index)
            self.dataOlv.SetToolTipString(msg)
        else:
            self.dataOlv.SetToolTipString("")
 
        event.Skip()
 
########################################################################
class MainFrame(wx.Frame):
    #----------------------------------------------------------------------
    def __init__(self):
        wx.Frame.__init__(self, parent=None, id=wx.ID_ANY, 
                          title="ObjectListView Demo", size=(800,600))
        panel = MainPanel(self)
 
#----------------------------------------------------------------------
def main():
    """
    Run the demo
    """
    app = wx.App(False)
    frame = MainFrame()
    frame.Show()
    app.MainLoop()
 
#---------------------------------------------------------------------- 
if __name__ == "__main__":
    main()

The primary change here is to remove the setToolTip method and add an updateTooltip method. In said method, we grab the mouse position and us it to update the tooltip. The issue I have with this approach is that the tooltip only updates when you mouse over the cells in the first column. Otherwise it works quite well. If you happen to discover some other method to make this work, let me know in the comments.

Update (2014/01/23):

One of my readers contacted me today with another fix to this code. He managed to figure out how to add the tooltip such that the tooltip will appear not matter which part of the row you mouse over. The change is very simple. The change is to the updateTooltip method. Here is the original version:

def updateTooltip(self, event):
    """
    Update the tooltip!
    """
    pos = wx.GetMousePosition()
    mouse_pos = self.dataOlv.ScreenToClient(pos)
    item_index, flag = self.dataOlv.HitTest(mouse_pos)
    print flag
    if flag == wx.LIST_HITTEST_ONITEMLABEL:
        msg = "%s is a good book!" % self.dataOlv.GetItemText(item_index)
        self.dataOlv.SetToolTipString(msg)
    else:
        self.dataOlv.SetToolTipString("")
 
    event.Skip()

Here is the fixed version:

def updateTooltip(self, event):
    """
    Update the tooltip!
    """
    pos = wx.GetMousePosition()
    mouse_pos = self.dataOlv.ScreenToClient(pos)
    item_index, flag = self.dataOlv.HitTest(mouse_pos)
    print flag
    if item_index != -1:
        msg = "%s is a good book!" % self.dataOlv.GetItemText(item_index)
        self.dataOlv.SetToolTipString(msg)
    else:
        self.dataOlv.SetToolTipString("")
 
    event.Skip()

The change was just to the if statement in that we now look at the item_index. I actually don’t know why this works, but it does for me. Special thanks goes out to Mike Stover for figuring this out.

Print Friendly