C++ At Work

Making Static Links Keyboard-Capable, Launching URLs from Your App

Paul DiLascia

Code download available at:CATWork0503.exe(184 KB)

Q I've been using your CStaticLink class for a couple of years now in my company's commercial app. In the March 1998 issue of MSJ, you showed how to add the hand icon to the hyperlink, but now I'm wondering about another use for the class. In Microsoft® Internet Explorer, you can use the Tab key to tab through all the hyperlinks in a Web page, and press Enter to click the link. Can I make CStaticLink do the same thing? Am I asking too much?

Q I've been using your CStaticLink class for a couple of years now in my company's commercial app. In the March 1998 issue of MSJ, you showed how to add the hand icon to the hyperlink, but now I'm wondering about another use for the class. In Microsoft® Internet Explorer, you can use the Tab key to tab through all the hyperlinks in a Web page, and press Enter to click the link. Can I make CStaticLink do the same thing? Am I asking too much?

Tom Gifford

A If you asked me to lend you a thousand dollars, that would be asking too much—but keystrokes for CStaticLink? That's a reasonable request. Especially since as a keyboard man, I can relate. I hate reaching for the pokey old mouse (which is why I use Emacs), and I hate it when apps don't provide adequate keyboard support. So you're definitely not asking too much; on the contrary, you get the user interface gold star merit medal!

A If you asked me to lend you a thousand dollars, that would be asking too much—but keystrokes for CStaticLink? That's a reasonable request. Especially since as a keyboard man, I can relate. I hate reaching for the pokey old mouse (which is why I use Emacs), and I hate it when apps don't provide adequate keyboard support. So you're definitely not asking too much; on the contrary, you get the user interface gold star merit medal!

For readers just jumping in, CStaticLink is a class I first wrote way back in December 1997 to let you add Web links in forms or your About dialog. CStaticLink is based on CStatic, MFC's class for static controls. I've enhanced CStaticLink over the years as readers asked for more features, and it appears I'm about to do it again. CStaticLink handles mouse clicks to launch the URL, and does other friendly things like paint the text in the standard blue/purple unvisited/visited link colors (which you can customize), underline the text, and display the proper finger-pointing cursor when the mouse hovers over the link. CStaticLink even automatically loads the URL from your resource file by looking for a resource string with same ID as your control. If you're not using CStaticLink yet, check it out.

But now Tom has wisely asked how to make static links keyboardable (to coin a verb). There are two basic things you have to do: first, you have to make the links Tab-able. Second, you have to handle the key that navigates the link. Let's start with tabbing.

When the user presses Tab in a dialog, Windows® moves the input focus to the next control in the tab order. The focus only lands on controls that are tab stops—that is, controls that have the WS_TABSTOP style. By default, the dialog editor doesn't create static controls with WS_TABSTOP. Why should it? Static controls are, after all, static. They don't do anything. They never change and they don't interact with the user.

But now I've given CStaticLink life by allowing mouse clicks, so it's appropriate to allow tabbing. One of the cardinal rules of user interface design (pay attention, folks!) is: whatever users can do with the mouse, they should also be able to do with the keyboard. It's not only friendly to keyboard folks like me, it comes in handy for mouse people when their mouse breaks. Perhaps your user has a cordless mouse, and the batteries just died at 1:00 AM and the report is due first thing that morning—it's sure nice to know you can get the job done using only the keyboard.

So the first thing to do is make the static link a tab stop. Just add WS_TABSTOP to the style in your resource file, or select True for the control's Tabstop property in Visual Studio® .NET (see Figure 1).

Figure 1 Making the Static Link a Tab Stop

Figure 1** Making the Static Link a Tab Stop **

Once you make the control a tab stop, users can tab to it when the dialog is running. The only problem now is that when they do, nothing happens. There's nothing the user can do, there's not even any visual indication that the control has focus. If you tab to an edit control, it displays a blinking cursor. If you tab to a listbox, Windows draws a dotted focus rectangle around the first item. If you tab to a button, Windows draws a focus rectangle inside the button. But CStaticLink does nothing. Nada.

So I have to add some visual cue that tells the user "you are here." For hyperlinks (<A> elements), Internet Explorer draws a focus rectangle around the link text. So why not do the same here? Especially since Windows has a handy function called (oddly enough) DrawFocusRect, which has the nifty characteristic of using an XOR raster op, so calling it twice erases the rectangle. The time to draw the focus rectangle is when your control gains focus; the time to erase is when it loses it. In code, it looks like the following:

