URL Control

A simple HTTP URL control

Updated 6th Jan 2005

Ralph Bittmann has kindly sent an update of the URL Control. It has some nice extra features:

  • Keyboard support for tabbing, focus and space key
  • Window resizing to fit the displayed text
  • Better font support
  • Better cursor support

Introduction

This tutorial will teach you how to implement a "URL" control, which pops up a browser when you click on the hyperlink. The control will be based around a simple static text label, and we're going to customize it so that it looks and feels like a hyperlink that you would find in a web-page.

Sample screen-shot

Our aim is to implement a single function, which takes a window handle and a URL, and converts the specified static control into a URL control. This is what the function prototype will look like:

BOOL StaticToURLControl(HWND hDlg, UINT uStaticId, TCHAR *szURL, COLORREF crLink);

The function above will actually take a dialog's window handle and the control ID of a static label. Because I envisage this URL control will be most commonly found in dialog boxes this method provides the simplest interface.

Subclassing again

The easiest way to create this URL control is to subclass a static control, and add on the functionality we require. The subclass procedure will need to perform these tasks:

  • Change the mouse cursor to a "hand" when the mouse moves over the control.
  • Change the colour of the label (Blue for normal, Purple for a visited link).
  • Display the static label with an underlined font.
  • Display a browser when the user clicks on the control.

Before we start to implement these features, we need to actually perform the subclass and initialize the static control correctly. The structure below will hold our URL control state.

