wxListItem ToolTip

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:
Tooltip Screenshot

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

Posted Wednesday, January 6th, 2010 under wxWidgets.

One comment so far

Leave a Reply