Small C++ Class to Transform Any Static Control into a Hyperlink Control

Introduction

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 pre-written hyperlink controls. There are 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 wanted 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; it 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.

Microsoft Options

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, because a 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:

  1. It has much more code.
  2. It uses a WTL message dispatching scheme that might be faster and slimmer than the MFC one but it is not as efficient as a pure Windows procedure that this class is using.
  3. It has tooltips, but I didn't see how you could customize them to send the URL text in the status bar as easily as it is with my class.
  4. You must have WTL in your project to use their hyperlink class. Mine works 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.

Features

First, the changes needed for a static control to become a hyperlink that Neal addressed are:

  1. Clicking the text needs to open a browser window to the location specified by the text.
  2. The cursor needs to change from the standard arrow cursor to a pointing index finger when it moves over the control.
  3. The text in the control needs to be underlined when the cursor moves over the control.
  4. A hyperlink control needs to display text in a different color—black just won't do.

The features that I added are:

  1. A hyperlink control once visited needs to change color.
  2. The hyperlink control should be accessible from the keyboard.
  3. It should 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:
   /*
    * Override if you want to perform some  action when
    * the link has the focus or when the cursor is over
    * the link such as displaying the URL somewhere.
    */
   virtual void OnSelect(void)   {}
   virtual void OnDeselect(void) {}

   LPTSTR   m_strURL;      // hyperlink URL

private:
   // Hyperlink colors
   static COLORREF g_crLinkColor, g_crVisitedColor;
   static HCURSOR  g_hLinkCursor;   // Cursor for hyperlink
   static HFONT    g_UnderlineFont; // Font for underline display
   static int      g_counter;       // Global resources user
                                    // counter
   BOOL     m_bOverControl;         // cursor over control?
   BOOL     m_bVisited;             // Has it been visited?
   HFONT    m_StdFont;              // Standard font
   WNDPROC  m_pfnOrigCtlProc;

   void createUnderlineFont(void);
   static void createLinkCursor(void);
   void createGlobalResources(void)
   {
      createUnderlineFont();
      createLinkCursor();
   }
   static void destroyGlobalResources(void)
   {
      /*
       * No need to call DestroyCursor() for cursors
       * acquired through LoadCursor().
       */
      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:

  1. Allow the user to derive a new class to customize the control behavior when a hyperlink is selected or deselected.
  2. 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 present 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 me 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 );  // Set Size
   sei.lpVerb = TEXT( "open" );              // Set Verb
   sei.lpFile = m_strURL;                    // Set Target
                                             // To Open
   sei.nShow = SW_SHOWNORMAL;                // Show Normal

   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)
      {
         // This is the most common case for static
         // branch prediction optimization
         SetTextColor(hdc, CHyperLink::g_crLinkColor);
      }
      else
      {
         SetTextColor(hdc, CHyperLink::g_crVisitedColor);
      }
      return lr;
   }
   break;
}

To support the keyboard, the following messages must be handled:

  1. WM_KEYUP
  2. WM_SETFOCUS
  3. 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. 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.

Small C++ Class to Transform Any Static Control into a Hyperlink Control

Another point of interest is why I chose WM_KEYUP and WM_LBUTTONUP when at first, and in many hyperlink implementations that I have seen, I used WM_LBUTTONDOWN? 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 the space bar, 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 uses 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:

  1. Simplicity: One handler (WM_KEYUP) versus two (WM_GETDLGCODE/WM_CHAR).
  2. Correctness: You want the hyperlink to be activated when you release the space bar as other classic controls do and the problem with WM_CHAR is that the control will receive multiple messages if the key remains pressed.
  3. 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 )
   {
      // calculate where to draw focus rectangle, in screen
      // coords
      RECT rc;
      GetWindowRect(hwnd, &rc);

      INFLATERECT(&rc,1,1); // add one pixel all around
                            // convert to parent
                            // window client coords
      ::ScreenToClient(hwndParent, (LPPOINT)&rc);
      ::ScreenToClient(hwndParent, ((LPPOINT)&rc)+1);
      HDC dcParent = GetDC(hwndParent);  // parent window's DC
      ::DrawFocusRect(dcParent, &rc);    // draw it!
      ReleaseDC(hwndParent,dcParent);
   }
}

case WM_KEYUP:
{
   if( wParam != VK_SPACE )
   {
      break;
   }
}

