Dr. Dobb's Journal January 1997
Al is a DDJ contributing editor. He can be contacted at 71101.1262@compuserve.com.My buddy Al Williams writes a column for Dr. Dobb's Sourcebook and an occasional article for DDJ. He is also a writer of programmers' books. Besides our first names, occupations, fondness for the output of most major domestic breweries, and having gorgeous wives, we have many other things in common, not the least of which is that we seem to get each other's e-mail all the time. There must be something about our last names that confuse readers. Williams and Stevens. Fairly common names; I can see how you might mix us up. We don't look alike, but then most of you have never seen us. I'm better looking, but Al is younger. At least once a month, I get a message from a reader asking a question about one of Al's columns or books, and he gets a message asking about one of mine. It has become a running joke between us.
Al's most recent book foreshadows what programming will be like before very long. I would be happy for you to think that I wrote this book. Happier still if I had actually written it. It's called Developing ActiveX Web Controls (The Coriolis Group, 1996, ISBN 1-57610-002-2). If you plan to be a programmer in the next century, you should read this book. Besides being straight to the point and excruciatingly relevant, this book allows you to enjoy the refreshing writing style of my pal, Al Williams. Usually I do not care for books that go for the jocular, but Al pulls it off. Maybe it's because I know him so well and can see his face and hear his voice in every word. He always makes me laugh. More than likely, it's because I never had to carefully reread a paragraph to parse it and figure out what was being said. It's just that interesting, and it's just that clear.
As a writer of books and a staunch defender of author's rights, I take the publishers to task for what can best be called a lapse in judgment. They, too, are my friends, so I think -- I hope -- I can take this liberty. They use Al's book to promote someone else's book. In an apparent act of commercialism, they plug the other book and their new line of books with similar (and tasteless and objectionable, in my opinion) titles on the cover of Al's book. On the cover! Then they stick a completely irrelevant chapter from the other book in the back of Al's book to make you think you're getting a bonus instead of some gratuitous subliminal plug. If I were Al, I'd hop on my hoss, mosey over to Scottsdale, and kick some, er, uh, never mind.
In the November 1996 "C Programming" column, I explained how to convert a dialog-based MFC application to use the Property Sheet/Property Page idiom for the user interface. That small example did not provide for an online Help feature. That omission would be a grave error for a contemporary Windows 95 program. Users expect standard Windows Help for all their applications, no matter how small or how intuitive the procedures. Even the Windows 95 Clock applet has a Help database. Coincidentally, that program is a property page dialog-based application that includes context-sensitive help for the controls on the dialog pages, which is a subset of what I want to add to my application.
My objectives go beyond what the Clock applet does. Like Clock, I want that little question mark icon in the upper right of the dialog window for context-sensitive help on the controls. But I also want the Help button back on the bottom of the dialog, and I want everything to behave as a user expects it to. Finally, I want to enable tooltips -- those little yellow one-liners that pop up when the mouse lingers somewhere -- for all the dialog controls.
The files PropSheet.h, PropSheet.cpp, Sheet.h, Sheet.cpp, Page.h, and Page.cpp, which constitute the majority of the project, are available electronically; see "Availability," page 3.
In November, I disabled the Help button on the PropertySheet dialog window. Now we want it back. Put m_psh.dwFlags |= PSH_HASHELP; in the derived CPropertySheet class's constructor, and m_psp.dwFlags |= PSP_HASHELP; in the derived CPropertyPage class's constructor. These flag settings enable the Help button. Most Property Sheet applications will have more than one page. It's a good idea to have a base class for the several pages. The base page class is derived from CPropertyPage. You can put common page operations, such as enabling the Help button, in this base class. If some pages have help and others do not, put m_psp.dwFlags &= ~PSP_HASHELP; in the constructors of the pages that provide no help. When the user opens a page that has no help, the Help button is dimmed and disabled.
In November I removed the application class's message map. Now I'll put it back to demonstrate a point. If you selected the AppWizard context-sensitive help feature, the application class's message map includes ON_COMMAND(ID_HELP, CWinApp::OnHelp) I'm restoring the application class's message map to get that statement back. With that statement in place, every time you press F1 or click the Help button, MFC automatically calls WinHelp with the calculated help ID of the active property-page dialog window. The current Microsoft view of how help should work says that each dialog has its own help display that describes all the dialog's controls. You don't get help for individual controls on a dialog. A dialog-based application, particularly one that uses the property-page idiom, might not fit that model. Inasmuch as the application's command structure is implemented with dialog controls, I prefer to have context-sensitive popups for each of those controls, similar to what a conventional document/view application has for its menu and toolbar commands. The ID_HELP statement in the application class's message map defeats that objective.
To make context-sensitive help work on a dialog, you must copy the ID_HELP statement into the message map of the derived CPropertyPage class. You cannot, however, make an exact copy. The CWinApp::OnHelp function is protected, so classes not derived from CWinApp cannot call CWinApp::OnHelp. You need to provide an OnHelp function for the page class and use it to call WinHelp. This measure is necessary in order to implement the question mark button strategy, discussed next, to work. Example 1 shows how to implement the page class's OnHelp function.
You can leave the ID_HELP statement in the application class's message map or take it out. Either way works. Now, when the user clicks the Help button, MFC calls your page class's OnHelp function, which calls the CWinApp::WinHelp function to get context-sensitive help on the dialog page. Remember that your page class is probably a base class for all the property page dialogs. My small example uses only one page, so the class is not a base.
Observe that the call to WinHelp adds a constant 0x20000 to the dialog's ID. This procedure mimics what CWinApp does to generate a Help ID value. More about Help IDs later.
Until now, nothing that we have done really changes how the program behaves when you press the Help button. But by taking the Help button's control away from the CWinApp class, we have enabled context-sensitive help via the question mark button, which would not otherwise be possible.
The theApp object that I use to call WinHelp is the name of the globally instantiated application object. AppWizard does not make this object externally visible, but you can put an extern declaration in the application class's header file.
Next we want to put the question mark button in the upper-right corner of the dialog window next to the minimize/maximize/restore button (if you use it) and the close button. The question mark button provides dialogs with a form of context-sensitive help. Nondialog applications often include a question mark tool button on the toolbar to implement this feature. When you click the question mark, the mouse cursor changes to a pointer with an adjacent question mark. Next, you click whatever you want help with. This procedure is the mouse equivalent of pressing the F1 key when a menu command is selected and ready to be chosen.
Recall from the November "C Programming" that the derived CPropertySheet class has an Initialize function. The derived CPropertyPage class's overridden OnInitDialog function calls its parent's Initialize function to disable and hide the OK button and to change the Cancel button's name to Exit. Put ModifyStyleEx(0, WS_EX_CONTEXTHELP); at the beginning of the Initialize function.
Adding the WS_EX_CONTEXTHELP extended style to the Property Sheet class puts the question mark button at the right end of the dialog's title bar.
If you do only what we have discussed so far, when you click on the question mark button and then click on a control, you'll get the same result that you get by clicking the Help button. MFC calls CWinApp::OnHelp, which calls WinHelp with the dialog's calculated help ID. MFC does not call your page class's OnHelp function in this case, but since that function mimics what CWinApp::OnHelp does, the result is the same.
We want the question mark button to call WinHelp with the help ID of the control clicked rather than with the help ID of the dialog. To do that, you must provide a handler for the WM_HELPINFO message. Microsoft has not documented this message very well, and what documentation they do provide is well hidden. The first clue that the message even exists is found in ClassWizard, which lists the WM_HELPINFO message among those available to CDialog classes. If you click on the word WM_HELPINFO in Developer Studio's editor and press F1, you are told that there is no entry in the online help for that message. If you use ClassWizard to add a message handling function for WM_HELPINFO, it adds the function named OnHelpInfo, for which you are told, incorrectly, that there is also no online description. There is, however, online documentation for ON_WM_HELPINFO. ClassWizard adds an entry for ON_WM_HELPINFO to the dialog's message map when you add its message handler. The online Help display of ON_WM_HELPINFO includes a link to the description of OnHelpInfo. That description tells only about the message as it relates to the F1 key. By experimenting with this message handler, however, I learned that it also applies when the user uses the question mark button to request context-sensitive help on a control. Example 2 shows the addition of a WM_HELPINFO message handler to the page class.
The OnHelpInfo handler's argument is a pointer to a HELPINFO structure that describes the control being queried. If the control's context type is HELPINFO_ WINDOW (which it always seems to be), the handler calls ::GetDlgCtrlID to compute the control's ID from its window handle, which is one of the HELPINFO structure members. The handler converts the control ID into its help ID and calls WinHelp.
Note that this procedure adds 0x10000 to the control ID to compute a Help ID. Again, more about this later.
Override the CPage class's OnDestroy function to add the statement theApp.WinHelp(0L, HELP_QUIT); which tells the Help application that it can terminate if no other applications are using it. You can't put this statement in the application class's ExitInstance function, because WinHelp expects the application window to still be active, which it is not by then.
If you do not include this statement, and the user has Help in view when closing the application, the Help application window remains on the screen and the user must close it manually.
Help ID codes are an anomaly with property pages. When you add a control to a property-page dialog, Developer Studio allows you to say that the control has a Help ID. As soon as you run the application, MFC throws an assertion exception. Example 3 is extracted from the comments that accompany the assertion in the MFC source code. These comments tell us in a roundabout way that controls on property-page dialogs may not, under any circumstances, have Help IDs. Until you remove the Help ID option from all the controls in all the property pages, your program aborts every time you try to run it. Perhaps this is Microsoft's way of enforcing that help model that I mentioned before. Fascist programming, I call it.
The Help compiler, which translates a complex set of text files into a .hlp file, expects Help ID source codes to begin with the characters HID. AppWizard constructs a mind-boggling procedure to implement this requirement.
If you tell AppWizard that you want context-sensitive help when you build the project, AppWizard builds a skeleton help file and puts the commands into the make procedure to update it as you make changes to the project that would affect the contents of the help file. If you don't tell AppWizard that you want context-sensitive help, you have to set up that procedure yourself. To work inside the procedure and coerce it into going outside the realm of standard MFC programming, you have to understand the procedure. I'll try to explain it. It won't be easy.
When you choose to include context-sensitive help, AppWizard builds a batch file named "MakeHelp.bat." That file, which is run by the project's makefile, which in turn is run from Developer Studio's build commands, builds a file named "project.hm," where project is the name of your project. The batch file calls a utility program named "makehm.exe" to read the AppWizard- and Developer Studio-generated resource.h file and convert the IDs of windows and commands into Help IDs, which it writes into project.hm. This process adds constant values to the IDs in resource.h. These values are among those that I mentioned earlier.
MakeHelp.bat runs the Help compiler, passing it the name of the project.hpj file (again, project is the project name). That file, also generated by AppWizard, includes the project.hm file, which provides the Help compiler with all the Help IDs that start with HID. Those Help IDs are used in the project.rtf file (also generated by AppWizard) to associate Help IDs with Help topics. See, I told you it wouldn't be easy.
Now, inasmuch as property-page dialog controls have, by default, control IDs that start with IDC_, and inasmuch as they are not supposed to have Help IDs, the procedure just described does not build Help IDs from the control IDs. What is missing is an entry in the MakeHelp.bat file. You can add that entry. Example 4 shows the statements that you add to MakeHelp.bat.
When you build the project.rtf help text file -- a procedure that I could not begin to explain in this space -- use the HIDC_ prefixed mnemonics to associate controls with topics.
The next time somebody tries to tell you that visual and object-oriented programming and contemporary integrated development environments are making the programmer's job easier, read the preceding explanation to them. No one ought to have to muck about in such mire.
I want to add Tooltips for the dialog controls to my application. Tooltips are usually associated with toolbar buttons, which are themselves associated with menu commands. MFC provides a convenient way to add tooltip text to a menu command. Each menu command has an associated prompt-text string that displays in the application's status bar when the command is selected. You supply that prompt in Developer Studio as one of the properties of a menu command. When you add a \n and some text to the end of the prompt, that text is used for a Tooltip. When you assign the same command ID to a toolbar button, the Tooltip text displays in a little yellow window when the user allows the mouse cursor to linger on the button.
The property sheet application has no menu. To support Tooltips for its command controls, you must implement the feature yourself. Add EnableToolTips( TRUE); to the base page class's OnInitDialog function after the call to CPropertyPage::OnInitDialog().
Next, you need a handler that tells MFC what the text is for each Tooltip. MFC sends a notification to a Tooltip-enabled window whenever the mouse lingers over one of the window's child controls. Listing One which shows the implementation of the Tooltip notification message, is an adaptation of one that Microsoft's online documentation provides (the difference being that this one really works). The ON_NOTIFY_EX statement in the message map specifies the name of a function to call when the window receives the TTN_NEEDTEXT message. This message says that the system needs text for a Tooltip, which the handler must provide. The handler function receives four arguments, but only the second argument, a pointer to a TOOLTIPTEXT structure, is important to this procedure. From that structure the handler gets the window handle of the control where the mouse cursor is sitting. The ::GetDlgCtrlID function converts that handle to the control's ID, which the handler uses to determine which Tooltip text string to supply.
The default case must set the Tooltip text to a null string. Otherwise MFC uses the most recent Tooltip text value for controls that have no Tooltip text assigned by the switch. When you provide a null string, MFC does not display a Tooltip.
In November I reported that every execution of this program under the Visual C++ 4.2 debugger produces the following message in the Debug window:
First-chance exception in PropSheet.exe (COMCTL32.DLL): 0xC0000005: Access Violation.
It still does. I said that I did not know what the message meant, and asked readers if they knew. Many of you replied. It turns out that the message is a kind of harmless warning. The online help for CPropertySheet::DoModal says this:
The first time a property page is created from its corresponding dialog resource, it may cause a first-chance exception. This is a result of the property page changing the style of the dialog resource to the required style prior to creating the page. Because resources are generally read-only, this causes an exception. The exception is handled by the system, and a copy of the modified resource is made automatically by the system. The first-chance exception can thus be ignored.
One reader reported that the exception goes away when you upgrade COMCTL32.DLL to the one that comes with Internet Explorer 3.0. Sounds like an anti-Netscape conspiracy to me.
DDJ
class CPage : public CPropertyPage {// ...
BOOL OnToolTipNotify(UINT, NMHDR* pNMHDR, LRESULT*);
DECLARE_MESSAGE_MAP()
};
BEGIN_MESSAGE_MAP(CPage, CPropertyPage)
ON_NOTIFY_EX(TTN_NEEDTEXT, 0, OnToolTipNotify)
// ...
END_MESSAGE_MAP()
BOOL CPage::OnToolTipNotify(UINT, NMHDR* pNMHDR, LRESULT*)
{
TOOLTIPTEXT* pTTT = (TOOLTIPTEXT*)pNMHDR;
if (pTTT->uFlags & TTF_IDISHWND) {
UINT nID = ::GetDlgCtrlID((HWND)pNMHDR->idFrom);
pTTT->hinst = AfxGetResourceHandle();
switch (nID) {
case IDC_EDIT1:
pTTT->lpszText = "An editor";
return(TRUE);
case IDC_TOGGLEEXIT:
pTTT->lpszText = m_pCSheet->IsExitEnabled() ?
"Disable exit" : "Enable exit";
return(TRUE);
default:
pTTT->lpszText = "";
break;
}
}
return(FALSE);
}