I had a basic need to show Tooltips over individual wxListItems in a wxListCtrl. wxListItems don’t let you assign tooltips, and I couldn’t get the SetData() method to store data on a per-cell basis. There also isn’t any Hover event, or MouseOver event, that we can listen for. I wrote this code to enable ToolTips for each individual cell in a wxListCtrl. First, what has to be solved:
- As you add items to the wxListCtrl, provide a method to add tooltips at the same time
- Listen for a MouseOver event on the wxListCtrl, and determine the X/Y coordinate of the mouse pointer(wxPoint)
- With the wxPoint, determine which cell the mouse is over(by row/col)
- Display a tooltip window with that cell’s tooltip
- Close that tooltip window in a graceful way(proper amount of delay)
This was a lot to do to enable nice tooltips. But I really don’t have much else going on so who cares. In order to solve step 1, I decided to extend wxListCtrl and add some new funcitonality. Here is the code for the ttListCtrl class, I’ll go over it.
| C++ | | copy code | | ? |
| 01 | /* ttListCtrl.h */ |
| 02 | /* Ryan Day, http://www.ryanday.net/ */ |
| 03 | |
| 04 | #include <wx/listctrl.h> |
| 05 | |
| 06 | class ttListCtrl : public wxListCtrl |
| 07 | { |
| 08 | private: |
| 09 | wxString *grid; |
| 10 | int cols, rows; |
| 11 | |
| 12 | public: |
| 13 | ttListCtrl() { }; |
| 14 | ttListCtrl( wxWindow *parent, |
| 15 | wxWindowID winid = wxID_ANY, |
| 16 | const wxPoint& pos = wxDefaultPosition, |
| 17 | const wxSize& size = wxDefaultSize, |
| 18 | long style = wxLC_REPORT, |
| 19 | const wxValidator& validator = wxDefaultValidator, |
| 20 | const wxString &name = wxListCtrlNameStr) |
| 21 | { |
| 22 | // Start with a 3x3 grid, and we can expand beyond that if necessary |
| 23 | cols = rows = 3; |
| 24 | grid = new wxString[3*3]; |
| 25 | Create(parent, winid, pos, size, style, validator, name); |
| 26 | } |
| 27 | |
| 28 | int SetTooltip(int row, int col, wxString& tip); |
| 29 | int GetTooltip(int row, int col, wxString& tip); |
| 30 | }; |
| 31 | |
| 32 | /* ttListCtrl.cpp */ |
| 33 | /* Ryan Day, http://www.ryanday.net/ */ |
| 34 | #include "ttListCtrl.h" |
| 35 | |
| 36 | int ttListCtrl::SetTooltip(int row, int col, wxString& tip) |
| 37 | { |
| 38 | int i, j; |
| 39 | |
| 40 | // If we are placing a tooltip beyond our predefined matrix, |
| 41 | // create a bigger matrix and copy the old matrix over. This |
| 42 | // lets us have a dynamic sized tooltip grid. |
| 43 | if( (row >= rows) || (col >= cols) ) |
| 44 | { |
| 45 | wxString* tmp = grid; |
| 46 | grid = new wxString[(row+5) * (col+5)]; |
| 47 | |
| 48 | for(i=0;i<rows;i++) |
| 49 | for(j=0;j<cols;j++) |
| 50 | grid[(i*cols)+j].Printf(wxT("%s"), tmp[(i*cols)+j]); |
| 51 | delete[] tmp; |
| 52 | rows = row+5; |
| 53 | cols = col+5; |
| 54 | } |
| 55 | // We don't want to store a pointer to the user supplied tooltip, |
| 56 | // we keep our own copy in the grid. |
| 57 | grid[(row*cols)+col].Printf(wxT("%s"),tip); |
| 58 | } |
| 59 | |
| 60 | int ttListCtrl::GetTooltip(int row, int col, wxString& tip) |
| 61 | { |
| 62 | if( &grid[(row*cols)+col] != NULL ) |
| 63 | tip.Printf(wxT("%s"), grid[(row*cols)+col]); |
| 64 | } |
| 65 |
This code does the following:
- Gives us a method to add tooltips to the X/Y (row/col) of the ttListCtrl
- Gives us a method to retrieve that tooltip
- Doesn’t require us to specify dimensions, it will automatically expand as our grid does
That last one was a big step for me. I add information to the grid as it comes in, so I have no idea how large my grid will get.
Next we have to listen for the mouse event, and determine the row/col the mouse is over. Here is the code, and I’ll talk about it.
| C++ | | copy code | | ? |
| 01 | // cut from frame with ttListCtrl object |
| 02 | ttListCtrl* ListCtrlObject = new ttListCtrl(this, wxID_ANY, wxDefaultPosition, wxSize(350,250), wxLC_REPORT | wxLC_HRULES | wxLC_SINGLE_SEL); |
| 03 | |
| 04 | ListCtrlObject->Connect(wxEVT_MOTION, wxMouseEventHandler(ttListCtrl::OnMouseMotion)); |
| 05 | // cut |
| 06 | |
| 07 | /* ttListCtrl.cpp */ |
| 08 | void ttListCtrl::OnMouseMotion(wxMouseEvent& event) |
| 09 | { |
| 10 | ttListCtrl* o = (ttListCtrl*)event.GetEventObject(); |
| 11 | if( o == NULL ) return; |
| 12 | int id = event.GetEventType(); |
| 13 | int count = o->GetColumnCount(); |
| 14 | long row=-1, col=-1; |
| 15 | int flags=0, i=0, totalWidth=0, tmpWidth=0; |
| 16 | wxPoint pt; |
| 17 | wxTipWindow* tipWin; |
| 18 | wxTimer* killTip; |
| 19 | wxString toolTip; |
| 20 | |
| 21 | if( id != wxEVT_MOTION ) |
| 22 | return; |
| 23 | |
| 24 | // We can get the row easily(HitTest) but the columns are more tricky. |
| 25 | // We get all the column widths, and once our total width is beyoind where the |
| 26 | // mouse is, we know which column the mouse is over. This lets us resize the |
| 27 | // columns during runtime and still get correct tooltips. |
| 28 | pt = event.GetPosition(); |
| 29 | row = o->HitTest(pt, flags); |
| 30 | for(i=0;i<count;i++) |
| 31 | { |
| 32 | tmpWidth = o->GetColumnWidth(i); |
| 33 | totalWidth += tmpWidth; |
| 34 | if( pt.x < totalWidth ) |
| 35 | { |
| 36 | col = i; |
| 37 | break; |
| 38 | } |
| 39 | } |
| 40 | |
| 41 | // If things look valid, get the tooltip |
| 42 | if( row > -1 && col > -1) |
| 43 | o->GetTooltip(row, col, toolTip); |
| 44 | |
| 45 | // If we have a tooltip, we want to show is for 1 second, and then disappear. |
| 46 | // We use a timer for this. |
| 47 | if( toolTip.Length() > 0 ) |
| 48 | { |
| 49 | tipWin = new wxTipWindow(o,toolTip); |
| 50 | // Bind() is only avail for 2.9.0 and later |
| 51 | //Bind(wxTimerEvent, Monitor::destroyTip, wxID_ANY, wxID_ANY, q); |
| 52 | SetClientData(tipWin); |
| 53 | Connect(wxEVT_TIMER, wxTimerEventHandler(ttListCtrl::destroyTip), NULL, this); |
| 54 | killTip = new wxTimer(this, wxID_ANY); |
| 55 | killTip->Start(1000, true); |
| 56 | } |
| 57 | } |
| 58 | |
| 59 | void ttListCtrl::destroyTip(wxTimerEvent& event) |
| 60 | { |
| 61 | wxTipWindow *obj = (wxTipWindow*)GetClientData(); |
| 62 | if( obj ) obj->Destroy(); |
| 63 | } |
| 64 |
This event methods will solve the rest of our problems:
- Listen for mouse movement over a cell
- Determine the row/col the mouse is over
- Display the proper tooltip and automatically destroy the window
Now all we need to do is create our ttListCtrl object, add columns and rows, assign the tooltips, and connect a wxEVT_MOTION to the ttListCtrl. Then we have tooltips! Here is a quick example of how to use the class, roughly based on my XBee Monitor program(next posting)
| C++ | | copy code | | ? |
| 01 | packetList = new ttListCtrl(this, wxID_ANY, wxDefaultPosition, wxSize(350,250), |
| 02 | wxLC_REPORT | wxLC_HRULES | wxLC_SINGLE_SEL); |
| 03 | wxListItem item; |
| 04 | |
| 05 | item.SetText(_("Type")); |
| 06 | packetList->InsertColumn(0, item); |
| 07 | item.SetText(_("Size")); |
| 08 | packetList->InsertColumn(1, item); |
| 09 | item.SetText(_("Network")); |
| 10 | packetList->InsertColumn(2, item); |
| 11 | |
| 12 | packetList->SetColumnWidth(0, wxLIST_AUTOSIZE_USEHEADER); |
| 13 | packetList->SetColumnWidth(1, wxLIST_AUTOSIZE_USEHEADER); |
| 14 | packetList->SetColumnWidth(2, wxLIST_AUTOSIZE_USEHEADER); |
| 15 | |
| 16 | wxListItem _item; |
| 17 | wxString tt; |
| 18 | |
| 19 | _item.SetId(0); |
| 20 | _item.SetColumn(0); |
| 21 | _item.SetText(wxT("acol 1")); |
| 22 | tt.Printf("acol1 Tooltip"); |
| 23 | packetList->SetTooltip(0, 0, tt); |
| 24 | packetList->InsertItem(_item); |
| 25 | |
| 26 | _item.SetId(0); |
| 27 | _item.SetColumn(1); |
| 28 | _item.SetText(wxT("acol 2")); |
| 29 | tt.Printf("acol2 Tooltip"); |
| 30 | packetList->SetTooltip(0, 1, tt); |
| 31 | packetList->SetItem(_item); |
| 32 | |
| 33 | // cut ... |
| 34 | |
| 35 | _item.SetId(2); |
| 36 | _item.SetColumn(3); |
| 37 | _item.SetText(wxT("ccol 4")); |
| 38 | tt.Printf("ccol4 Tooltip"); |
| 39 | packetList->SetTooltip(2, 3, tt); |
| 40 | packetList->SetItem(_item); |
| 41 | |
| 42 | packetList->Connect(wxEVT_MOTION, wxMouseEventHandler(ttListCtrl::OnMouseMotion)); |
| 43 |
And a screen shot of its success:

