Updates
Even though it is a tiny but handy utility class to support TitleTip for Edit, ListBox and ComboBox controls with only a few lines of code, it has been limited in usage due to ATL/WTL dependency. Most of all, it exhibits a problem in non-WIN32/x86 systems (i.e. x64) since the custom ATL-style thunk (MessageHook.hpp) -- which I used in the original code -- is not NX/DEP-compatible. It can thus throw an exception in the 64-bit Windows system.
x64 Support and NX/DEP Compatibility
I completely rewrote the instance sub-classing code to deprecate the old custom thunk, and now it steals the thunk definition from ATL. As result, it is NX/DEP compatible if compiled against ATL8 or higher and can support various platforms (x86/x64/IA64/alpha...) that ATL supports. Even though it is designed to compile against ATL3 or ATL7 as well, such binaries will suffer from all the issues which legacy ATL binaries do nowadays and are not NX/DEP compatible.
Stealing the ATL thunk definition does not imply ATL dependency at the binary level, but only at the source level. Two ATL header files, atlbase.h and atlwin.h, will be included into the project. If not done yet, when MessageHook.hpp is being included.
C API-style DLL Version Porting
By encapsulating class into DLL and exporting C-style API functions, it has potential to be used in many other programming languages, not only in C++. It is implemented as a handle/body idiom. The DLL binaries attached here are compiled against ATL8 and are thus NX/DEP compatible. Alternatively, you can create your own DLL for the platform of your interest using the DLL project called "TipTMan"' in the attached demo project files. Please note that the method of deriving from CTitleTipHandlerT<T>
in C++ is still valid and works well.
C API Example
Using DLL functions is as simple as shown below:
#include "TTipMan.h"
# pragma message("Auto link to TTipMan.lib")
# pragma comment(lib, "TTipMan.lib")
BOOL CTestDlg::OnInitDialog()
{
CDialog::OnInitDialog();
HANDLE hTTMan = CreateTitleTipHandler( GetSafeHwnd() );
AddTitleTipEdit( hTTMan, m_Edit.GetSafeHwnd(), RGB(255, 255, 0), RGB(0, 0, 0) );
}
CreateTitleTipHandler()
and DestroyTitleTipHandler()
have been added to implement handle/body idioms in the DLL version and all the existing functions are modified to take the additional HANDLE
returned by CreateTitleTipHandler()
.
C Functions Exported from DLL
#define COLOR_DEFAULT (DWORD)0xffffffff
HANDLE WINAPI CreateTitleTipHandler(HWND hwnd);
void WINAPI DestroyTitleTipHandler(HANDLE handle);
BOOL WINAPI AddTitleTipEdit(HANDLE handle, HWND hwndEdit,
COLORREF crTipTextColor = COLOR_DEFAULT,
COLORREF crTipBkColor = COLOR_DEFAULT);
void WINAPI DelTitleTipEdit(HANDLE handle, HWND hwndEdit);
BOOL WINAPI AddTitleTipListBox(HANDLE handle, HWND hwndListBox,
COLORREF crTipTextColor = COLOR_DEFAULT,
COLORREF crTipBkColor = COLOR_DEFAULT,
COLORREF crTipTextColorSelection = COLOR_DEFAULT,
COLORREF crTipBkColorSelection = COLOR_DEFAULT);
void WINAPI DelTitleTipListBox(HANDLE handle, HWND hwndListBox);
BOOL WINAPI AddTitleTipComboBox(HANDLE handle, HWND hwndComboBox,
COLORREF crTipTextColor = COLOR_DEFAULT,
COLORREF crTipBkColor = COLOR_DEFAULT,
COLORREF crTipTextColorSelection = COLOR_DEFAULT,
COLORREF crTipBkColorSelection = COLOR_DEFAULT);
void WINAPI DelTitleTipComboBox(HANDLE handle, HWND hwndComboBox);
HWND WINAPI GetHwndToolTip(HANDLE handle);
LPTSTR WINAPI GetToolTipBuf(HANDLE handle);
UINT WINAPI GetToolTipBufLen(HANDLE handle);
void WINAPI ResetToolTipBuf(HANDLE handle);
LPTSTR WINAPI ReallocToolTipBuf(HANDLE handle, UINT nLen);
-- End of Updates --
Introduction
While both the List View control and the Tree View control have easy-to-use built-in support for TitleTips (ToolTips), the other most commonly used three (Edit, List Box and Combo Box controls) don't have similar support at all. You need to create your own support using a ToolTip control. "TitleTips are tips that elongate a truncated item in a control so that you can see all of the item. For instance, Visual Studio has TitleTips in its Project Workspace. If a class name in the Project Workspace is too long to see all of it, a TitleTip appears that displays the full text. This makes it possible to use the Project Workspace without scrolling horizontally and without having to make it wider." For basic and more detail about ToolTips and TitleTips, refer to this article in MSJ.
When I was writing a small code to support TitleTips for the Edit control and List Box control in my Dialog Box, I hit upon the idea that it might be possible to create a generic mix-in class to support TitleTips for these controls. Actually, it wasn't as simple as I thought it would be in the beginning, since the ToolTip control's behavior for *fine* tuning wasn't well documented in MSDN nor anywhere else and, even worse, it behaves slightly differently according to the common control's version. Sigh~ .0
Background
The CMessageHook
class was intensively used while implementing this mix-in class. The CMessageHook
class is my own small but handy class to subclass window message procedures. It uses an assembly thunk technique that is identical to that of ATL, but purely WIN32. Thus, it can be used in any WIN32 platform (Intel CPU only). I wrote this class myself to understand the ins-and-outs of ATL and the thunk technique and I have used it in my several projects successively. I would rather say that I copied-'n-pasted the good features of a similar assembly's thunk implementations of others into my class :)
Now back to the TitleTip issue: I might have been able to implement it using a tracking ToolTip control (TTF_TRACK
flag). Then I would implement my own custom timer and WM_MOUSEMOVE
message handler, activate/deactivate tips (TTM_TRACKACTIVATE
) after positioning tips on the right location and all that. There is very good example of this at The Code Project by Hans Dietrich. Instead, however, I decided to stick to the traditional TTN_GETDISPINFO
(which used to be TTN_NEEDTEXT
in olden times) and TTN_SHOW
notification handling method simply because I am used to this method. Therefore I could leverage the built-in functions of the ToolTip control such as timer (TTM_SETDELAYTIME
), automatic subclassing (TTF_SUBCLASS
) and so on. That is why I thought it would be as simple as writing regular ToolTips. Unfortunately, I was wrong and I will explain the problems with which I was confronted during my implementation below.
Implementation Note
1. Using the TTF_TRANSPARENT
Flag
As a nature of TitleTips, the tip will be laid over the tool window completely when it is being shown. In other words, at the moment of a tip's appearance, the tool window will stop receiving mouse messages. In obedience to this, it sends a TTM_POP
message to the tip. In turn, the tip will be disappear, which makes the tool window start to receive mouse messages again. This causes the tip to be shown again and again and again.
We will see the tip blinking like crazy. The TTF_TRANSPARENT
flag comes into play to solve this problem and, when specified while adding tool info (TOOLINFO.uFlags
), it makes the tip re-direct certain mouse messages to the tool window so that the tool window can see the mouse message. This makes the tip remaining to be shown on the screen without blinking, even though the tool window is completely covered by the tip window.
Now you can try it on a Win2K or Win98 machine. The tip will be shown nicely and stay without blinking. You can click the left mouse button on the tip and it will disappear. Thus, you can perform a regular task (selecting a range of text, choosing a list item and so on).
Ok fine, now you can try it on WinXP with common control version 6. The story goes the same until you try to click on the tip. Unfortunately, the tip will remain shown on the screen even though you clicked, double-clicked or triple-clicked it. Not being disappeared on WinXP is very annoying, so I decided to change this behavior so that it becomes of one accord, regardless of platforms.
class CToolTipCtrlHook : public CMessageHook
{
public:
CToolTipCtrlHook(BOOL bAutoDelete) : CMessageHook(bAutoDelete) { }
protected:
protected:
virtual BOOL ProcessWindowMessage(UINT uMsg, WPARAM wParam,
LPARAM lParam, LRESULT &lResult)
{
BOOL bHandled = FALSE;
switch(uMsg)
{
case WM_LBUTTONDOWN:
case WM_MBUTTONDOWN:
case WM_RBUTTONDOWN:
case WM_XBUTTONDOWN:
{
TOOLINFO ti;
ti.cbSize = sizeof(TOOLINFO);
::SendMessage(GetHwnd(), TTM_GETCURRENTTOOL, 0, (LPARAM)&ti);
::SendMessage(GetHwnd(), TTM_POP, 0, 0);
POINT ptTarget = { GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam) };
::ClientToScreen(GetHwnd(), &ptTarget);
::ScreenToClient((HWND)ti.uId, &ptTarget);
lResult = ::SendMessage((HWND)ti.uId, uMsg, wParam,
MAKELPARAM(ptTarget.x, ptTarget.y));
bHandled = TRUE;
}
break;
}
return bHandled;
}
};
2. Handling the TTN_GETDISPINFO
Notification
TitleTips are different from regular tips in the way that they appear only if the length of text on tool window being displayed is wider than its displaying area (mostly the client rectangle of the control window) so that some portion of the text is being clipped. If the tool window can show complete text without any clipping, TitleTip should not be shown. In the moment of receiving TTN_SHOW
, it is too late to determine the to show or not to show tip and the tip will be shown somewhere onscreen at least once, even though you try to pop it right away in the TTN_SHOW
notification handler (it will blink).
In the TTN_GETDISPINFO
notification hander, NMTTDISPINFO.lpszText
needs to be set to a pointer to a private buffer for the text displayed in tip. NMTTDISPINFO.szText
is limited to being used for text less than 80 char
s in its length. Thus, it is not suitable for our purpose. So, if we don't want to show the tip onscreen, we can simply set NULL
to NMTTDISPINFO.lpszText
-- in the case of the text being clipped -- in the TTN_GETDISPINFO
notification handler and return.
It sounds very simple, but there is a hidden pitfall in this simple method. Let's say we have only one edit control in our dialog box and we installed only one tip for the only edit control we have. If the edit control's text is being clipped, we can see the tip pop and pop up as intended when we move the mouse cursor over or leave the mouse cursor from the edit control. However, in the case that the text is not being clipped, our TTN_GETDISPINOF
notification handler will set NMTTDISPINFO.lpszText
as NULL
and return. As a result, we won't be able to see any tip onscreen.
Let's type more characters in the edit control so that the text gets clipped again. Moving the cursor over the edit control, you will be surprised that no tip is popped up anymore, even though the text is being clipped (basically, no more TTN_GETDISPINFO
notifications are issued). If we have more than one control and have installed more than one tip, then we can move the mouse cursor over from one tool window to the other tool window. This will cause the TTN_GETDISPINFO
notification to be issued again when some condition has been met. However, you will still see in some cases that the tip doesn't pop again as it should.
I spent quite a large amount of time on finding out the solution to this and, ironically, it was found to be so simple when I finally got it. You need to *post* a TTM_POP
message to the ToolTip control window in order to pop the tip explicitly if you set NMTTDISPINFO.lpszText
as NULL
in the TTN_GETDISPINFO
notification handler. I emphasized the word *post* since if you send a message using ::SendMessage()
instead of using ::PostMessage()
in the TTN_GETDISPINFO
notification handler, it will not work well in WinXP common control version 6.
LRESULT OnToolTipNotifyGetDispInfo(int nID,
LPNMHDR lpnmhdr, BOOL& bHandled)
{
bHandled = TRUE;
LPNMTTDISPINFO lpnmtdi= reinterpret_cast<LPNMTTDISPINFO>(lpnmhdr);
HWND hwndTool = reinterpret_cast<HWND>(lpnmtdi->hdr.idFrom);
...
if(m_bTextClipped)
{
...
lpnmhdr.lpszText = pszMyPrivateBufer;
}
else
{
lpnmhdr.lpszText = NULL;
::PostMessage(m_hwndToolTip, TTM_POP, 0, 0L);
}
return 0;
}
3. Sharing a ToolTip Control and Private Storage for Tip Text
We can not only have as many ToolTip control windows as we want, but we can also have as many private storage spaces for the corresponding individual ToolTip control. But think again. Do we really use more than one ToolTip control at an any given moment? No. Only one tip whose tool window the mouse cursor is on will be activated. So, why don't we just use only one ToolTip control window and private storage for all the TitleTip support and even give you the chance to share them in any place you want? Let's save some resource! Regardless of how many tool windows exist, one and only ToolTip control window and private storage for tip text will be created globally in my mix-in class. I used the generic singleton technique to implement this.
Use the following public member function of the CTitleTipHandler<T>
template class to share the ToolTip control window and private storage in the same dialog, which is derived from the CTitleTipHandler<T>
template class (mixed-in class):
CToolTipCtrl &GetToolTipCtrl();
LPTSTR GetToolTipBuf();
int GetToolTipBufLen() const;
void ResetToolTipBuf();
LPTSTR ReallocToolTipBuf(int nLen);
...or use the following counterpart public class member function of the CSSTip
singleton class to share the ToolTip control window and private storage in any place:
using codeproject::CSSTip;
static CToolTipCtrl &CSSTip::ToolTipCtrl();
static LPTSTR CSSTip::GetToolTipBuf();
static int CSSTip::GetToolTipBufLen() const;
static void CSSTip::ResetToolTipBuf();
static LPTSTR CSSTip::ReallocToolTipBuf(int nLen);
4. Nested Instance Subclassing Issue
As I mentioned earlier, I intensively used my own handy CMessageHook
class to subclass window procedures. Any control that will get TitleTip support from my mix-in class will be subclassed when its tool info is being added to the ToolTip control. If you are using a control window attached to the ATL::CWindowImpl<T>
-derived class, the control is also subclassed internally by ATL implementation. When two or more instance subclassing occasions are involved, their subclass and unsubclass order becomes very important. The subclasses must be removed in reverse of the order in which they were performed. Otherwise, you will screw yourself.
Usually, an instance subclassing class implementator takes such a situation into his/her implementation consideration and puts minimal safety into their implementation. For example, CMessageHook
doesn't delete itself when the subclass/unsubclass order gets mixed up and start parasitizing to windows it subclassed (it automatically deletes itself on receiving the WM_NCDESTROY
message later). However, it is a really bad idea to mix up subclass and unsubclass order anyway, so remember and try to avoid it by some means or other.
class CListBoxImpl : public CWindowImpl<CListBoxImpl, CListBox>
{
public:
BEGIN_MSG_MAP(CListBoxImpl)
END_MSG_MAP()
};
using codeproject::CTitleTipHandler;
class CMainDlg : public CDialogImpl<CMainDlg>,
public CTitleTipHandler<CMainDlg>
{
public:
enum { IDD = IDD_MAINDLG };
BEGIN_MSG_MAP(CMainDlg)
CHAIN_MSG_MAP(CTitleTipHandler<CMainDlg>)
MESSAGE_HANDLER(WM_INITDIALOG, OnInitDialog)
...
END_MSG_MAP()
protected:
CListBoxImpl c_lbList;
public:
LRESULT OnInitDialog(UINT , WPARAM ,
LPARAM , BOOL& )
{
...
HWND hwndList = NULL;
{
c_lbList.SubclassWindow(GetDlgItem(IDC_LIST));
hwndList = c_lbList.UnsubclassWindow();
}
{
c_lbList.SubclassWindow(hwndList);
c_lbList.AddString(_T(
"0. Long String - TitleTip for EDIT, LISTBOX & COMBOBOX controls"));
c_lbList.AddString(_T("1. Short String - Hello"));
c_lbList.AddString(_T("2. Short String - Hello"));
{
AddTitleTipListBox(c_lbList);
DelTitleTipListBox(c_lbList);
}
hwndList = c_lbList.UnsubclassWindow();
}
{
AddTitleTipListBox(hwndList);
{
c_lbList.SubclassWindow(hwndList);
c_lbList.AddString(_T(
"3. Long String - TitleTip for EDIT, LISTBOX & COMBOBOX controls"));
c_lbList.AddString(_T(
"4. Long String - TitleTip for EDIT, LISTBOX & COMBOBOX controls"));
}
}
...
return TRUE;
}
...
}
Using the Code
It is very easy to use. The first thing to do is add CTitleTipHandler<T>
as a base class of your dialog class (mixed-in) and then chain to the message map in CTitleTipHandler<T>
. Use the AddTitleTipXXX(HWND hwndTool)
functions or the DelTitleTipXXX(HWND hwndTool)
functions respectively to add or to remove TitleTips support for the specific control (hwndTool
) in your dialog initialization code (OnInitDialog()
).
using codeproject::CTitleTipHandler;
class CMainDlg : public CDialogImpl<CMainDlg>,
public CTitleTipHandler<CMainDlg>
{
public:
enum { IDD = IDD_MAINDLG };
BEGIN_MSG_MAP(CMainDlg)
CHAIN_MSG_MAP(CTitleTipHandler<CMainDlg>)
MESSAGE_HANDLER(WM_INITDIALOG, OnInitDialog)
...
END_MSG_MAP()
protected:
CEdit c_edEdit;
CListBox c_lbList;
CComboBox c_cbCombo;
public:
LRESULT OnInitDialog(UINT , WPARAM ,
LPARAM , BOOL& )
{
...
c_edEdit.Attach(GetDlgItem(IDC_EDIT1));
c_lbListBox.Attach(GetDlgItem(IDC_LIST1));
c_cbComboBox.Attach(GetDlgItem(IDC_COMBO1));
AddTitleTipEdit(c_edEdit.m_hWnd, RGB(0, 0, 255), RGB(255, 255, 255));
AddTitleTipListBox(c_lbList.m_hWnd, RGB(0, 0, 0),
RGB(255, 255, 255), RGB(255, 0, 0), RGB(0, 0, 0));
AddTitleTipComboBox(c_cbCombo.m_hWnd);
...
return TRUE;
}
...
}
Public Functions
BOOL AddTitleTipEdit(HWND hwndEdit, COLORREF crTipTextColor = COLORREF_DEFAULT, COLORREF crTipBkColor = COLORREF_DEFAULT);
- Add TitleTip support for the specified Edit control (
hwndEdit
).
- Default color - tip text:
::GetSysColor(COLOR_WINDOWTEXT)
and tip background: ::GetSysColor(COLOR_WINDOW)
.
- Return
TRUE
if successful.
void DelTitleTipEdit(HWND hwndEdit);
- Remove TitleTip support for the specified Edit control (
hwndEdit
).
BOOL AddTitleTipListBox(HWND hwndListBox, COLORREF crTipTextColor = COLORREF_DEFAULT, COLORREF crTipBkColor = COLORREF_DEFAULT, COLORREF crTipTextColorSelection = COLOR_DEFAULT, COLORREF crTipBkColorSelection = COLOR_DEFAULT);
- Add TitleTip support for the specified ListBox control (
hwndListBox
).
- Default color - tip text:
::GetSysColor(COLOR_WINDOWTEXT)
, tip background: ::GetSysColor(COLOR_WINDOW)
, tip text (selected): ::GetSysColor(COLOR_HIGHLIGHTTEXT)
and tip background (selected): ::GetSysColor(COLOR_HIGHLIGHT)
.
- Return
TRUE
if successful.
void DelTitleTipListBox(HWND hwndListBox);
- Remove TitleTip support for the specified ListBox control (
hwndListBox
).
BOOL AddTitleTipComboBox(HWND hwndComboBox, COLORREF crTipTextColor = COLORREF_DEFAULT, COLORREF crTipBkColor = COLORREF_DEFAULT, COLORREF crTipTextColorSelection = COLOR_DEFAULT, COLORREF crTipBkColorSelection = COLOR_DEFAULT);
- Add TitleTip support for the specified ComboBox control (
hwndComboBox
).
- Default color - tip text:
::GetSysColor(COLOR_WINDOWTEXT)
, tip background: ::GetSysColor(COLOR_WINDOW)
, tip text (selected): ::GetSysColor(COLOR_HIGHLIGHTTEXT)
and tip background (selected): ::GetSysColor(COLOR_HIGHLIGHT)
.
- Return
TRUE
if successful.
void DelTitleTipComboBox(HWND hwndComboBox);
- Remove TitleTip support for the specified ComboBox control (
hwndComboBox
).
CToolTipCtrl &GetToolTipCtrl();
- Return the ToolTip control.
LPTSTR GetToolTipBuf();
- Return the pointer to the buffer (private storage) for tip text.
int GetToolTipBufLen() const
- Return the size of the buffer in
TCHAR
s.
void ResetToolTipBuf();
LPTSTR ReallocToolTipBuf(int nLen);
- Delete the old buffer and reallocate a new buffer of size
nLen
in heap memory.
Requirement
Internet Explorer 5 or later installed - to use TTM_ADJUSTRECT
message.
History
Version 1.10 - 02/14/2008
- Modified c'tor to accept
AutoDelete
flag for DLL version porting.
- Bug-fix:
EM_GETLINE
- copied lines do not contain a null character.
- Bug-fix: size of
TOOLINFO
struct can be different according to the version of the common control used. The correct size is 44 bytes for Win 2000 and Win XP without common controls 6.0, and 48 bytes for XP with common controls 6.0
Version 1.01 - 10/26/2004
- Optimized to make it respond faster (lookup map only once) .
- Fixed all the resource leaks found.
- Some bugs fixed (Edit control related).
Version 1.0 - 10/08/2004
- Initial release on The Code Project.