(c) 2002 Lucian Wischik. This code is free, and anyone can do with it whatever they like (except sell it or claim ownership).
This code is concerned with making a hyperlinked control in a dialog, in a plain C++ Windows API application, without having to use a framework class like MFC/ATL and without having any extra DLLs or OCXs. It offers a single simple routine, SetDlgItemUrl(...), which makes a static text control turn blue and underlined and have a hand-shaped cursor, and when the user clicks on it then the URL will be opened.
The example code consists of a single function, written in C++. I use it as a unit (seturl.cpp) added to my own project. This I wrote it under Visual Studio .NET, but it should work on most compilers without too much fuss.
Suppose you have created a dialog, and it has a static text control in it with a particular ID. Then just add this code to your dialog proc:
// We'll prototype the functio here -- there's only a single function,
// so it's hardly worth creating a header file for it.
void SetDlgItemUrl(HWND hdlg,int id,const char *url);
BOOL CALLBACK MyDlgProc(HWND hdlg,UINT msg,WPARAM wParam,LPARAM lParam)
{ switch (msg)
{ case WM_INITDIALOG:
{ SetDlgItemUrl(hdlg,IDC_URL,"http://www.wischik.com/lu/programmer/");
return TRUE;
}
case WM_COMMAND:
{ int id=LOWORD(wParam);
if (id==IDOK || id==IDCANCEL) {EndDialog(hdlg,id); return TRUE;}
} break;
}
return FALSE;
}
#define STRICT
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <shellapi.h>
// --------------------------------------------------------------------------------
// SetDlgItemUrl(hwnd,IDC_MYSTATIC,"http://www.wischik.com/lu");
// This routine turns a dialog's static text control into an underlined hyperlink.
// You can call it in your WM_INITDIALOG, or anywhere.
// It will also set the text of the control... if you want to change the text
// back, you can just call SetDlgItemText() afterwards.
// --------------------------------------------------------------------------------
void SetDlgItemUrl(HWND hdlg,int id,const char *url);
// Implementation notes:
// We have to subclass both the static control (to set its cursor, to respond to click)
// and the dialog procedure (to set the font of the static control). Here are the two
// subclasses:
LRESULT CALLBACK UrlCtlProc(HWND hwnd,UINT msg,WPARAM wParam,LPARAM lParam);
LRESULT CALLBACK UrlDlgProc(HWND hwnd,UINT msg,WPARAM wParam,LPARAM lParam);
// When the user calls SetDlgItemUrl, then the static-control-subclass is added
// if it wasn't already there, and the dialog-subclass is added if it wasn't
// already there. Both subclasses are removed in response to their respective
// WM_DESTROY messages. Also, each subclass stores a property in its window,
// which is a HGLOBAL handle to a GlobalAlloc'd structure:
typedef struct {char *url; WNDPROC oldproc; HFONT hf; HBRUSH hb;} TUrlData;
// I'm a miser and only defined a single structure, which is used by both
// the control-subclass and the dialog-subclass. Both of them use 'oldproc' of course.
// The control-subclass only uses 'url' (in response to WM_LBUTTONDOWN),
// and the dialog-subclass only uses 'hf' and 'hb' (in response to WM_CTLCOLORSTATIC)
// There is one sneaky thing to note. We create our underlined font *lazily*.
// Initially, hf is just NULL. But the first time the subclassed dialog received
// WM_CTLCOLORSTATIC, we sneak a peak at the font that was going to be used for
// the control, and we create our own copy of it but including the underline style.
// This way our code works fine on dialogs of any font.
// SetDlgItemUrl: this is the routine that sets up the subclassing.
void SetDlgItemUrl(HWND hdlg,int id,const char *url)
{ // nb. vc7 has crummy warnings about 32/64bit. My code's perfect! That's why I hide the warnings.
#pragma warning( push )
#pragma warning( disable: 4312 4244 )
// First we'll subclass the edit control
HWND hctl = GetDlgItem(hdlg,id);
SetWindowText(hctl,url);
HGLOBAL hold = (HGLOBAL)GetProp(hctl,"href_dat");
if (hold!=NULL) // if it had been subclassed before, we merely need to tell it the new url
{ TUrlData *ud = (TUrlData*)GlobalLock(hold);
delete[] ud->url;
ud->url=new char[strlen(url)+1]; strcpy(ud->url,url);
}
else
{ HGLOBAL hglob = GlobalAlloc(GMEM_MOVEABLE,sizeof(TUrlData));
TUrlData *ud = (TUrlData*)GlobalLock(hglob);
ud->oldproc = (WNDPROC)GetWindowLongPtr(hctl,GWLP_WNDPROC);
ud->url=new char[strlen(url)+1]; strcpy(ud->url,url);
ud->hf=0; ud->hb=0;
GlobalUnlock(hglob);
SetProp(hctl,"href_dat",hglob);
SetWindowLongPtr(hctl,GWLP_WNDPROC,(LONG_PTR)UrlCtlProc);
}
//
// Second we subclass the dialog
hold = (HGLOBAL)GetProp(hdlg,"href_dlg");
if (hold==NULL)
{ HGLOBAL hglob = GlobalAlloc(GMEM_MOVEABLE,sizeof(TUrlData));
TUrlData *ud = (TUrlData*)GlobalLock(hglob);
ud->url=0;
ud->oldproc = (WNDPROC)GetWindowLongPtr(hdlg,GWLP_WNDPROC);
ud->hb=CreateSolidBrush(GetSysColor(COLOR_BTNFACE));
ud->hf=0; // the font will be created lazilly, the first time WM_CTLCOLORSTATIC gets called
GlobalUnlock(hglob);
SetProp(hdlg,"href_dlg",hglob);
SetWindowLongPtr(hdlg,GWLP_WNDPROC,(LONG_PTR)UrlDlgProc);
}
#pragma warning( pop )
}
// UrlCtlProc: this is the subclass procedure for the static control
LRESULT CALLBACK UrlCtlProc(HWND hwnd,UINT msg,WPARAM wParam,LPARAM lParam)
{ HGLOBAL hglob = (HGLOBAL)GetProp(hwnd,"href_dat");
if (hglob==NULL) return DefWindowProc(hwnd,msg,wParam,lParam);
TUrlData *oud=(TUrlData*)GlobalLock(hglob); TUrlData ud=*oud;
GlobalUnlock(hglob); // I made a copy of the structure just so I could GlobalUnlock it now, to be more local in my code
switch (msg)
{ case WM_DESTROY:
{ RemoveProp(hwnd,"href_dat"); GlobalFree(hglob);
if (ud.url!=0) delete[] ud.url;
// nb. remember that ud.url is just a pointer to a memory block. It might look weird
// for us to delete ud.url instead of oud->url, but they're both equivalent.
} break;
case WM_LBUTTONDOWN:
{ HWND hdlg=GetParent(hwnd); if (hdlg==0) hdlg=hwnd;
ShellExecute(hdlg,"open",ud.url,NULL,NULL,SW_SHOWNORMAL);
} break;
case WM_SETCURSOR:
{ SetCursor(LoadCursor(NULL,MAKEINTRESOURCE(IDC_HAND)));
return TRUE;
}
case WM_NCHITTEST:
{ return HTCLIENT; // because normally a static returns HTTRANSPARENT, so disabling WM_SETCURSOR
}
}
return CallWindowProc(ud.oldproc,hwnd,msg,wParam,lParam);
}
// UrlDlgProc: this is the subclass procedure for the dialog
LRESULT CALLBACK UrlDlgProc(HWND hwnd,UINT msg,WPARAM wParam,LPARAM lParam)
{ HGLOBAL hglob = (HGLOBAL)GetProp(hwnd,"href_dlg");
if (hglob==NULL) return DefWindowProc(hwnd,msg,wParam,lParam);
TUrlData *oud=(TUrlData*)GlobalLock(hglob); TUrlData ud=*oud;
GlobalUnlock(hglob);
switch (msg)
{ case WM_DESTROY:
{ RemoveProp(hwnd,"href_dlg"); GlobalFree(hglob);
if (ud.hb!=0) DeleteObject(ud.hb);
if (ud.hf!=0) DeleteObject(ud.hf);
} break;
case WM_CTLCOLORSTATIC:
{ HDC hdc=(HDC)wParam; HWND hctl=(HWND)lParam;
// To check whether to handle this control, we look for its subclassed property!
HANDLE hprop=GetProp(hctl,"href_dat");
if (hprop==NULL) return CallWindowProc(ud.oldproc,hwnd,msg,wParam,lParam);
// There has been a lot of faulty discussion in the newsgroups about how to change
// the text colour of a static control. Lots of people mess around with the
// TRANSPARENT text mode. That is incorrect. The correct solution is here:
// (1) Leave the text opaque. This will allow us to re-SetDlgItemText without it looking wrong.
// (2) SetBkColor. This background colour will be used underneath each character cell.
// (3) return HBRUSH. This background colour will be used where there's no text.
SetTextColor(hdc,RGB(0,0,255));
SetBkColor(hdc,GetSysColor(COLOR_BTNFACE));
if (ud.hf==0)
{ // we use lazy creation of the font. That's so we can see font was currently being used.
TEXTMETRIC tm; GetTextMetrics(hdc,&tm);
LOGFONT lf;
lf.lfHeight=tm.tmHeight;
lf.lfWidth=0;
lf.lfEscapement=0;
lf.lfOrientation=0;
lf.lfWeight=tm.tmWeight;
lf.lfItalic=tm.tmItalic;
lf.lfUnderline=TRUE;
lf.lfStrikeOut=tm.tmStruckOut;
lf.lfCharSet=tm.tmCharSet;
lf.lfOutPrecision=OUT_DEFAULT_PRECIS;
lf.lfClipPrecision=CLIP_DEFAULT_PRECIS;
lf.lfQuality=DEFAULT_QUALITY;
lf.lfPitchAndFamily=tm.tmPitchAndFamily;
GetTextFace(hdc,LF_FACESIZE,lf.lfFaceName);
ud.hf=CreateFontIndirect(&lf);
TUrlData *oud = (TUrlData*)GlobalLock(hglob); oud->hf=ud.hf; GlobalUnlock(hglob);
}
SelectObject(hdc,ud.hf);
// Note: the win32 docs say to return an HBRUSH, typecast as a BOOL. But they
// fail to explain how this will work in 64bit windows where an HBRUSH is 64bit.
// I have supressed the warnings for now, because I hate them...
#pragma warning( push )
#pragma warning( disable: 4311 )
return (BOOL)ud.hb;
#pragma warning( pop )
}
}
return CallWindowProc(ud.oldproc,hwnd,msg,wParam,lParam);
}
The code on this page demonstrates the following:
(functions) SetDlgItemUrl, SetProp, GetProp, RemoveProp, GlobalAlloc, GlobalFree, GlobalLock, GlobalUnlock, GetWindowLongPtr, SetWindowLongPtr, ShellExecute, SetTextColor, SetBkColor, GetSysColor
(events) WM_DESTROY, WM_SETCURSOR, WM_NCHITTEST, HTCLIENT, WM_CTLCOLORSTATIC
(concepts) subclassing, property, properties, handle, hyperlinks, child controls, 64bit, pointer, warning, hyper-link, URL, shortcut