// Fall through
case WM_LBUTTONUP:
{
   pHyperLink->Navigate();
   return 0;
}
case WM_SETFOCUS:    // Fall through
case WM_KILLFOCUS:
{
   if( message == WM_SETFOCUS )
   {
      pHyperLink->OnSelect();
   }
   else    // WM_KILLFOCUS
   {
      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 to 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 )
   {
      // This is the most common case for static branch
      // prediction optimization
      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 subclassed already 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:

/*
 * typedefs
 */
class CGlobalAtom
{
public:
   CGlobalAtom(void)
   { atom = GlobalAddAtom(TEXT("_Hyperlink_Object_Pointer_")
            TEXT("\\{AFEED740-CC6D-47c5-831D-9848FD916EEF}")); }
   ~CGlobalAtom(void)
   { DeleteAtom(atom); }

   ATOM atom;
};

/*
 * Local variables
 */
static CGlobalAtom ga;

#define PROP_OBJECT_PTR       ((LPCTSTR)(DWORD)ga.atom)
#define PROP_ORIGINAL_PROC    ((LPCTSTR)(DWORD)ga.atom)

The Demo Program

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, derive a new class from CHyperLink to override OnSelect() and OnDeselect(). I send 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();

   // TODO: Add extra initialization here
   setURL(m_DemoLink,IDC_HOMEPAGE);
   setURL(m_DemoMail,IDC_EMAIL);

   return TRUE;  // return TRUE unless you set the focus
                 // to a control
                 // EXCEPTION: OCX Property Pages should
                 // return FALSE
}

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 will first show you the code using the new derived class and then explain the purpose of the new class:

BOOL CAboutDlgWithToolTipURL::OnInitDialog()
{
   CAboutDlg::OnInitDialog();
   
   // TODO: Add extra initialization here
   m_ctlTT.Create(this);
   setURL(m_DemoLink,IDC_HOMEPAGE);
   setURL(m_DemoMail,IDC_EMAIL);

   /*
    * It is OK to add a Window tool to the tool tip
    * control with the CHyperLink dynamically allocated
    * URL string because the windows are destroyed with
    * WM_DESTROY before the CHyperLink destructor where
    * the URL string is freed.
    */
   m_ctlTT.AddWindowTool(GetDlgItem(IDC_HOMEPAGE)
      ->GetSafeHwnd(),
      (LPTSTR)m_DemoLink.getURL());
   m_ctlTT.AddWindowTool(GetDlgItem(IDC_EMAIL)->GetSafeHwnd(),
      (LPTSTR)m_DemoMail.getURL());

   return TRUE;  // return TRUE unless you set the focus
                 // to a control
                 // EXCEPTION: OCX Property Pages should
                 // return FALSE
}

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 because one of the design goals is to not force users to use MFC. The low-level API could have been used with the TTM_RELAYEVENT message 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
{
// Construction
public:
   CSubclassToolTipCtrl();

// Operations
public:
/*********************************************************
 *
 * Name      : AddWindowTool
 *
 * Purpose   : Add a window tool by using the Tooltip 
 *             subclass feature
 *
 * Parameters:
 *     hWin    (HWND)    Tool window
 *     pszText (LPCTSTR) Tip text (can also be 
 *                       a string resource ID).
 *
 * Return value : Returns TRUE if successful, 
 *                or FALSE otherwise.
 *
 *********************************************************/
    BOOL AddWindowTool( HWND hWin, LPTSTR pszText );

// Implementation
public:
   virtual ~CSubclassToolTipCtrl();
};

/*
 * Function CSubclassToolTipCtrl::AddWindowTool
 */
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.

Conclusion

That is it! I hope you enjoyed this article. If you did and found it useful, please take a few seconds to rank it. You can do so at the bottom of the article.

Bibliography

Revision History

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

  • Original article.


About the Author

Olivier Langlois

I have been in the telecomm industry doing, among other things, embedded protocol stacks. I also worked for the FAA to develop the next generation oceanic ATM system. I was (surprise, surprise) in charge ATM protocols. I got tired of working with old technologies aka UNIX, so I am now working at Quazal, best known for offering the best products and services for Multiplayer Game Development. You can contact me through my website

Downloads

Comments

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

Leave a Comment
  • Your email address will not be published. All fields are required.

Top White Papers and Webcasts

  • Java developers know that testing code changes can be a huge pain, and waiting for an application to redeploy after a code fix can take an eternity. Wouldn't it be great if you could see your code changes immediately, fine-tune, debug, explore and deploy code without waiting for ages? In this white paper, find out how that's possible with a Java plugin that drastically changes the way you develop, test and run Java applications. Discover the advantages of this plugin, and the changes you can expect to see …

  • Live Event Date: September 10, 2014 @ 11:00 a.m. ET / 8:00 a.m. PT Modern mobile applications connect systems-of-engagement (mobile apps) with systems-of-record (traditional IT) to deliver new and innovative business value. But the lifecycle for development of mobile apps is also new and different. Emerging trends in mobile development call for faster delivery of incremental features, coupled with feedback from the users of the app "in the wild". This loop of continuous delivery and continuous feedback is …

Most Popular Programming Stories

More for Developers

Latest Developer Headlines

RSS Feeds