I’ve also attached the entire class just for completeness. I’m new to wxWidgets, so there may be a better way tha tI would love to hear.
| C++ | | copy code | | ? |
| 001 | /* ttListCtrl.h */ |
| 002 | /* Ryan Day, http://www.ryanday.net/ */ |
| 003 | |
| 004 | #include <wx/timer.h> |
| 005 | #include <wx/tipwin.h> |
| 006 | #include <wx/listctrl.h> |
| 007 | |
| 008 | class ttListCtrl : public wxListCtrl |
| 009 | { |
| 010 | private: |
| 011 | wxString *grid; |
| 012 | int cols, rows; |
| 013 | |
| 014 | public: |
| 015 | ttListCtrl() { }; |
| 016 | ttListCtrl( wxWindow *parent, |
| 017 | wxWindowID winid = wxID_ANY, |
| 018 | const wxPoint& pos = wxDefaultPosition, |
| 019 | const wxSize& size = wxDefaultSize, |
| 020 | long style = wxLC_REPORT, |
| 021 | const wxValidator& validator = wxDefaultValidator, |
| 022 | const wxString &name = wxListCtrlNameStr) |
| 023 | { |
| 024 | cols = rows = 3; |
| 025 | grid = new wxString[3*3]; |
| 026 | Create(parent, winid, pos, size, style, validator, name); |
| 027 | } |
| 028 | |
| 029 | int SetTooltip(int row, int col, wxString& tip); |
| 030 | int GetTooltip(int row, int col, wxString& tip); |
| 031 | |
| 032 | void OnMouseMotion(wxMouseEvent& event); |
| 033 | void destroyTip(wxTimerEvent& event); |
| 034 | }; |
| 035 | |
| 036 | /* ttListCtrl.cpp */ |
| 037 | /* Ryan Day, http://www.ryanday.net/ */ |
| 038 | |
| 039 | #include "ttListCtrl.h" |
| 040 | |
| 041 | int ttListCtrl::SetTooltip(int row, int col, wxString& tip) |
| 042 | { |
| 043 | int i, j; |
| 044 | |
| 045 | if( (row >= rows) || (col >= cols) ) |
| 046 | { |
| 047 | wxString* tmp = grid; |
| 048 | grid = new wxString[(row+5) * (col+5)]; |
| 049 | |
| 050 | for(i=0;i<rows;i++) |
| 051 | for(j=0;j<cols;j++) |
| 052 | grid[(i*cols)+j].Printf(wxT("%s"), tmp[(i*cols)+j]); |
| 053 | delete[] tmp; |
| 054 | rows = row+5; |
| 055 | cols = col+5; |
| 056 | } |
| 057 | grid[(row*cols)+col].Printf(wxT("%s"),tip); |
| 058 | } |
| 059 | |
| 060 | int ttListCtrl::GetTooltip(int row, int col, wxString& tip) |
| 061 | { |
| 062 | if( &grid[(row*cols)+col] != NULL ) |
| 063 | tip.Printf(wxT("%s"), grid[(row*cols)+col]); |
| 064 | } |
| 065 | |
| 066 | void ttListCtrl::OnMouseMotion(wxMouseEvent& event) |
| 067 | { |
| 068 | ttListCtrl* o = (ttListCtrl*)event.GetEventObject(); |
| 069 | if( o == NULL ) return; |
| 070 | int id = event.GetEventType(); |
| 071 | int count = o->GetColumnCount(); |
| 072 | long row=-1, col=-1; |
| 073 | int flags=0, i=0, totalWidth=0, tmpWidth=0; |
| 074 | wxPoint pt; |
| 075 | wxTipWindow* tipWin; |
| 076 | wxTimer* killTip; |
| 077 | wxString toolTip; |
| 078 | |
| 079 | if( id != wxEVT_MOTION ) |
| 080 | return; |
| 081 | |
| 082 | pt = event.GetPosition(); |
| 083 | row = o->HitTest(pt, flags); |
| 084 | for(i=0;i<count;i++) |
| 085 | { |
| 086 | tmpWidth = o->GetColumnWidth(i); |
| 087 | totalWidth += tmpWidth; |
| 088 | if( pt.x < totalWidth ) |
| 089 | { |
| 090 | col = i; |
| 091 | break; |
| 092 | } |
| 093 | } |
| 094 | |
| 095 | if( row > -1 && col > -1) |
| 096 | o->GetTooltip(row, col, toolTip); |
| 097 | |
| 098 | if( toolTip.Length() > 0 ) |
| 099 | { |
| 100 | tipWin = new wxTipWindow(o,toolTip); |
| 101 | //Bind(wxTimerEvent, Monitor::destroyTip, wxID_ANY, wxID_ANY, q); |
| 102 | SetClientData(tipWin); |
| 103 | Connect(wxEVT_TIMER, wxTimerEventHandler(ttListCtrl::destroyTip), NULL, this); |
| 104 | killTip = new wxTimer(this, wxID_ANY); |
| 105 | killTip->Start(1000, true); |
| 106 | } |
| 107 | } |
| 108 | |
| 109 | void ttListCtrl::destroyTip(wxTimerEvent& event) |
| 110 | { |
| 111 | wxTipWindow *obj = (wxTipWindow*)GetClientData(); |
| 112 | if( obj ) obj->Destroy(); |
| 113 | } |
| 114 |
