This article may contain URLs that were valid when originally published, but now link to sites or pages that no longer exist. To maintain the flow of the article, we've left these URLs in the text, but disabled the links.


March 1998

Microsoft Systems Journal Homepage

C++ Q&A

Download cMar98.exe (31KB)

Paul DiLascia is the author of Windows ++: Writing Reusable Code in C++ (Addison-Wesley, 1992) and a freelance consultant and writer-at-large. He can be reached at askpd@pobox.com or http://pobox.com/~askpd.

      First, I'd like to clear up a minor goof in my February 1998 column. I claimed you can't use Visual Studio to extract resources from EXEs or DLLs. As several readers pointed out, you can. Just select the resource and right-click to get a menu with Export as an option (see Figure 1). Gee, why didn't I think of that?
Figure 1 Exporting Resources
Figure 1 Exporting Resources

Q How can I disable the tabs in a CTabCtrl? I have seen other applications with property sheets that have disabled tabs. For example, the Grammar tab in the Tools Options dialog in Microsoft® Word is disabled if grammar-checking is not installed. I figured there must be some flag to set somewhere—but I've looked all over and I can't find it. Please help!

Several readers
A Isn't it strange how Microsoft products keep coming out with all sorts of new user interface features that seem like they should be part of the operating system—but when you go to implement them they aren't there! And if you try to implement the features, you discover it takes thousands of hours of programming, which Microsoft can afford and you can't. All you have to do is look at any Office mega app with a tool like Spy++ to see the Redmondtonians don't follow the eat-our-own-dogfood philosophy of software development. Practically every window class is homegrown, with strange names like OpusStat and T3TbDock. And even though the Options dialog in Figure 2 looks like an ordinary property sheet with an ordinary tab control, the spy program reveals it's really something called "sa_sdm_Microsoft Word 8.0" (see Figure 3).
Figure 2 Disabled Tab
Figure 2 Disabled Tab


      So, you aren't going crazy. There is no disable-the-tab flag for tab controls. There's no way to disable a tab—no easy way, that is. But you can almost always defeat Windows® if you're prepared to spend the time and the brain power.

Figure 3 Microsoft Word in Spy++
Figure 3 Microsoft Word in Spy++


      CTabCtrlWithDisable is my answer to the problem of disabled tabs (see Figure 4), and TabApp is a dopey application that shows how to use it in a property sheet (see Figure 5). To use CTabCtrlWithDisable, you must derive your own tab control class from it and implement the pure virtual function IsTabEnabled. TabApp has a CMyTabCtrl that makes all tabs disabled except tab number three (see Figure 6):


 BOOL CMyTabCtrl::IsTabEnabled(int iTab) {
    return iTab!=2; // tab 3 disabled
 }
Once you've written IsTabEnabled, you can instantiate your tab control in your dialog, and subclass in OnInitDialog.

 // (in CMyPropSheet::OnInitDialog)
 HWND hWndTab = (HWND)SendMessage(PSM_GETTABCONTROL);
 m_tabCtrl.SubclassDlgItem(::GetDlgCtrlID(hWndTab), this);
The last step is to implement a PreTranslateMessage handler for your property sheet like this:

 BOOL CMyPropSheet::PreTranslateMessage(MSG* pMsg)
 {
   return m_tabCtrl.TranslatePropSheetMsg(pMsg) ? TRUE :
       CPropertySheet::PreTranslateMessage(pMsg);
 }
      To summarize: derive your own tab control, implement IsTabEnabled, instantiate and subclass the control, and handle PreTranslateMessage. The result is a tab control similar to the one in Figure 6. The only nongeneric code will be your tab control's IsTabEnabled function, which determines which tabs are enabled.
