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.
Pingback: wxPython: Adding Tooltips to ObjectListView | Hello Linux
Pingback: Mike Driscoll: wxPython: Adding Tooltips to ObjectListView | The Black Velvet Room
There is an error in the ‘updateControl’ method, where you extend the list of Book instances with a list of dicts and pass that to ‘SetObjects’. This leads to an error in ‘onSetToolTip’, when the selected item is a dict and doesn’t have an ‘author’ attribute. You should just turn the list of dicts in ‘updateControl’ into a list of Book instances or fix ‘onSetToolTip’ to handle either dict key or attribute access on items.
Yup…that’s definitely an issue. Thanks for catching that. I updated the article with a fix and a note about the error.
Pingback: Best way to show truncated data in ObjectListView cell | QueryPost.com
Pingback: Best way to show truncated data in ObjectListView cell | StackAnswer.com
I found the method HitTestSubItem(mouse_pos) to be useful in returning the column as well as row position for my tooltips. It returns the tuple: (rowIndex, flag, columnIndex). In your code where item_index != 1, it is just saying “as long as the row number is 0 or higher…” set the tooltip.
Thanks for your site. I keep coming back here with my searches.