Contents
This class is small, efficient and is compatible with Win32 API programs and MFC programs as well.
Prior to developing my own hyperlink control implementation, I have been looking around on the net in search of already written hyperlink controls. There is a lot around but none were good enough for my standards. Some are bloated with features that I don’t need and want. Some are plagued with bugs. Some are simple but lack one or two features I wished to see. The best that I have found is the excellent code written by Neal Stublen. I have been inspired by the elegance that his solution offers which can be used with Win32 API programs and MFC programs as well. Unfortunately, it has a bug and misses three features I was looking for. For this reason, I decided to write my own hyperlink control code based on Neal Stublen's work. In this article, I will mention hyperlink options that Microsoft offers, describe the features Neal did put in his code, the features that I added, the bug fix I propose to Neal's code and some other improvements that I have added.
After the initial release of this article, some readers pointed out that WTL is offering exactly what I was looking for. In retrospect, I still think that it was the right decision to write my own class. One benefit of WTL over my solution is that it is more complete and has more features. However, since WTL user's goal is to create small and fast executable files, they would benefit from using my class over the WTL one *if* my class provides all the features they need. In some aspects, WTL implementation has some drawbacks that my class doesn't have:
- It has much more code.
- It uses WTL messages dispatching scheme which might be faster and slimmer than the MFC one but it is not as efficient as a pure Window procedure that this class is using.
- It has tooltips but I didn't see how you could customize it for sending the URL text in the status bar as easily as it is with my class.
- You must have WTL in your project to use their hyperlink class. Mine is working with a Win32 API C project, an MFC one and even with WTL.
I have never used WTL to write a program but if I would, I would use the class described in this article. Microsoft proposes another option for people looking for hyperlink controls. It added such a control to comctrl32.dll version 6 but this version is available only on XP and Microsoft states. An application that has to run on other Windows operating systems cannot rely on the new common control library being present and should not use the features that are available only in ComCtl32.dll, version 6. It might pose a problem if you don't want to restrict your potential user base strictly to this target.
First, the changes needed to a static control to become a hyperlink that Neal addressed are:
- Clicking the text needs to open a browser window to the location specified by the text.
- The cursor needs to change from the standard arrow cursor to a pointing index finger when it moves over the control.
- The text in the control needs to be underlined when the cursor moves over the control.
- A hyperlink control needs to display text in a different color - black just won't do.
The features that I added are:
- A hyperlink control once visited needs to change color.
- The hyperlink control should be accessible with keyboard.
- Install some kind of hooks to allow the programmer to perform some actions when the control has the focus or when the cursor is hovering over the control.
Before describing how the new features have been implemented, let me introduce you to the major architectural change that the code underwent. I placed the code into a class. Here is the class definition:
class CHyperLink
{
public:
CHyperLink(void);
virtual ~CHyperLink(void);
BOOL ConvertStaticToHyperlink(HWND hwndCtl, LPCTSTR strURL);
BOOL ConvertStaticToHyperlink(HWND hwndParent,
UINT uiCtlId, LPCTSTR strURL);
BOOL setURL( LPCTSTR strURL);
LPCTSTR getURL(void) const { return m_strURL; }
protected:
virtual void OnSelect(void) {}
virtual void OnDeselect(void) {}
LPTSTR m_strURL;
private:
static COLORREF g_crLinkColor, g_crVisitedColor;
static HCURSOR g_hLinkCursor;
static HFONT g_UnderlineFont;
static int g_counter;
BOOL m_bOverControl;
BOOL m_bVisited;
HFONT m_StdFont;
WNDPROC m_pfnOrigCtlProc;
void createUnderlineFont(void);
static void createLinkCursor(void);
void createGlobalResources(void)
{
createUnderlineFont();
createLinkCursor();
}
static void destroyGlobalResources(void)
{
g_hLinkCursor = NULL;
DeleteObject(g_UnderlineFont);
g_UnderlineFont = NULL;
}
void Navigate(void);
static void DrawFocusRect(HWND hwnd);
static LRESULT CALLBACK _HyperlinkParentProc(HWND hwnd,
UINT message, WPARAM wParam, LPARAM lParam);
static LRESULT CALLBACK _HyperlinkProc(HWND hwnd,
UINT message, WPARAM wParam, LPARAM lParam);
};
The reasons that motivated this change are:
- Allow the user to derive a new class to customize the control behavior when a hyperlink is selected or deselected.
- Reduce the number of
GetProp()
calls in the window procedures by fetching the pointer on an object containing all the needed variables with one GetProp()
call.
#1 can be achieved by overriding the OnSelect()
and OnDeselect()
functions. This will be demonstrated later when I will be presenting the demo application.
This brought me to introduce another improvement. Have you noticed that some members are static
? This allows multiple hyperlink controls to share the same resources. Shared resources include the hand cursor and the underlined font. This block has been added to the ConvertStaticToHyperlink()
function:
if( g_counter++ == 0 )
{
createGlobalResources();
}
And this code has been added to the WM_DESTROY
message handler in the control window procedure:
if( --CHyperLink::g_counter <= 0 )
{
destroyGlobalResources();
}
To the first ConvertStaticToHyperlink()
call, global resources will be allocated and when the last hyperlink is destroyed, it will destroy the shared resources as well. The advantages to this approach are that it will make memory usage more efficient and the hand cursor will be loaded just once. Here is the new WM_SETCURSOR
code:
case WM_SETCURSOR:
{
SetCursor(CHyperLink::g_hLinkCursor);
return TRUE;
}
Now let's get back to the new features, the simplest one is to change the color of the hyperlink when it is visited. A very simple change to the WM_CTLCOLORSTATIC
handler is needed. It just checks a boolean variable state that is set to true
when the link is visited. Here is the pertinent code:
inline void CHyperLink::Navigate(void)
{
SHELLEXECUTEINFO sei;
::ZeroMemory(&sei,sizeof(SHELLEXECUTEINFO));
sei.cbSize = sizeof( SHELLEXECUTEINFO );
sei.lpVerb = TEXT( "open" );
sei.lpFile = m_strURL;
sei.nShow = SW_SHOWNORMAL;
WINXDISPLAY(ShellExecuteEx(&sei));
m_bVisited = TRUE;
}
case WM_CTLCOLORSTATIC:
{
HDC hdc = (HDC) wParam;
HWND hwndCtl = (HWND) lParam;
CHyperLink *pHyperLink =
(CHyperLink *)GetProp(hwndCtl, PROP_OBJECT_PTR);
if(pHyperLink)
{
LRESULT lr = CallWindowProc(pfnOrigProc,
hwnd, message, wParam, lParam);
if (!pHyperLink->m_bVisited)
{
SetTextColor(hdc, CHyperLink::g_crLinkColor);
}
else
{
SetTextColor(hdc, CHyperLink::g_crVisitedColor);
}
return lr;
}
break;
}
Now to support keyboard, the following messages must be handled:
WM_KEYUP
WM_SETFOCUS
WM_KILLFOCUS
The control will respond to a space key press. WM_SETFOCUS
and WM_KILLFOCUS
draw the focus rectangle. It is drawn against the parent window. The reason for doing so is because first, otherwise, the focus rectangle will be too close to the hyperlink text, and secondly, I played with making the hyperlink controls transparent by returning a hollow brush from the WM_CTLCOLOR_STATIC
handler. When the parent was erasing the control background, it was messing with the focus rectangle. By drawing the focus rectangle against the parent window, it fixes these small problems.
Another point of interest is why choosing WM_KEYUP
and WM_LBUTTONUP
because at first, and in many hyperlink implementations that I have seen, I used WM_LBUTTONDOWN
? Well, the answer is simple. It is to be consistent with how IE hyperlinks and classic Windows controls behave. I am sure most of you have never paid attention, as I did, to this little detail so go try it out in IE, click on a hyperlink and keep the button pressed. The link won't be triggered until you release the mouse button. The same thing is true with dialog push buttons, if you focus on a pushbutton and press on space, the button won't activate any action as long as the space is pressed. Now, during my research to figure out how to support the keyboard, I read the excellent Paul DiLascia article at MSDN. He is using a combination of WM_GETDLGCODE
/WM_CHAR
messages handlers. In WM_GETDLGCODE
, he returns DLGC_WANTCHARS
to signify to the dialog box manager that the control wants to receive WM_CHAR
messages. I don't agree with this approach and here are the reasons:
- Simplicity: one handler (
WM_KEYUP
) versus two (WM_GETDLGCODE
/WM_CHAR
).
- Correctness: you want the hyperlink to be activated when you release the spacebar as other classic controls do and the problem with
WM_CHAR
is that the control will receive multiple messages if the key remains pressed.
- Finally to back up these claims, Petzold uses
WM_KEYUP
in his PW book when he subclasses controls.
Anyway, here is the relevant code:
inline void CHyperLink::DrawFocusRect(HWND hwnd)
{
HWND hwndParent = GetParent(hwnd);
if( hwndParent )
{
RECT rc;
GetWindowRect(hwnd, &rc);
INFLATERECT(&rc,1,1);
::ScreenToClient(hwndParent, (LPPOINT)&rc);
::ScreenToClient(hwndParent, ((LPPOINT)&rc)+1);
HDC dcParent = GetDC(hwndParent);
::DrawFocusRect(dcParent, &rc);
ReleaseDC(hwndParent,dcParent);
}
}
case WM_KEYUP:
{
if( wParam != VK_SPACE )
{
break;
}
}
case WM_LBUTTONUP:
{
pHyperLink->Navigate();
return 0;
}
case WM_SETFOCUS:
case WM_KILLFOCUS:
{
if( message == WM_SETFOCUS )
{
pHyperLink->OnSelect();
}
else
{
pHyperLink->OnDeselect();
}
CHyperLink::DrawFocusRect(hwnd);
return 0;
}
Have you noticed that both Navigate()
and DrawFocusRect()
are inline
functions? Both functions are called from the hyperlink window procedure. They are inline to optimize the window procedure by not calling unnecessary functions from it while keeping its readability to the maximum.
Now, let's attack the bug fix. The control can lose the mouse capture in others ways than calling ReleaseCapture()
. For instance, click on a link and keep the cursor over the link. When the web browser window pops up, it will break the mouse capture. Because the capture is broken, the control finds itself in an inconsistent state. The trick to fix that bug is to not assume that the control will keep the mouse capture until it releases it and to handle the WM_CAPTURECHANGED
message. Here is the code:
case WM_MOUSEMOVE:
{
if ( pHyperLink->m_bOverControl )
{
RECT rect;
GetClientRect(hwnd,&rect);
POINT pt = { LOWORD(lParam), HIWORD(lParam) };
if (!PTINRECT(&rect,pt))
{
ReleaseCapture();
}
}
else
{
pHyperLink->m_bOverControl = TRUE;
SendMessage(hwnd, WM_SETFONT,
(WPARAM)CHyperLink::g_UnderlineFont, FALSE);
InvalidateRect(hwnd, NULL, FALSE);
pHyperLink->OnSelect();
SetCapture(hwnd);
}
return 0;
}
case WM_CAPTURECHANGED:
{
pHyperLink->m_bOverControl = FALSE;
pHyperLink->OnDeselect();
SendMessage(hwnd, WM_SETFONT,
(WPARAM)pHyperLink->m_StdFont, FALSE);
InvalidateRect(hwnd, NULL, FALSE);
return 0;
}
To complete the window procedures topic, there is an important detail that needs to be highlighted. The processed messages are not passed back to the static control procedure because the static control does not need them. It does work fine but be aware that it could cause some problems if the static control is already subclassed. Consider the example where the static control would be already subclassed with the Tooltip control that needs to process mouse messages. In that situation, the Tooltip control would not work as expected. In the demo program section, I will show how you can use the Tooltip control with CHyperLink
.
And finally, to speed the GetProp()
calls, an ATOM is used instead of strings. A simple global object is used to store the Atom to be sure that it will be initialized prior to any use of the CHyperLink
and that it will be present for the whole lifetime of the program. A GUID is appended to a meaningful string to ensure its uniqueness across the system:
class CGlobalAtom
{
public:
CGlobalAtom(void)
{ atom = GlobalAddAtom(TEXT("_Hyperlink_Object_Pointer_")
TEXT("\\{AFEED740-CC6D-47c5-831D-9848FD916EEF}")); }
~CGlobalAtom(void)
{ DeleteAtom(atom); }
ATOM atom;
};
static CGlobalAtom ga;
#define PROP_OBJECT_PTR ((LPCTSTR)(DWORD)ga.atom)
#define PROP_ORIGINAL_PROC ((LPCTSTR)(DWORD)ga.atom)
The demo is just a simple MFC AppWizard generated application where the About dialog class has been changed to demonstrate how to use my CHyperLink
class.
First, in the Dialog editor, add some static controls. Make sure to select the TABSTOP style and to give the control a unique ID. Then I derive a new class from CHyperLink
to override OnSelect()
and OnDeselect()
. I am sending the URL text to the status bar by calling the Frame window SetMessageText()
function.
[Editor comments: Line breaks used to avoid scrolling.]
class CDemoLink : public CHyperLink
{
protected:
virtual void OnSelect(void)
{ ((CFrameWnd *)AfxGetMainWnd())->
SetMessageText(m_strURL); }
virtual void OnDeselect(void)
{ ((CFrameWnd *)AfxGetMainWnd())->
SetMessageText(AFX_IDS_IDLEMESSAGE); }
};
Then add member variables of type CDemoLink
to the dialog class and do the following in the WM_INITDIALOG
handler:
void CAboutDlg::setURL(CHyperLink &ctr, int id)
{
TCHAR buffer[128];
int nLen = ::LoadString(AfxGetResourceHandle(),
id, buffer, 128);
if( !nLen )
{
lstrcpy( buffer, __TEXT(""));
}
ctr.ConvertStaticToHyperlink(GetSafeHwnd(),id,buffer);
}
BOOL CAboutDlg::OnInitDialog()
{
CDialog::OnInitDialog();
setURL(m_DemoLink,IDC_HOMEPAGE);
setURL(m_DemoMail,IDC_EMAIL);
return TRUE;
}
The demo program also demonstrates how to use CHyperLink
with the tooltip control. To do so, I had to derive a new class from MFC CToolTipCtrl
class. I am going to first show you the code using the new derived class and then explain the purpose of the new class:
BOOL CAboutDlgWithToolTipURL::OnInitDialog()
{
CAboutDlg::OnInitDialog();
m_ctlTT.Create(this);
setURL(m_DemoLink,IDC_HOMEPAGE);
setURL(m_DemoMail,IDC_EMAIL);
m_ctlTT.AddWindowTool(GetDlgItem(IDC_HOMEPAGE)->GetSafeHwnd(),
(LPTSTR)m_DemoLink.getURL());
m_ctlTT.AddWindowTool(GetDlgItem(IDC_EMAIL)->GetSafeHwnd(),
(LPTSTR)m_DemoMail.getURL());
return TRUE;
}
Note that m_ctlTT
type is CSubclassToolTipCtrl
and that AddWindowTool()
is a new function. The reason why MFC CToolTipCtrl
was not used directly is that the ToolTip control needs to receive the tool's messages in order to work. The low level ToolTip API provides different ways to achieve this but the MFC class only exposes one way of doing it. It is to relay the tool messages through CToolTipCtrl::RelayEvent()
. It is relatively easy to use but it was inconvenient for CHyperLink
as one of the design goals is to not force users to use MFC. The low level API could have been used with the message TTM_RELAYEVENT
but still it would incur unnecessary overhead to all the hyperlinks not using tooltips. I found the solution in the book Programming Windows with MFC. It uses the ToolTip control feature that allows it to subclass the tools windows.
class CSubclassToolTipCtrl : public CToolTipCtrl
{
public:
CSubclassToolTipCtrl();
public:
BOOL AddWindowTool( HWND hWin, LPTSTR pszText );
public:
virtual ~CSubclassToolTipCtrl();
};
BOOL CSubclassToolTipCtrl::AddWindowTool(
HWND hWin, LPTSTR pszText )
{
TOOLINFO ti;
::ZeroMemory(&ti, sizeof(TOOLINFO));
ti.cbSize = sizeof(TOOLINFO);
ti.uFlags = TTF_IDISHWND | TTF_SUBCLASS;
ti.hwnd = ::GetParent(hWin);
ti.uId = (UINT)hWin;
ti.hinst = AfxGetInstanceHandle();
ti.lpszText = pszText;
return (BOOL)SendMessage(TTM_ADDTOOL,0,(LPARAM)&ti);
}
I added a minor improvement to the solution found in the Programming Windows with MFC book. I initialized the TOOLINFO
structure to zero prior to using it. Initializing structures prior to using them is a good programming practice. If the structure definition is updated with the addition of new fields, without initializing the structure, you would have some fields unitialized with random values and this can lead to nasty hard to find bugs. By resetting the structure, you shield your code against that.
That is it! I hope you enjoyed this article and if you did and found it useful, please take a few seconds to rank it. You can do so right at the bottom of the article.
- Neil Stublen, Transforming Static Text Controls into Active Hyperlinks Using the Win32 API.
- Paul DiLascia, Making Static Links Keyboard-Capable, Launching URLs from Your App, MSDN magazine, March 2005.
- Charles Petzold, Programming Windows, Fifth Edition, Microsoft Press, 1999.
- Jeff Prosise, Programming Windows with MFC, Second Edition, Microsoft Press, 1999.
- George Shepherd, Scot Wingo, MFC Internals, Addison Wesley, 1996.
- 12-03-2005
- Added alternatives that are WTL and comctlr32.dll version 6.
- Added changes to make
CHyperLink
compatible with UNICODE.
- Added an example on how to use
CHyperLink
with ToolTips.
- 11-17-2005: