Showing Tooltips/Icons for Status Bar Panes
Environment: VC6
Contents
- Introduction to the MMStatusBar Class
- How to Use the MMStatusBar Class
- Miscellaneous Public Methods of MMStatusBar
- Miscellaneous Public Methods to Change Properties of Panes
- How to Integrate MMStatusBar into Your Project
- Implementation Details of the MMStatusBar Class
- About the Included Demo Project
- Acknowledgments
- Downloads
Introduction
Of late, I had seen some questions going back and forth in the forums about handling tooltips for status bar panes. Hence, I sat down to put together a class to simplify this process to make it easy to support this in a MFC framework.
The class, which will do this, is MMStatusBar, derived from MFC's CStatusBar. One can use MMStatusBar to do any of the following:
- Set tooltips for individual panes.
- Set icons for individual panes.
- Change the pane text and/or tooltips for individual panes at runtime.
- Change the icon for individual panes at runtime.
- Use double-clicking on individual panes to generate a WM_COMMAND message to the parent window.
How to go about implementing each of these using the MMStatusBar class is explained below.
How to...
Set tooltips for individual panes?
MMStatusBar exposes a public method
Set icons for individual panes?
If you have read the steps for supporting tooltips for panes, you will note that the approach followed to set icons is similar to the one to set tooltips.Change the pane text and/or tooltips for individual panes at runtime?
Change the icon for individual panes at runtime?
Trap WM_COMMAND message generated by double-clicking on the panes?
BOOL SetIndicators(const UINT* lpIDArray,int nIDCount, const UINT* lpIDToolTipArray = NULL, const HICON* lphIconArray = NULL);
To set tooltips for individual panes, you'll have to pass an array of string resource IDs in the lpToolTipArray parameter. If no, the tooltip is to be associated with a pane. Simply set the corresponding array element to 0.
When to use this approach:
One would use this approach when one just has to set the tooltips one time and the tooltips are static throughout the lifetime of the window.
Example:
In the sample source code, I want to set the tooltips for Pane 1, Pane 2, and Pane 3 only. So, I maintain an array such as:
static UINT tooltipIndicators[] =
{
0, // status line indicator
IDS_PANE1TIP, // Pane 1 tooltip resource ID
IDS_PANE2TIP, // Pane 2 tooltip resource ID
IDS_PANE3TIP_WITHICON, // Pane 3 tooltip resource ID
0,
0,
0,
};
and pass this array in the call as shown below:
if (!m_wndStatusBar.Create(this) ||
!m_wndStatusBar.SetIndicators(indicators,
sizeof(indicators)/sizeof(UINT),tooltipIndicators,
hIconArray))
{
TRACE0("Failed to create status bar\n");
return -1; // fail to create
}
Note: The first two parameters are identical to CStatusBar. We do not want to alter the existing behavior of CStatusBar and only want to extend it.
MMStatusBar exposes a public method.
BOOL SetIndicators(const UINT* lpIDArray,int nIDCount,
const UINT* lpIDToolTipArray = NULL,
const HICON* lphIconArray = NULL);
To set icons for individual panes, you'll have to pass an array of HICONs (HANDLE to icon) in the lphIconArray parameter. If no icon is to be associated with a pane, simply set the corresponding array element to NULL.
When to use this approach:
One would use this approach when one just has to set the tooltips one time and the tooltips are static throughout the lifetime of the window.
Example:
In the sample source code, I want to set the icon for Pane 3 to the application icon. So, I maintain an array, say:
static HICON hIconArray[] =
{
NULL, // status line indicator
NULL,
NULL,
NULL,
NULL,
NULL,
NULL,
};
and pass this array in the call as shown below:
Set the Pane's fourth element to:
HINSTANCE hInst = AfxFindResourceHandle(MAKEINTRESOURCE
(IDR_MAINFRAME),
RT_GROUP_ICON);
m_hTooltipIcon = (HICON)LoadImage(hInst,
MAKEINTRESOURCE(IDR_MAINFRAME),
IMAGE_ICON,16,16,
LR_DEFAULTCOLOR);
hIconArray[3] = m_hTooltipIcon;
if (!m_wndStatusBar.Create(this) ||
!m_wndStatusBar.SetIndicators(indicators,
sizeof(indicators)/sizeof(UINT),tooltipIndicators,hIconArray))
{
TRACE0("Failed to create status bar\n");
return -1; // fail to create
}
Note: The first two parameters are identical to CStatusBar. We do not want to alter the existing behavior of CStatusBar and only want to extend it.
In some scenarios, you may want to change the tooltip text based on certain situations. To do this, you will have to handle UPDATE_COMMAND_UI for the pane ID in one of the classes in the command routing sequence. I do it in CMainFrame in the sample code. The steps are given below:
Add an UPDATE_COMMAND_UI for the pane ID, i.e. ID_MYPANE_2:
ON_UPDATE_COMMAND_UI(ID_MYPANE_2,OnUpdateStatusPane2)
In case of idle processing, the OnUpdateStatusPane2 handler will receive a CCmdUI* pointer whose m_pMenu will be set to MM_STATUS_PANE_UPDATE. If that is the case, the CCmdUI* is actually a MMStatusCmdUI* pointer and one can use the MMStatusCmdUI's helper methods to set the tooltiptext. The code below explains this:
void CMainFrame::OnUpdateStatusPane2(CCmdUI* pCmdUI)
{
// check if it is an Idle processing request
BOOL bIsToolTipUpdate = FALSE;
if(MM_STATUS_PANE_UPDATE == (int)pCmdUI->m_pMenu)
bIsToolTipUpdate = TRUE; //yes, it is!!!
MMStatusCmdUI* pStatusUI = (MMStatusCmdUI*)pCmdUI;
// pane 2's text as well as tooltips are dynamic; hence,
// special case here
if(bIsToolTipUpdate)
{
pStatusUI->SetText(m_szPane2Text);
pStatusUI->SetToolTipText(m_szPane2TooltipText);
}
else
pCmdUI->ContinueRouting();
}
In some scenarios, you may want to change the icon or remove the icon based on certain situations. To do this, you will have to handle UPDATE_COMMAND_UI for the pane ID in one of the classes in the command routing sequence. I do it in CMainFrame in the sample code. The steps are given below:
Add an UPDATE_COMMAND_UI for the pane ID i.e. ID_MYPANE_3:
ON_UPDATE_COMMAND_UI(ID_MYPANE_3,OnUpdateStatusPane3)
In case of idle processing, the OnUpdateStatusPane3 handler will receive a CCmdUI* pointer whose m_pMenu will be set to MM_STATUS_PANE_UPDATE. If that is the case, the CCmdUI* is actually a MMStatusCmdUI* pointer and one can use the MMStatusCmdUI's helper methods to set the tooltiptext. The code below explains this:
void CMainFrame::OnUpdateStatusPane3(CCmdUI* pCmdUI)
{
// check if it is an Idle processing request
BOOL bIsToolTipUpdate = FALSE;
if(MM_STATUS_PANE_UPDATE == (int)pCmdUI->m_pMenu)
bIsToolTipUpdate = TRUE; //yes, it is!!!
MMStatusCmdUI* pStatusUI = (MMStatusCmdUI*)pCmdUI;
// pane 3 has an icon associated with it
if(bIsToolTipUpdate)
{
if(FALSE == m_bShowIcon)
{
pStatusUI->SetIcon(NULL); // this removes the icon
szTooltipText.LoadString(IDS_PANE3TIP_NOICON);
pStatusUI->SetToolTipText(szTooltipText);
}
else
{
pStatusUI->SetIcon(m_hTooltipIcon);
szTooltipText.LoadString(IDS_PANE3TIP_WITHICON);
pStatusUI->SetToolTipText(szTooltipText);
}
}
else
pCmdUI->ContinueRouting();
}
You may want to do some action when the user double-clicks on one of the panes. The MMStatusBar class assists handling it in an elegant way by generating a WM_COMMAND with the ID equal to the pane ID. It posts the WM_COMMAND message to its parent window.
To handle this, you'll have to add an ON_COMMAND handler for the pane ID in one of the class's message maps. In the sample code, I pop up a message box when I receive a WM_COMMAND for Pane 1. The steps are given below:
Add a ON_COMMAND message map entry for the pane ID:
ON_COMMAND(ID_MYPANE_1,OnPane1)
In the handler OnPane1, add code for how you would want to handle the command.
void CMainFrame::OnPane1()
{
AfxMessageBox(_T("Pane 1 doubelclicked"));
}
Miscellaneous Public Methods of MMStatusBar
Apart from the CStatusBar public methods, MMStatusBar has a few more helper methods that can be useful.
void SetDelayTime(UINT uDelayTime);
This method can be used to set the tooltip delay time. This is the time (in milliseconds) for which the mouse has to be over a pane before the tooltip can appear (if the pane has tooltip text).
void SetPaneTextColor(int nIndex,COLORREF crTextColor);
This method can be used to set the color of the text of a particular pane. It works by sending a SB_SETTEXT message to the statusbar with the SBT_OWNERDRAW set. The drawing is handled in the the MMStatusBar's DrawItem method.
void SetPaneTextBkColor(int nIndex,COLORREF crTextBkColor);
This method can be used to set the color of text background of a particular pane. It works by sending a SB_SETTEXT message to the statusbar with the SBT_OWNERDRAW set. The drawing is handled in the the MMStatusBar's DrawItem method.
Miscellaneous Public Methods of MMStatusCmdUI
virtual void Enable(BOOL bOn);
To enable or disable a pane. The pane text will not appear in that case.
virtual void SetCheck(int nCheck);
If nCheck is non-zero, will make the pane pop out.
virtual void SetWidth(int cxWidth);
Can be used to set the pane width in pixels.
MMStatusBar overrides the OnUpdateCmdUI method and uses this to pass an UPDATE_COMMAND_UI message to each of the panes.
How does MMStatusBar class do all these? Let us take it one by one. Creation: The MMStatusBar class traps WM_CREATE and creates the m_oToolTip tooltip control and activates it. Setting tooltip text and icons: In its SetIndicators method, MMStatusBar calls CStatusBar::Setindicators to let the baseclass do the preparation of the panes. It then checks to see whether it was passed a lpTooltipArray, and if so, adds a tool to the tooltip control specifying the pane rectangle and the ID for tooltip text as shown below. That makes the tooltip control show the tooltip whenever the mouse hovers over and stays within the rectangle. Icons are added in a similar way. Note that when there is an icon associated with a pane, one would have to add the icon's width to the pane width and set the new pane width as is done below. Considerations during resizing: When the status bar is resized, the toolrectanges have to be recomputed so the tooltips appear in the proper panes. The same scenario exists for cases when the pane width changes for some reason. It may be when one sets a new pane text or when an icon is shown and removed. The MMStatusBar::Redraw method does this recomputing. It loops through all the panes, gets the pane rectangles, and sets the new tool rectangles. The Redraw method is called from MMStatusCmdUI::SetWidth for this exact same reason. Any change in pane width should result in a recomputing of the tool rectangles. How does MMStatusBar enable pane texts, tooltips, and icons to be updated through UPDATE_COMMAND_UI? MMStatusBar overrides the OnUpdateCmdUI() virtual method. This method gets called by the framework during idle processing. So, I figured out that this would be a good place to handle updating the tooltips and pane texts. In this handler, MMStatusBar will instantiate a MMStatusCmdUI object and set its values. The most important thing here is state.m_pMenu = (CMenu*)MM_STATUS_PANE_UPDATE. This is used by the UPDATE_COMMAND_UI handler to know that this is a status bar update and so it can handle it appropriately. The reason for doing this is that if one has a menu with the same ID as the pane, UPDATE_COMMAND_UI would come in two forms for the same command ID: one when the status bar needs to be udpated, and another when the menu status for the command ID needs to be updated. How does MMStatusBar generate WM_COMMAND on double-clicking in any pane? To acheive this, MMStatusBar overrides the virtual function OnChildNotify(). If the notification received is a NM_DBLCLK, it gets the command ID for the item and then posts a WM_COMMAND message to the statusbar's parent window. And the one single line which makes all this possible... is below. For tooltips to show, all mouse events on the statusbar have to be relayed to the tooltip control. Otherwise, the tooltip control will not be aware of when to show tooltips. This is because we do not use the TTF_SUBCLASS flag. So, MMStatusBar overrides another virtual function, PreTranslateMessage, and relays all mouse events to the tooltip control. I hope this provided a small insight to the inner workings of MMStatusBar. I have tried to provide as many comments as possible in the source code. The code can be used with UNICODE or non-UNICODE settings. The demo project included shows a way of implementing all of those listed above.How to Integrate MMStatusBar into Your Project
Implementation Details of the MMStatusBar Class
for(int i =0;i<nIDCount;i++) // each (new) indicator
{
CRect oRect;
GetItemRect(i,&oRect);
UINT uID = lpIDArray[i];
UINT uIDText = lpIDToolTipArray[i];
if (uID != ID_SEPARATOR && // have indicator and
uIDText != 0) // want tooltip text for it
VERIFY(m_oToolTip.AddTool(this,uIDText,oRect,uID));
}
for(int i =0;i<nIDCount;i++) // each (new) indicator
{
UINT uID = lpIDArray[i];
if (uID != ID_SEPARATOR && // have indicator and
lphIconArray[i] != NULL) // want icon
{
GetStatusBarCtrl().SetIcon(i,lphIconArray[i]);
int nWidth;
UINT nID,nStyle;
GetPaneInfo(i,nID,nStyle,nWidth);
SetPaneInfo(i,nID,nStyle,nWidth +
GetSystemMetrics(SM_CXSMICON));
}
}
void MMStatusBar::Redraw()
{
CToolInfo oToolInfo;
memset(&oToolInfo,0,sizeof(TOOLINFO));
oToolInfo.cbSize = sizeof(TOOLINFO) ;
CRect oRect;
UINT uId;
for(int i = 0 ; i < m_nCount ; i++)
{
uId = GetItemID(i);
GetItemRect(i,&oRect);
oToolInfo.hwnd = this->GetSafeHwnd();
oToolInfo.uId = uId;
oToolInfo.rect.left = oRect.left;
oToolInfo.rect.right = oRect.right;
oToolInfo.rect.top = oRect.top;
oToolInfo.rect.bottom = oRect.bottom;
m_oToolTip.SendMessage(TTM_NEWTOOLRECT,0,(LPARAM)
(TOOLINFO*)&oToolInfo);
}
}
void MMStatusBar::OnUpdateCmdUI(CFrameWnd* pTarget,
BOOL bDisableIfNoHndler)
{
MMStatusCmdUI state;
state.m_pOther = this;
state.m_pMenu = (CMenu*)MM_STATUS_PANE_UPDATE;
state.m_nIndexMax = (UINT)m_nCount;
for (state.m_nIndex = 0; state.m_nIndex < state.m_nIndexMax;
state.m_nIndex++)
{
state.m_nID = GetItemID(state.m_nIndex);
// allow the statusbar itself to have update handlers
if (CWnd::OnCmdMsg(state.m_nID, CN_UPDATE_COMMAND_UI,
&state, NULL))
continue;
// allow target (owner) to handle the remaining updates
state.DoUpdate(pTarget, FALSE);
}
// update the dialog controls added to the status bar
UpdateDialogControls(pTarget, bDisableIfNoHndler);
}
BOOL MMStatusBar::PreTranslateMessage(MSG *pMsg)
{
switch(pMsg->message)
{
case WM_MOUSEMOVE:
case WM_LBUTTONDOWN:
case WM_LBUTTONUP:
case WM_MBUTTONDOWN:
case WM_MBUTTONUP:
case WM_RBUTTONDOWN:
case WM_RBUTTONUP:
m_oToolTip.RelayEvent (pMsg);
default :break;
}
return CStatusBar::PreTranslateMessage(pMsg);
}
About the Included Demo Project
Acknowledgements
Thanks to all the CodeGuru readers for their encouraging comments and suggestions. These comments encouraged me to provide an update with any bug fixes and improvements.
- Update 1. Thanks to Roberto Cagnetti, Mark Williams, Robin Bannister, and Jose Ramos for providing their useful comments, which I have attempted to incorporate in this update. They were mainly bug fixes.
- Update 2. Thanks to zephyrer and matro for their suggestions. I have tried to address their concerns in this update. zephyrer's suggestion lead to a feature enhancement to the MMStatusBar class to support setting of text color and background color for individual panes.

Comments
There are no comments yet. Be the first to comment!