Click here to Skip to main content
15,867,488 members
Articles / Desktop Programming / MFC
Article

Small C++ class to transform any static control into a hyperlink control

Rate me:
Please Sign up or sign in to vote.
4.31/5 (23 votes)
6 Feb 200611 min read 99.7K   1.3K   41   23
This class is small, efficient and is compatible with Win32 API programs and MFC programs as well.

Contents

Introduction

This class is small, efficient and is compatible with Win32 API programs and MFC programs as well.

Image 1

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.

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

  1. It has much more code.
  2. 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.
  3. 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.
  4. 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.

Features

First, the changes needed to 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 with keyboard.
  3. 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 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 );  // 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;
}

Now to support 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, 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:

  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 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.
  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, 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 )
    {
        // 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 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:

/*
 * 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 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();

     // 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 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();
    
    // 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 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
{
// 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 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.

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.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here


Written By
Web Developer
Canada Canada
I'm located in Montreal,Canada and I graduated in electrical engineering at E.T.S.

Highly experienced C++ developer. I have been working 3 years at Nortel Networks on their Next-gen OC-768 products firmware. I then worked for the FAA 3 years on their next-gen Oceanic Air Traffic Control system. Followed by a short period in the video game industry at Quazal, the online multiplayer middleware provider and now I am in the Internet audio/video streaming business at StreamTheWorld.

To contact me, visit my website

Comments and Discussions

 
GeneralThe web browser window in opening more than once Pin
sosan48121-Jan-09 17:27
sosan48121-Jan-09 17:27 
GeneralGreat work! Pin
Drew_Benton15-May-08 8:46
Drew_Benton15-May-08 8:46 
Generaldatagridview is not going to null Pin
sharmaasheesh22-Feb-08 1:04
sharmaasheesh22-Feb-08 1:04 
GeneralRe: datagridview is not going to null Pin
sharmaasheesh22-Feb-08 1:05
sharmaasheesh22-Feb-08 1:05 
QuestionConflict with WM_CTLCOLORSTATIC Pin
siavan22-Feb-08 0:29
siavan22-Feb-08 0:29 
GeneralRe: Conflict with WM_CTLCOLORSTATIC Pin
sharmaasheesh22-Feb-08 0:59
sharmaasheesh22-Feb-08 0:59 
GeneralRe: Conflict with WM_CTLCOLORSTATIC Pin
siavan22-Feb-08 8:37
siavan22-Feb-08 8:37 
GeneralWTL::CHyperLink is of the same behaviour... Pin
siavan26-Feb-08 1:58
siavan26-Feb-08 1:58 
GeneralExample web URL blocked by my company Pin
yafan18-Jan-06 5:11
yafan18-Jan-06 5:11 
GeneralAlso, your code is excellent. Pin
yafan18-Jan-06 5:27
yafan18-Jan-06 5:27 
GeneralRe: Example web URL blocked by my company Pin
lano110618-Jan-06 6:14
lano110618-Jan-06 6:14 
Generalwhy... Pin
toxcct16-Jan-06 1:01
toxcct16-Jan-06 1:01 
...not having made the CHyperLink class inherit directly from CStatic ?



TOXCCT >>> GEII power
[toxcct][VisualCalc 2.20][VisualCalc 3.0]
GeneralRe: why... Pin
lano110616-Jan-06 10:36
lano110616-Jan-06 10:36 
GeneralRe: why... Pin
peterchen7-Nov-06 5:15
peterchen7-Nov-06 5:15 
Questionhow would you...? Pin
drewcanitz19-Dec-05 13:11
drewcanitz19-Dec-05 13:11 
AnswerRe: how would you...? Pin
lano110619-Dec-05 13:51
lano110619-Dec-05 13:51 
Generalcomctl32.dll version 6 Pin
lano110618-Nov-05 6:35
lano110618-Nov-05 6:35 
GeneralWTL::CHyperLink Pin
WTL Student17-Nov-05 13:52
WTL Student17-Nov-05 13:52 
GeneralRe: WTL::CHyperLink Pin
Dandy Cheung17-Nov-05 22:06
Dandy Cheung17-Nov-05 22:06 
GeneralRe: WTL::CHyperLink Pin
lano110618-Nov-05 6:16
lano110618-Nov-05 6:16 
GeneralRe: WTL::CHyperLink Pin
Todd Smith18-Nov-05 12:27
Todd Smith18-Nov-05 12:27 
GeneralRe: WTL::CHyperLink Pin
lano110618-Nov-05 13:27
lano110618-Nov-05 13:27 
GeneralOne more WTL implementation whcih allows mixing text and hyperlinks Pin
ryltsov20-Dec-06 4:56
ryltsov20-Dec-06 4:56 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.