typedef struct
{
    TCHAR  szURL[_MAX_PATH];    // desination URL
    WNDPROC  oldproc;           // old window procedure
    COLORREF crLink;            // current link colour
    COLORREF crVisited;         // link colour if visited} URLCtrl;

A hyperlink in a web page has several properties which we need to emulate. In addition to the visible display text, a hyperlink also has a destiniation URL which is visited when the link is clicked on. Now, a standard static control already has a text label which you set with SetWindowText or WM_SETTEXT. However, we need to somehow specify what the destination URL is, so the structure above includes this information.

The colour of a hyperlink is also important. A hyperlink has two colours - blue when unvisited, and purple when the link has been visited. The two COLORREF structure members will represent these properties.

The function below shows how to convert a static control into a URL control. It performs a number of steps, described below.

  1. Allocate memory for the URL state structure.
  2. Set the URL destination text.
  3. Set the hyperlink colours.
  4. Replace the static control's window procedure with our custom one.
  5. Associate the URL state with the new URL control.
BOOL StaticToURLControl(HWND hDlg, UINT staticid, TCHAR *szURL, COLORREF crLink)
{
    HWND hwndCtrl = GetDlgItem(hDlg, staticid);

    // Allocate memory for the URL structure
    URLCtrl *url = (URLCtrl *)HeapAlloc(GetProcessHeap(), 0, sizeof(URLCtrl));

    // set the URL text (not the display text)
    if(szURL) lstrcpy(url->szURL, szURL);
    else      url->szURL[0] = _T('\0');

    // set the hyperlink colour
    if(crLink != -1) url->crLink = crLink;
    else             url->crLink = RGB(0,0,255);

    // set the visited colour
    url->crVisited = RGB(128,0,128);

    // subclass the static control
    url->oldproc = (WNDPROC)SetWindowLong(hwndCtrl, GWL_WNDPROC, (LONG)URLCtrlProc);

    // associate the URL structure with the static control
    SetWindowLong(hwndCtrl, GWL_USERDATA, (LONG)url);
    return TRUE;
}

The actual subclass procedure

A subclass procedure is just a window procedure. The only difference is, instead of calling DefWindowProc you must use CallWindowProc. The skeleton function below performs no customization. All it does at present is to free the URL structure when the window is destroyed.

// Subclass window procedure for a static control
LRESULT CALLBACK URLCtrlProc(HWND hwnd, UINT iMsg, WPARAM wParam, LPARAM lParam)
{

    // get the URL state structure for this window
    URLCtrl  *url     = (URLCtrl *)GetWindowLong(hwnd, GWL_USERDATA);
    WNDPROC   oldproc = url->oldproc;

    switch(iMsg)
    {
    case WM_NCDESTROY:
        HeapFree(GetProcessHeap(0), url);
        break;

        // handle other messages here.
    }

    // perform default processing for all other messages
    return CallWindowProc(oldproc, hwnd, iMsg, wParam, lParam);
}

The rest of this tutorial will show you what is necessary to fill in the rest of the subclass procedure.

Changing the mouse cursor

This is the easiest task to perform, so I'll do this first to get started. All we need to do is handle the WM_SETCURSOR message in our subclass procedure. We use the SetCursor API call to actually change the mouse cursor, and then return TRUE to tell Windows to stop any further processing.

case WM_SETCURSOR:
    SetCursor(hCurs);
    return TRUE;

It's that simple. The only other thing to do is to create an appropriate cursor. The simplest way to do this is to use the LoadCursor API call, and include a cursor as a resource in your main executable. This is slightly disadvantagous because you have to remember to include this cursor every time you want a URL control.

Our solution will be to create the cursor manually, with the CreateCursor API call. Using this method, our URL source file will be completely standalone, with no other dependencies. This is what the function looks like (from MSDN):

HCURSOR CreateCursor(
  HINSTANCE hInst,         // handle to application instance
  int xHotSpot,            // x coordinate of hot spot
  int yHotSpot,            // y coordinate of hot spot
  int nWidth,              // cursor width
  int nHeight,             // cursor height
  CONST VOID *pvANDPlane,  // AND mask array
  CONST VOID *pvXORPlane   // XOR mask array
);

We have to build those mask arrays somehow. For a standard 32x32 monochrome cursor, we require two 128 byte arrays, which represent the bit-masks used to create the cursor image. The download includes these two arrays, but it's worth mentioning how you can create your own by extracting the bit masks from a cursor resource.

  1. Create a cursor using the Visual C++ resource editor.
  2. Locate the *.CUR file in your project directory.
  3. Open the cursor file in your favourite Hex Editor. I recommend my very own HexEdit :-)
  4. Select the last 128 bytes in the .CUR file. This is the XOR mask.
  5. The mask is stored upside down for some reason, so you need to reverse each line in the bitmap. We can calculate that 1 line in the cursor requires 4 bytes in the mask, because 128 bytes divided by 32 lines equals 4.
  6. Reverse the selection using the Edit->Reverse menu command ->
  7. Enter "4" in the Item Width edit field. Click on OK.
  8. Export the raw bytes as a C-style array of BYTES.
  9. Select the File->Export menu command.
  10. Select "C Source" and "Byte" export types.
  11. Select a file name and hit Export.
  12. Repeat from step 4, but this time select the second-last 128 byte block before the current selection. This is the AND mask.

You should now have two *.C files saved somewhere. They will look like this:

/* Generated by HexEdit */
/* C:\MSVC\My Projects\URLCtrl\cur00001.cur */
BYTE XORMask[128] =
{
  0xff, 0xff, 0xff, 0xff,
  0xf9, 0xff, 0xff, 0xff, 
  ..., ...
};

Once you've got your two arrays included in your source, you can create a cursor like this:

hCurs = CreateCursor(GetModuleHandle(0), 5, 2, 32, 32, XORMask, ANDMask);

Just store this cursor handle somewhere so that the WM_SETCURSOR handler has access to it. Although it may seem as if I've side-tracked a little from the original task, I think this technique is really useful so I included it anyway!

Painting the URL control

Now for the artistic part. We need to customize both the font and the colour of the static control if we are to create a proper hyperlink. There are two ways to do this. The first is to handle the WM_CTLCOLORSTATIC message in the static control's parent window (the dialog box). However, this means that every time we want to use a URL control we have to manually handle this message. A better solution is to completely take over drawing in the static control's WM_PAINT handler.

Find the text alignment.

A standard static control can have its text aligned to the left, right or center. In addition, text can also be centered vertically. Utimately we will be using DrawText to draw the text, so we need to map the static control text-alignment styles to those that can be specified with DrawText. The code below shows you how.

