Home
Fractals
Tutorials
Books
My blog
My LinkedIn Profile

BOOKS i'm reading

Napoleon Hill Keys to Success: The 17 Principles of Personal Achievement, Napoleon Hill, ISBN: 978-0452272811
The 4-Hour Workweek: Escape 9-5, Live Anywhere, and Join the New Rich (Expanded and Updated), Timothy Ferriss, ISBN: 978-0307465351
The Fountainhead, Ayn Rand, ISBN: 0452273331

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

Olivier Langlois, IT superstar coach, Dominate LinkedIn Formula author
by Olivier Langlois

Contents





Introduction

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

C++ hyperlink demo sample program preview

Prior to developing my own hyperlink control C++ implementation for Windows, I have been looking around on the net in search of already written hyperlink controls for Windows in C/C++. There is a lot around but none were good enough for my standards. Some are bloated with features that I do not 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 C++ code based on Neal Stublen's work. In this C++ Windows programming tutorial, 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 control 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 hyperlink 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:
    static COLORREF g_crLinkColor, g_crVisitedColor;// Hyperlink colors
    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 control 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 control 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

    LASTERRORDISPLAYR(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 hyperlink 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 control 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 control 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 control 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 control 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         MAKEINTATOM(ga.atom)
#define PROP_ORIGINAL_PROC      MAKEINTATOM(ga.atom)


Page 1 2

Home :: Fractals :: Tutorials :: Books :: My blog :: My LinkedIn Profile :: Contact