void CStaticLink::OnSetFocus(...) { DrawFocusRect(); } void CStaticLink::OnKillFocus(...) { DrawFocusRect(); }

What could be easier? This even works if your control loses focus because the user switched to another app (as opposed to tabbing to another control).

Discerning readers may be wondering: where are the arguments? The DrawFocusRect in the preceding snippet isn't the real DrawFocusRect. It's an impostor I wrote, a protected CStaticLink member function that does a bunch of setup before calling the real DrawFocusRect. Figure 2 shows the code. It's mostly the usual GDI fare—selecting fonts, converting coordinates, and so on—so I'll only highlight the interesting details.

Figure 2 DrawFocusRect.cpp

///////////////// // Gaining or losing focus: draw focus rectangle. // For bitmaps, use window rect; for text, use actual text rectangle. // void CStaticLink::DrawFocusRect() { CWnd* pParent = GetParent(); ASSERT(pParent); // calculate where to draw focus rectangle, in screen coords CRect rc; DWORD dwStyle = GetStyle(); if (dwStyle & (SS_BITMAP|SS_ICON|SS_ENHMETAFILE|SS_OWNERDRAW)) { GetWindowRect(&rc); // images use full window rectangle } else { // text uses text rectangle. Don't forget to select font! CClientDC dc(this); CString s; GetWindowText(s); CFont* pOldFont = dc.SelectObject(GetFont()); rc.SetRectEmpty(); // important—DT_CALCRECT expands, so start empty dc.DrawText(s, &rc, DT_CALCRECT);// calculate text rect dc.SelectObject(pOldFont); ClientToScreen(&rc); // convert to screen coords } rc.InflateRect(1,1); // add one pixel all around pParent->ScreenToClient(&rc); // convert to parent window coords CClientDC dcParent(pParent); // parent window's DC dcParent.DrawFocusRect(&rc); // draw it! }

The real ::DrawFocusRect (or its MFC equivalent, CDC::DrawFocusRect) needs a rectangle, of course, as well as the device context (DC) to draw on. But which device context should I use? Normally you should only draw in your own space, not someone else's—that is, in your control's window or client DC. But this is a case where drawing outside the window is appropriate because the focus rectangle needs to be a little bigger than the control in order to look right. So CStaticLink::DrawFocusRect draws on its parent window's client DC. Drawing focus rectangles is one of the few situations where it's OK to draw directly on the screen or parent window. In general, it's OK when you're doing some kind of temporary XOR thing, like drawing an icon or transparent image while dragging it across several windows; in that case you'd probably use the screen DC. If you paint on another window's device context, the only rule is: whatever thou paintest, thou shalt also unpaintest!

Next, what rectangle should I use? The window rectangle, of course. Think again. The window rectangle is fine for bitmaps where the bitmap is the same size as the control, but what about text? With text, the control is often bigger than the text, usually wider. Who bothers to size their static controls perfectly? If you use the window rectangle, you could end up with a long rectangle enclosing a small string of text, or even bumping into another control, as shown in the top part of Figure 3. This is the sort of thing that makes you look bad. That's why for text, CStaticLink::DrawFocusRect first calls CDC::DrawText with DT_CALCRECT to calculate the rectangle that exactly encloses the text. Inflate by one pixel all around for good measure, convert to parent window client coordinates, call CDC::DrawFocusRect and—voilà! The lower part of Figure 3 shows the happy result.

Figure 3 Without DT_CALCRECT and With DT_CALCRECT

Figure 3** Without DT_CALCRECT and With DT_CALCRECT **

So now the user can tab to the static link and see a focus rectangle. The last thing to do is handle keyboard input. Which key should navigate the link? Internet Explorer uses Enter, but I don't like that for two reasons. First, the accepted user interface model for pressing a button from the keyboard is to press the space bar. A hyperlink is like a button, so space makes more sense to me. This is how dialogs do it, and what Internet Explorer does for buttons in Web forms. I don't know why the Redmondtonians chose Enter to navigate links in Internet Explorer. Enter usually means "I'm done," the same as pressing OK in a dialog. Since CStaticLink is designed for use in dialogs, it shouldn't conflict with Enter. The second reason is that trapping Enter in dialogs requires more work (see my July 2000 column).

