|
|||||||||||||||||||||
|
|||||||||||||||||||||
|
Announcements
Chapters
Services
Feature Zones
|
ContentsIntroductionThis 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 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 optionsAfter 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:
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. FeaturesFirst, the changes needed to a static control to become a hyperlink that Neal addressed are:
The features that I added are:
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 can be achieved by overriding the This brought me to introduce another improvement. Have you noticed that some members are if( g_counter++ == 0 ) { createGlobalResources(); } And this code has been added to the if( --CHyperLink::g_counter <= 0 ) { destroyGlobalResources(); } To the first 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 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:
The control will respond to a space key press. Another point of interest is why choosing
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 Now, let's attack the bug fix. The control can lose the mouse capture in others ways than calling 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 And finally, to speed the /* * 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 programThe demo is just a simple MFC AppWizard generated application where the About dialog class has been changed to demonstrate how to use my 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 [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 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 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 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 ConclusionThat 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
| ||||||||||||||||||||