Figure 6 Tab Control
Figure 6 Tab Control


      So much for using CTabCtrlWithDisable. How does it work? There are two main aspects. First is drawing the tabs so the text looks disabled. Second is preventing the user from activating a disabled tab. To draw disabled tabs, CTabCtrlWithDisable makes the tab control owner-draw by overloading SubclassDlgItem:


 CTabCtrlWithDisable::SubclassDlgItem(UINT nID, 
                                      CWnd* pParent)
 {
    if (!CTabCtrl::SubclassDlgItem(nID, pParent))
       return FALSE;
    ModifyStyle(0, TCS_OWNERDRAWFIXED);

       • • • }
This function, which you must call from your property sheet's OnInitDialog handler, calls the base class CTabCtrl:: SubclassDlgItem to do the subclassing. Then, assuming that succeeded, it calls ModifyStyle to set the TCS_OWNERDRAWFIXED style. Since SubclassDlgItem is not virtual, my override can only work if you call it from within some scope where CTabCtrlWithDisable is defined. This is the case for the normal way you'd subclass a control—namely, from the OnInitDialog function of a dialog that contains an instance of it. If you have some weird setup where you call pWnd->SubclassDlgItem on a CWnd or CTabCtrl pointer, you must take other measures to set the style.
      Once TCS_OWNERDRAWFIXED is on, the next thing is to handle draw messages. Fortunately, MFC does a lot of friendly, good stuff to route WM_DRAWITEM messages to the virtual function CTabCtrl::DrawItem, so CTabCtrlWithDisable only has to implement it. The details are straightforward: if IsTabEnabled says a particular tab is disabled, DrawItem draws it using the embossed gray look; otherwise it uses the normal system color COLOR_BTNTEXT. Just to be nice, I introduced a new virtual function, OnDrawText, that encapsulates only the text-drawing portion of the draw operation. You can override it if you want to draw the tabs using some other font or color. This isn't necessary for doing disabled tabs, but it's a nice feature. At least one reader asked me how to do colored text in tab controls; you can do it easily by overriding OnDrawText.
      Once disabled text is painting correctly, the next thing is to actually disable the tabs—that is, prevent the user from navigating to a tab that's disabled. The basic idea is to use ON_NOTIFY_REFLECT to let CTabCtrlWithDisable handle its own TCN_SELCHANGING notification, and reject navigation to a disabled tab by returning nonzero. It sounds easy, but as you've learned many times by now, nothing is ever simple in Windows, even when it should be. The problem is that there are three different ways users can navigate to a new tab: by selecting the tab with the mouse, by pressing left or right arrow, or by pressing Ctrl+Tab or Ctrl+Shift+Tab from within the property sheet. Naturally, Windows processes each case differently.
      Let's take the mouse first, since that's the easiest. When the user clicks the mouse on a new tab, the tab control sends TCN_SELCHANGING to its parent window. The notification bounces back via message reflection to CTabCtrlWithDisable:: OnSelChanging:

 void CTabCtrlWithDisable::OnSelChanging(NMHDR* pnmh, 
                                         LRESULT* pRes)
 {

• • • int iNewTab = // get index of new tab
• • • if (!IsTabEnabled(iNewTab)) *pRes = TRUE; // disallow }
Assuming it can get the index of the proposed new tab, OnSelChanging calls IsTabEnabled to decide whether to allow the new tab. But how do you get the new tab? There's no iNewTab argument passed in the TCN_SELCHANGING notification; all you get is the control ID of the tab control in LOWORD(wParam). There's no special WM_NOTIFY struct, either; you only get the standard NMHDR with its usual HWND, control ID, and notification code. Why you get the control ID in wParam as well as the NMHDR is beyond me. But I confess there are many things in Windows that leave me wondering.
      How do you get the new tab index? Here's how I did it:

 TC_HITTESTINFO htinfo;
 GetCursorPos(&htinfo.pt);   // mouse pos
 ScreenToClient(&htinfo.pt); // to client coords
 int iNewTab = HitTest(&htinfo);
CTabCtrl::HitTest retrieves information about where a given point in client space is relative to the tab control. It sets a bunch of flags in TC_HITTESTINFO and returns the index of the tab containing the point, or -1 if point isn't on a tab. This solution works only if the TCN_SELCHANGING came as a result of a mouse click, not a keystroke. In the case of a keystroke, the mouse could be anywhere, including not even on the tab control at all.
      Preventing the user from landing on a disabled tab as a result of a keystroke requires a totally different approach. Once again, there are two possibilities. Case one occurs when the user presses the left or right arrow from within the tab control. Case two is when the user presses Ctrl+Tab or Ctrl+Shift+Tab from the property sheet. Both methods navigate to the next or previous page/tab in a property sheet. (Go ahead, try it!)
      In both situations, handling OnSelChanging is not good enough, even if you can figure out the index of the new tab. Why not? Because you need to know where you're coming from to know where you're going. If the user pressed Ctrl+Tab or right arrow (next page/tab) and landed on a disabled tab, you presumably want to skip the disabled tab and go on to the next one; but if the user pressed Ctrl+Shift+Tab or the left arrow (previous page/tab), you want to move backward. Are you with me?
      To handle the left or right arrow, I overrode PreTranslateMessage. This CWnd virtual function is one of the many places you can intercept a window's message. PreTranslateMessage's role is to translate keystrokes, so it's the logical place.

 BOOL CTabCtrlWithDisable::
    PreTranslateMessage(MSG* pMsg)
 {
    if (/* left or right-arrow */) {
       int iNewTab = (/* left-arrow */) ?
          PrevEnabledTab(GetCurSel(), FALSE) :
          NextEnabledTab(GetCurSel(), FALSE);
       SetActiveTab(iNewTab);
       return TRUE;
    }
    return CTabCtrl::PreTranslateMessage(pMsg);
 }
Next/PrevEnabledTab are two new functions that get the index of the enabled tab after or before a given one. SetActiveTab is another new function that calls CTabCtrl::SetCurSel to change the tab. As with DrawItem, these functions are straightforward, so I refer you to the code in Figure 4.
      There are two tricks I want to point out. Next/PrevEnabledTab have a BOOL argument that specifies whether or not to wrap. This is required because the normal way a tab control in a property sheet operates is that Ctrl+Tab and Ctrl+Shift+Tab wrap, but the left and right arrows do not. (I suspect this operation is more by accident than design.) The other trick is that in addition to changing the tab, SetActiveTab also sends TCN_SELCHANGING and TCN_SELCHANGED notifications so the parent property sheet has a chance to know what's going on in case it needs to do something special. In this case, TCN_SELCHANGING should, however, always return zero (success) since PreTranslateMessage only calls SetActiveTab with an enabled tab.
      Other than wrapping, the code to handle Ctrl+Tab and Ctrl+Shift+Tab is virtually the same as for the left and right arrows. The crucial difference is that Tab keys come from a property sheet, not the tab control. So I wrote another function, TranslatePropSheetMsg, which you must call from your property sheet's PreTranslateMessage.

 BOOL CMyPropSheet::PreTranslateMessage(MSG* pMsg)
 {
   return m_tabCtrl.TranslatePropSheetMsg(pMsg) ? TRUE :
        CPropertySheet::PreTranslateMessage(pMsg);
 }
TranslatePropSheetMsg works just like PreTranslateMessage, except that it translates Ctrl+Shift+Tab instead of VK_LEFT or VK_RIGHT; it handles parent window messages, not messages for the tab control itself; and it calls Next/PrevEnabledTab with bWrap=TRUE so navigation with the Tab key wraps.
      There's one final detail to work out before calling it a day. When a property sheet comes up, it normally starts on the first page. But what if the first page is disabled? Oops. I added a few lines to SubclassDlgItem so you go to the first enabled tab instead. (See DisabTab.cpp for details.) And if all the tabs in your property sheet are disabled, I have no idea what will happen. Your computer may blow up.

Q I liked your CStaticLink class from the December 1997 issue, but your control doesn't display the pointing finger cursor when the mouse is over the link the way most browsers do. How can I change CStaticLink to show a hand when the mouse is over the link?

More than one reader
A Yeah, yeah. OK, so I was lazy. That's the trouble with writing code people actually use—they always want new features. But you're quite right. It's a tad gauche to have a hyperlink with no changing cursor to alert the user to press the Go button.
Figure 8 CStaticLink in action
Figure 8 CStaticLink in action
      For those of you who are totally confused because you didn't read the December issue, CStaticLink is a little class I wrote that lets you convert an ordinary static text control into a hyperlink to a document, program, or URL (see Figure 7 and Figure 8). When you click the text, CStaticLink passes it to ShellExecute and away you go. CStaticLink even works with static icons. In the case of text, it displays the text in the standard blue with underlining, or purple if the link has already been visited. But, as you point out, the one thing CStaticLink doesn't do is change the cursor when the mouse is over the link. That's what I'm going to show you now.
Figure 9 Pointing Finger
Figure 9 Pointing Finger
      Most browsers use the pointing finger cursor in Figure 9 as a cue to the user that perhaps it's time to press the mouse button, or at least that it's possible. It requires two steps to implement. First, you have to write some code to change the cursor when the mouse is over the static control. That's the easy part. Part two is harder: finding the finger cursor itself.
      Assuming you have a handle to the cursor you want, displaying it when the mouse is over your window is just a matter of handling WM_SETCURSOR.

 BOOL CStaticLink::OnSetCursor(
    CWnd* pWnd, UINT nHitTest, UINT nMsg)
 {
    HCURSOR hCursor = AfxGetApp()->LoadCursor(
                      ID_MY_CURSOR);
    ASSERT(hCursor);
    ::SetCursor(hCursor);
    return TRUE;
 }
This is one situation where the global unary scope operator (::) is required—that is ::SetCursor and not SetCursor. CStatic has its own SetCursor function that does something totally different; it doesn't set the system cursor, it sets the static control image from a cursor resource. CStatic:: SetCursor is like CStatic::SetIcon, except it takes a cursor instead of an icon. If you don't use the global scope operator, C++ will assume the nearest scope and use CStatic:: SetCursor, definitely not what you want. (I found out the hard way.)
      Once you've set the cursor, make sure you return TRUE to indicate you've done your thing; otherwise Windows will do its thing, which is to use the registered window class cursor if there is one or the standard arrow if there isn't (as in the case of a normal static control). The arguments to OnSetCursor are as follows: pWnd is the window the cursor is over, which could be different from "this" if Windows is sending WM_SETCURSOR to the parent of the window the mouse is in. nHitTest is a code like HTCLIENT or HTBORDER that indicates logically where the cursor is within the window. nMsg is a mouse message like WM_MOUSEMOVE or WM_LBUTTONDOWN. You could use nMsg if you want to do something fancy like change the cursor from a finger to a mushroom cloud while the mouse button is down.
      OK, so much for setting the cursor. The next step is to figure out where to find the fickle finger in Figure 9. No, there's no built-in stock symbol like IDC_FINGER the way there is for IDC_ARROW (though there is one in Windows NT 5.0 and Windows 98). Nor is the finger in shell32 or any other obvious place. You know it has to be in Microsoft Internet Explorer somewhere, but you don't want to rely on having Internet Explorer installed on your client's machine.
      One way to get the finger is to press PrintScrn to grab a screen capture while the cursor is displayed—but good luck trying to capture the cursor, since it's not normally included in the snapshot. (There are articles floating around that describe ways around this.) Another way is to find a DLL or EXE that has the cursor and copy it. The folks in Redmond assure me the finger lives in the latest version of comctl32.dll, but for some reason when I try to open it as a resource in Visual C++, I get the message "Unknown Language 0x1, 0x1."
      Of course, I could always just pass the buck. That is, provide a public global like CStaticLink::g_hCursorLink and let you set it. Indeed, I did just that. But who remembers to set the cursor when you're rushing to get the demo out the door? Wouldn't it be nicer if CStaticLink could come up with its own finger in case you forget to supply one?
Figure 10 Winhlp32 uses the finger
Figure 10 Winhlp32 uses the finger

      So, racking my feeble brain for where else to find the fickle finger, I suddenly remembered Help. That old precursor to Web linking also uses the pointing finger to indicate when you can jump to a new help topic by clicking (see Figure 10). Being the sort of person who can put two and two together, I realized there had to be a finger somewhere in the help system. So I opened winhlp32.exe as a resource file in Visual C++ 5.0. This time there was no language 0x1, 0x1 error and, sure enough, the finger is right there as cursor resource #106. Once you know that, it's easy to write some code to grab it.

 // in OnSetCursor
 HMODULE hModule = LoadLibrary(_T("winhlp32.exe"));
 if (hModule) {
    g_hCursorLink =
       CopyCursor(::LoadCursor(hModule, 
          MAKEINTRESOURCE(106)));
 }
 FreeLibrary(hModule);
      The real code uses a flag bTriedOnce so it only loads the cursor once, the first time OnSetCursor is called. It's important to copy the resource with ::CopyCursor because Windows destroys the one from winhlp32.exe the minute you call FreeLibrary.
      Under Windows NT, things are a little tricky because there are two winhlp32.exe files, one in winnt\system32 and one in \winnt. The former is a stub that calls the latter, so you want to make sure you load the one in \winnt. So the final code in Figure 7 calls ::GetWindowsDirectory to build the absolute file name, for example "c:\winnt\winhlp32.exe".
      Now, please don't everyone send me mail about all the reasons this is yucky. (What if help is missing? What if the Redmondtonians change the resource ID?) I'll be the first to stand up right now, raise my right hand and say, "This Is A Kludge." But it's a good kludge! Winhlp32.exe is 99.9 percent guaranteed to live on any system. And don't forget: the load-it-from-winhelp strategy is intended only as a last resort, in case you forget to do the correct thing:

 CStaticLink::g_hCursorLink.
 // (in your app's InitInstance or 
 // some other good initialization place)
 CStaticLink::g_hCursorLink = 
    AfxGetApp()->LoadCursor(ID_MYFINGERPOINTER);
      Naturally, you can use whatever cursor your little heart desires as your hotlink cue, but I strongly encourage you to be a good UI citizen and use the finger. Just to be nice, I've included it with the source code in the file finger.cur.
      There's one more thing I need to say about CStaticLink. Some of you asked how to make links "remember" their visited/unvisited state. Say the user invokes one of your fancy new dialogs with CStaticLinks. The dialog comes up and the links are blue. The user clicks one of the links and the text turns purple. Fine. Now suppose the user closes the dialog, then opens it again. Should the link be blue or purple? In other words, should the link remember its visited state across dialog invocations?
      And the answer is, it's up to you! If you want to remember the visited state, go right ahead. That's why I made m_color public. So you can change it. Just copy m_wndLink.m_color somewhere after you run your dialog, then use the same value to initialize it the next time. You could even write a DDX macro for CStaticLink. If you're too lazy to copy state around, you can always make the whole dialog static.

 void CMyApp::OnAbout()
 {
    static CAboutDialog dlg;
    dlg.DoModal();
 }
Now whatever data members are in dlg (and any objects it contains) will retain their values across invocations of DoModal. If you don't want to make the whole dialog static, you can make just the CStaticLinks (m_wndLink1 and m_wndLink2 in the example) static. But you have to be careful to unsubclass the links (CWnd::UnsubclassWindow) when the dialog is destroyed; otherwise MFC is likely to get confused the second time you run your dialog and your m_wndLinks still have HWNDs in them. For the sake of brevity, Figure 7 shows only the new code for CStaticLink.

Have a question about programming in C or C++? Send it to askpd@pobox.com

From the March 1998 issue of Microsoft Systems Journal.