So when the user tabs to the link, pressing the space bar should navigate the link. To make it happen, you have to handle two messages. WM_CHAR is the obvious one:

void CStaticLink::OnChar(UINT nChar,...) { if (nChar==VK_SPACE) { Navigate(); } }

But before your static link can even get WM_CHAR, you have to tell the dialog you're interested. Normally static controls don't get WM_CHAR messages (because they're static, remember?). There's a special message to tell the dialog you want them—WM_GETDLGCODE:

UINT CStaticLink::OnGetDlgCode() { // tell dialog I want chars return DLGC_WANTCHARS; }

And with that, all is done. Now users can tab to hyperlinks and press the space bar to navigate. Cool.

One final warning: be careful to choose the correct tab order for your static links. Your hyperlinks should usually come at the end of the tab order, even if they appear visually at the top of the dialog. You probably don't want your dialog to start with the focus on your ACME logo link. And if you use CStaticLink in a form/dialog with other controls, you probably don't want the Tab key to jump from an edit box to a hyperlink to another edit box or button. So my advice is to keep all your hyperlinks at the end of the tab order, unless you have good reason not to.

I wrote a fully working sample program called LinkTest that uses the new keyboard-capable CStaticLink. You can download it from the link at the top of this article.

Q I have an MFC app that calls ShellExecute to open a Web page. For example:

ShellExecute "http://www.microsoft.com"

This was working fine until I started using the Managed Extensions and suddenly it stopped working. The error code returned is 5, which is ERROR_ACCESS_DENIED in WinError.h. I don't understand why access is denied. Doesn't ShellExecute work with the Managed Extensions?

Q I have an MFC app that calls ShellExecute to open a Web page. For example:

ShellExecute "http://www.microsoft.com"

This was working fine until I started using the Managed Extensions and suddenly it stopped working. The error code returned is 5, which is ERROR_ACCESS_DENIED in WinError.h. I don't understand why access is denied. Doesn't ShellExecute work with the Managed Extensions?

A You're among the many who have discovered this unfortunate bugaboo. Indeed, as soon as you throw the /clr switch to use Managed Extensions, ShellExecute fails if you try to use it to open a Web page. This one stumped me for a while. What could the Managed Extensions and /clr possibly have to do with ShellExecute? And why do you get an access violation?

A You're among the many who have discovered this unfortunate bugaboo. Indeed, as soon as you throw the /clr switch to use Managed Extensions, ShellExecute fails if you try to use it to open a Web page. This one stumped me for a while. What could the Managed Extensions and /clr possibly have to do with ShellExecute? And why do you get an access violation?

As is so often the case in Windows, the error code provides little clue as to what's really happening. But a little hunting through the MSDN Library reveals the answer in an article called "Calling Shell Functions and Interfaces from a Multithreaded Apartment". Eons ago, ShellExecute began life humbly; all it did was let you run a program, that is, an EXE file. As Windows has become more complex, ShellExecute has grown to the point where you can "execute" almost anything—such as a disk file (open the file with the associated program), FTP protocol, or Web page—just by passing the file name or URL. This works through shell extensions and IShellExecuteHook, which is a COM interface that extends the shell by telling it how to "execute" strings passed to ShellExecute(Ex). For example, there's an HTTP protocol extension hook that handles strings that begin with "http://". The extension handler launches the default browser to the given URL.

The problem is that using /clr and the Managed Extensions forces your app into multithreading mode because the garbage collector wants to run asynchronously in a separate thread. But as the INFO article explains, many IShellExecuteHook extensions don't work in multithreaded environments because they don't have the required proxy/stub code for COM to marshal arguments and synchronize access. If you're confused, you're not alone. Suffice it to say that ShellExecute simply doesn't work for all cases in multithreaded environments.

So, if you're already using the Managed Extensions, why not use the .NET Framework Process class and Process::Start instead of ShellExecute? There's a static overload that does just what you want:

Process::Start("http://www.microsoft.com");

Alas, if you try this at home, the same thing happens. The call throws a Win32Exception which yields an even more cryptic code in its NativeErrorCode property: ERROR_SXS_KEY_NOT_FOUND. The description for this error is "The requested lookup key was not found in any active activation context." Huh?

If you poke around the documentation for Process and read all the fine print, you'll discover that Process uses something called StartInfo in order to tell it how to start the process. One of the properties in StartInfo is UseShellExecute. By default, UseShellExecute equals True, which tells the Framework to launch the process using the shell, that is, ShellExecuteEx. Okay, so try setting it to False. But then, as the fine print explains, you can only start EXEs, not file names or URLs. You're hosed either way, and going in circles.

There's even more fine print in the documentation for Process::Start, advising you that if you want to UseShellExecute, you must make sure to specify [STAThread] (single-thread apartment model) as an attribute on your app's main function:

// in C# public class MyForm : Form { [STAThread] public static void Main(string[] args) { ... } }

Well, that's fine for C# or even "pure" (non-MFC) C++ programs, which have their own main functions, but what if you're using MFC? In that case, main, _tmain, _tWinMain, or whatever entry point it is for your platform is buried deep inside MFC where you can't edit the source to add [STAThread]. You could use /ENTRY:MyMain and write your own [STAThread] MyMain that calls the CRT startup, but now life is getting ugly. Surely there must be a simpler way.

In fact, there is a way to force single-thread apartment (STA) model threading in MFC apps without using [STAThread]. All you have to do is call CoInitialize(NULL) before the Framework tries to call CoInitializeEx(COINIT_MULTITHREADED). Figure 4 shows a little class that does this.

Figure 4 Force STA Threading

////////////////// // Use this class to force STA (single-threaded apartment model) // threading in mixed-mode apps. To use, instantiate a static // instance in your main application module, for example, MyApp.cpp or // anywhere where the constructor will run before the CLR. // class CSTAThread { public: CSTAThread() { CoInitialize(NULL); } ~CSTAThread() { CoUninitialize(); } };

The constructor calls CoInitialize(NULL) (STA threading) and the destructor calls CoUninitialize. So all you have to do is insert an instance in your MFC program's main application module:

// This does the equivalent of the [STAThread] attribute CSTAThread forceSTAThread;

Pretty clever. (Thanks to Redmondtonian Martyn Lovell for suggesting this to me.) In Visual C++® 2005, you'll be able to tell the linker you want STAThread on your entry point, but for now you can use CSTAThread.

One other easy way to launch URLs from your app, which works even in multithreading mode, is by using rundll32.exe, a handy program that lets you call a function in any DLL. You give it the DLL, the function, and the parameters you want to pass, and rundll32.exe calls the DLL function with your parameters. Rundll32.exe is extremely versatile. You can use it to shut down or restart Windows, create shortcuts, and launch control panel applets. There's probably even a way to make it run your dishwasher if you have the right hardware. I've seen entire Web sites devoted to all the tricks you can do with rundll32.exe; it's simply a matter of knowing which DLLs to call. You can use rundll32.exe to launch a URL from the command prompt like so:

rundll32.exe url.dll,FileProtocolHandler www.microsoft.com

The function FileProtocolHandler in url.dll does the job. With ShellExecute, it looks like this:

LPCTSTR url = _T("www.microsoft.com"); CString args; args.Format(_T("url.dll,FileProtocolHandler %s"), url); ShellExecute(NULL, _T("open"), _T("rundll32.exe"), args);

This works even in multithreaded apps because you're giving ShellExecute a true EXE, not a file name it has to run using shell extensions and IShellExecuteHook. The only drawback is you don't get any error code back from url.dll if it can't open the URL. For this reason, I recommend using CSTAThread instead and directly invoking the URL with ShellExecute, especially in MFC apps, which work better with apartment model threading anyway.

As a practical example of this, I updated my CStaticLink class from the previous question to use CSTAThread and ShellExecute, and wrote a managed test program, LinkTest, to prove it works in managed mode. I've included the CSTAThread class in StatLink.h. So now CStaticLink has yet another bonus feature: it works in both native apps and apps compiled with /clr and the Managed Extensions. As always, you can download the code from the MSDN Magazine Web site. Happy programming!

Send your questions and comments for Paul to  cppwork@microsoft.com.

Paul DiLascia is a freelance writer, consultant, and Web/UI designer-at-large. He is the author of Windows++: Writing Reusable Windows Code in C++ (Addison-Wesley, 1992). Paul can be reached at www.dilascia.com.