DWORD dwStyle   = GetWindowLong(hwnd, GWL_STYLE);
DWORD dwDTStyle = DT_SINGLELINE;

// Test if centered horizontally or vertically
if(dwStyle & SS_CENTER)      dwDTstyle |= DT_CENTER;
if(dwStyle & SS_RIGHT)       dwDTstyle |= DT_RIGHT;
if(dwStyle & SS_CENTERIMAGE) dwDTstyle |= DT_VCENTER;

Set the text colours before drawing.

SetTextColor(hdc, url->crLink);
SetBkColor  (hdc, GetSysColor(COLOR_3DFACE));

Create an underlined font.

This one is easy. We first of all find the default GUI font that we would be drawing with normally. Then, we create a new font based on that one, but with the underline style set, like this:

// Get the default GUI font
LOGFONT lf;
HFONT hf = (HFONT)GetStockObject(DEFAULT_GUI_FONT);

// Add UNDERLINE attribute
GetObject(hf, sizeof lf, &lf);
lf.lfUnderline = TRUE;
            
// Create a new font
hfUnderlined = CreateFontIndirect(&lf);

// Use the font
SelectObject(hdc, hfUnderlined);

Draw the text.

The static control already has its window text set, so we'll just use that instead of having to store our own copy. Note the dwDTStyle flag for DrawText, which we worked out earlier.

TCHAR szWinText[200];

GetWindowText(hwnd, szWinText, sizeof szWinText);
DrawText(hdc, szWinText, -1, &rect, dwDTStyle);

Launch the Default Browser

At this point we have a static control that looks like a hyperlink, but it doesn't do anything yet. We only need to handle three mouse messages to launch a browser when the user clicks on the static control.

Enable mouse messages (WM_NCHITTEST)

This is the most important message. A standard static control will return HTTRANSPARENT for this message, which means that all mouse messages get passed to the parent window instead of the static control. We have to override this behaviour so that our subclass procedure receives the mouse messages instead.

case WM_NCHITTEST:
    return HTCLIENT; 

Mouse-down (WM_LBUTTONDOWN)

All we need to do for this message is to set a flag saying "The user has clicked the mouse down in our window". This is so that when the mouse is released in the same window we can interpret this as a mouse click. We need the flag, because it is quite possible for a window to receive a button-up message even if the mouse button wasn't pressed down in that window.

case WM_LBUTTONDOWN:
    fClicking = TRUE;
    break;

Mouse-up (WM_LBUTTONUP)

This is when we launch the browser.

case WM_LBUTTONUP:
    if(fClicking)
    {
        fClicking = FALSE;

        // Change the link colour to purple
        url->crLink = url->crVisited;

        InvalidateRect(hwnd, 0, 0);
        UpdateWindow(hwnd);

        // Open a browser
        ShellExecute(NULL, _T("open"), url->szURL, NULL, NULL, SW_SHOWNORMAL);
    }

The default browser is launched by a call to ShellExecute. To get this function to display a browser, all we do is specify "open" as the second parameter, and pass a pointer to a string containing a fully qualified URL as the third parameter. ShellExecute does the rest for us, by displaying a browser window for the specified URL.

Conclusion

That, in a nutshell, is it. The source-code download includes the complete URL control source, and a mini test application to demonstrate it. If you want to use a URL control in one of your applications, just include "URLCtrl.h" in your source, and call the following function:

StaticToURLControl(hDlg, IDC_HYPER1, "http://www.mysite.com", -1);

Just a quick mention on how to use the function above. In the resource editor for your dialog box, you need to give your static control a unique ID (instead of ID_STATIC), because by default all static controls have an ID of -1, which means that you can't locate a standard static control using GetDlgItem.

The second issue is the URL text. You need to specify a complete URL, including the http:// prefix, in order for ShellExecute to interpret the string as a URL and launch the browser accordingly.

The last issue is specifying the colour for an unvisited hyperlink. You can specify either a RGB(r,g,b) value, or just pass -1, which gives the control the default blue colour.

Have fun!

James.