Dr. Dobb's is part of the Informa Tech Division of Informa PLC

This site is operated by a business or businesses owned by Informa PLC and all copyright resides with them. Informa PLC's registered office is 5 Howick Place, London SW1P 1WG. Registered in England and Wales. Number 8860726.


Channels ▼
RSS

C/C++

C Programming


November 1996: C Programming

A Star is Born

Al Stevens

Guess what? I'm doing an infomercial. That's right, I'm appearing in one of those late-night half-hour commercials, similar to those that peddle gadgets, exercise machines, psychic readings, motivational courses, and real-estate investment schemes to insomniacs. This enterprise is my first as a celebrity endorser (unpaid, by the way), although I doubt anyone in the audience will have heard of me. If any of you are up that late, you are probably pounding on the keyboard, and the TV set is dark.

It all started when my friend Ron Skipper rigged a book holder from a coat hanger and some scrap material. A retired airline pilot and confirmed bachelor, Ron often dines alone and likes to read while eating. Ron rigged this adjustable device to hold a book in an upright position so that he could read hands-off.

Ron frequently dines out, and the little device always gets a lot of attention. Everyone who sees his book holder wants one. Waiters and waitresses, other patrons, total strangers, all approach Ron unabashedly, interrupt his meal and his concentration, and demand to know where he got it and how they can get one. When I first saw it, I wanted one, too. As a consequence, Ron decided to build and sell what he calls the "Book Bear." He gave me an early prototype, which became a fixture on my desk, hosting at any time one of the many programming books and manuals that I use for reference. My Book Bear easily handles the five-pound, three-inch, three-ring, bound X3J16 draft proposed C++ specification document.

In addition to the web (http://www .bookbear.com), Ron's marketing strategies involve the infomercial, and there is something to be learned from this experience. The lesson is found in the economy of infomercials, which surprised me. You can have a professional half-hour infomercial developed and produced for as little as five grand (particularly if you have friends like me who are willing to participate), depending on where you have it produced. Big city studios are more expensive. So are celebrities (Dionne Warwick and Fran Tarkenton, not me). Ron used a small-town operation and local talent. For his investment, he gets a Betamax cassette suitable for distribution to TV stations. He'll need copies, of course, which cost more. Broadcast time is ridiculously cheap considering the exposure. He'll spend about $300 for a late-night half hour on a local cable station that covers several counties. Prime time costs about a hundred bucks more. Ron has been told that coverage in major cities costs less than $1000 per half hour.

What does all this mean to programmers? I haven't seen many software infomercials-just a few that peddle instructional video tapes on Windows 95 and various applications. It occurs to me that the infomercial could become what shareware was to the late 1980s and early 1990s. Shareware was a marketing phenomenon that provided exposure for the products of small, undercapitalized software developers. Most successful shareware vendors used their profits to produce and release shrink-wrapped commercial versions of their products, complete with more features, bound manuals, and 800- support and order phone numbers. While the infomercial is much more expensive to launch than a shareware mass upload, the potential return is impressive, assuming you understand who your market is and how to reach them. So, dear readers, get to work. Use those developer skills to produce a killer application aimed at overweight, compulsive-buying, plastic-wielding insomniacs who yearn to know what their future holds and how to build personal wealth and buy real estate with nothing down, who require self-empowering motivational instruction to quit smoking and improve the quality of their lives, and who need their abs hardened. With a package like that, who could go wrong?

The Property Page Dialog-Based Application

Recently, I set about to convert a dialog-based MFC application to use the Property Sheet/Property Page idiom for the user interface. Plenty of books and magazine articles explain how these new common controls work and how you use them to implement dialogs from within MFC applications. I found no references, however, on what turned out to be three significant issues, which are:

  • How to convert a standard dialog box to a Property Page.
  • How to use the idiom in ways not foreseen by MFC's and Visual C++'s designers.
  • How to build a property sheet dialog-based application as opposed to a dialog within an application.

As usual with MFC, you have to experiment and find your own way whenever you want to do anything out of the ordinary.

The Visual C++ Developer Studio doesn't let you start out with a property sheet dialog-based application. You get three choices: an MDI document/view application, an SDI document/view application, and a dialog-based application. The last choice is the closest one to what I wanted, so that's where I started.

A dialog-based application starts out with two basic classes, one derived from CWinApp for the application and one derived from CDialog for the dialog box. You can compile and run the application, and it displays a generic dialog box with a static text control and OK and Cancel buttons. The first things you have to do are remove the dialog box from the resource file and the dialog class from the project. Although CPropertyPage objects work just like CDialog objects, you can't convert the class derived from CDialog that Developer Studio provides. You can try, but when you compile and run the program, it aborts with an assertion from within MFC. Comments at the ASSERT statement say that Windows does not yet support extended styles for property-page dialogs. The comments are less than instructive-misleading, actually-about how to deal with the problem. The best thing to do is toss out what Developer Studio provides and build your own page dialogs from scratch.

Of course, when you are porting a dialog-based application to this idiom, you might want to leverage existing work, which means importing the resource file from the original application. In this case, change all the DIALOGEX statements to DIALOG. That should take care of most of it. The rest you'll have to feel your way through.

To remove a class from a project, you must open the FileView tab page in the Project Workspace Window and delete the xxxDlg.cpp file from the list (where xxx is the prefix that Developer Studio uses, based on the name of your application). Then you must remove the #include xxxDlg.h statements from the application class's .cpp file. The next time you expand the class tree control in the ClassView tab page of the Project Workspace Window, the derived dialog class is gone.

To remove the dialog from the project, open the ResourceView tab of the Project Workspace Window, select the dialog's resource ID, and press the Del key.

At this point, you cannot compile the program because the overridden InitInstance member function of the application derived class still contains code that instantiates an object of the dialog class and calls DoModal for that object. Ignore that code for the moment. You'll replace it soon.

Next you need a CPropertySheet class for your application. A tabbed dialog box consists of a CPropertySheet object that hosts several CPropertyPage objects. Any good Windows 95 MFC book explains all that. Use the ClassWizard to build a class derived from CPropertySheet. I'll call the new class CSheet for this discussion. The default implementation in sheet.h and sheet.cpp creates two constructors, one with a resource ID code to identify the dialog's caption in the title bar and one with a null-terminated string for the caption. Usually, you need only one of these constructors, so you can remove the other. Remember that a property sheet is not itself a dialog, so there is no entry in the resource file for a caption. Each property page is a dialog, but property pages do not have captions.

Both the CSheet constructors have a parent-window pointer parameter and an integer parameter that specifies which of several pages is open when the dialog initializes. Both parameters have default values. A dialog-based application has no parent window, so the default suffices. If you don't need the second parameter, you can eliminate it, too. The base CPropertySheet class needs those values, however. You can use constants for its arguments. Listing One shows the CSheet constructor.

Now you can return to the InitInstance function of the application class and replace the instantiation of the now-defunct dialog class with that of the CSheet class. Developer Studio instantiates the dialog as an automatic object and calls DoModal with code to test the IDOK or IDCANCEL response. For reasons that become evident soon, I'll put a pointer to the CSheet class data member in the application class and instantiate the dialog on the heap. Listing Two shows the CPropSheetApp class declaration as it appears so far. Listing Three shows the modified CPropSheetApp::InitInstance function that instantiates and executes the dialog along with the CPropSheetApp::ExitInstance function that deletes the dialog object. Don't forget to put an #include "sheet.h" statement in the application's .cpp file.

You can compile and run the program now, but it exits immediately because the property sheet has no pages. You need at least one class derived from CPropertyPage. You also need a dialog box for the page to implement, and you need to integrate the page into the sheet.

Begin by using the ResourceView tab in the Project Workspace Window to go to where you can insert a new dialog into the project. Delete the OK and Cancel buttons from the default dialog box that the resource editor creates. Add any controls that you need-I'll put an editbox into this example so that the application seems to have something to do-and make note of the dialog's resource ID, which is probably IDD_DIALOG1. I usually change these generic IDs to something more meaningful. Right-click the ID in the resources tree in the Project Workspace Window, choose Properties from the context menu, and change the ID in the Dialog Properties dialog box.

Next, use ClassWizard to add a class derived from CPropertyPage. I'll call my derived class CPage. Add an instance of this class as a data member in the CSheet class. Listing Four shows this addition. Don't forget the #include "page.h" statement. Use the AddPage function in the CSheet constructor to add the page to the sheet as Listing Five shows. Now you can compile and run the program, which opens a property sheet dialog box as in Figure 1.

Note in Figure 1 that the application includes four push buttons-OK, Cancel, Apply, and Help. The framework builds the property sheet this way because that is how the Microsoft designers decided that property-sheet dialogs should look. But suppose that you want to reconfigure the operating model. After all, this is an application, not a dialog that gathers user input for an application. Why does it need that grayed-out Apply button? Assuming that it does not, you can get rid of the Apply button by including m_psh.dwFlags |= PSH_ NOAPPLYNOW; in CSheet's constructor. This action eliminates the Apply button from the dialog, leaving the OK, Cancel, and Help buttons. I don't need both OK and Cancel, since, in this case, they do the same thing, which is close the dialog box and exit from the application.

You can change the OK button to a Close button and gray out the Cancel button by calling CPropertySheet::CancelToClose in the OnInitDialog function of the CPage class. You have to override OnInitDialog first, then add the function call. You can do that from ClassWizard by attaching a function to the WM_INITDIALOG message. If your application has more than one page, which it probably does, this call needs to be made from the first page to be selected. If you use the default selection, that's the first page added to the sheet with the AddPage function. If you use dynamic selection, perhaps opening the page that was open when the application last terminated, you should add the CancelToClose call to all the pages.

That action leaves the OK button (renamed "Close") enabled and the Cancel button grayed out. Now how do you get rid of that useless, grayed-out Cancel button? One way is to find its handle and send the button a message telling it to hide itself. But if you can do that, you can also just hide the OK button and use the Cancel button as an Exit button instead of using CancelToClose.

As it turns out, the buttons are child windows of the CSheet window, but the current open CPage window gets their OnOK, OnCancel, and OnClose messages. To hide a window, you need a pointer to its CWnd base class. To get the handle of a CPropertySheet's button, you have to wait until at least one of the CPropertyPage dialogs has been initialized. This means that the CPropertyPage objects must be able to communicate with the CPropertySheet object at the time of the page initialization, which means that a page must know the address of the sheet. No problem, because the pages are children of the sheet. The page classes can call a public Initialize member function in the sheet class from their OnInitDialog function (Listing Six) through the pointer returned by GetParent. The m_pCSheet is a data member pointer that I added to the CSheet class. The CPage constructor initializes that pointer to zero.

If there is only one page or if the CPage class in Listing Six is a base class for all the pages, the sheet's Initialize function executes only once (which is all it needs). The sheet's Initialize function juggles the dialog's buttons (Listing Seven). It gets the window handle for the OK button, then hides and disables that button. It then gets the window handle for the Cancel button and changes its label to Exit.

Now we have an application with only Exit and Help buttons. As an exercise, let's eliminate the Help button, leaving only an Exit button. The Help button transcends all the pages in the application. If any page needs help, the Help button is in view all the time. If one page does not need help, the Help button is grayed out while that page is selected. You specify that a page is not going to need help by inserting the m_psp.dwFlags &= ~PSP_HASHELP; statement in the page class's constructor.

Clearing the PSP_HASHELP bit in the page class's dwFlags member of the PROPSHEETPAGE structure tells the framework that the page needs no help. To eliminate the Help button altogether, clear this bit for all pages and then insert the statement m_psh.dwFlags &= ~PSH_HASHELP; in the CSheet class's constructor. This statement tells the framework to gray out the Help button when pages are selected that do not need help and to eliminate the button altogether if no page needs help.

Now, you have an application with only an Exit button. The last problem to solve is how to intercept the former Cancel (now Exit) button to ask if the user really wants to exit. Perhaps this is a keyboard-intensive application, and the user is liable to bump either the Enter or the Esc key by mistake. Either of those keys can exit the application, no questions asked. Disabling the OK button in Listing Six prevents the Enter key from sending an IDOK message to the dialog box, but if the Exit button is selected, both the Enter key and the spacebar choose it and exit the application. Pressing Alt+F4 or clicking the X button in the right end of the title bar have the same effect.

Overriding OnCancel in a class derived from CPropertyPage does not work. The Esc keystroke calls the overridden functions, but, unlike a standard dialog-based application, the system has already decided to close the dialog by then.

You can't override the OnCancel functions for the CPropertySheet class because CPropertySheet is derived from CWnd instead of CDialog, and there is no OnCancel function.

You can't override OnDestroy because the IDCANCEL command calls the base CDialog::DestroyWindow function, which suppresses any call to the OnDestroy function.

There are two ways to handle the problem. One solution overrides the OnCommand function for the sheet class and intercepts commands sent to the window as a consequence of events related to the IDCANCEL command, which is encoded in the wParam argument to OnCommand. Listing Eight shows this solution.

The other solution is to save the CWnd pointer to the Cancel button that Listing Seven retrieved, and call EnableWindow(FALSE) through the pointer when the program does not want to allow the user to exit, or EnableWindow(TRUE) when it's okay to exit.

Neither of these solutions prevents the user from quitting the program by clicking the X icon in the upper-right corner of the dialog box or by pressing Alt+F4. To do that, you have to intercept the SYS_COMMAND message with the SC_CLOSE identification code. Listing Nine shows that solution.

Each of these examples uses a common dialog box to ask users if they really want to exit. Some applications make that decision by temporarily disabling the Exit button with EnableWindow(FALSE), but that action doesn't disable Alt+F4 or the X icon. Listing Ten shows two functions that cover it all. You would call EnableExit when it's OK for the user to exit and DisableExit when it is not.

These functions enable and disable the Exit button and the System Menu's Close command. Even when an application has no System Menu, the Close command is in effect through the X icon at the right end of the title bar. When you disable the Close command, the X is grayed out.

The m_bExitEnabled data member is a BOOL that deals with some odd behavior in the framework. Even when the Close command is disabled, Alt+F4, (which is the default accelerator key for the Close command) exits the application as if the command were enabled. Therefore, to facilitate automatic suppression of the Close command, you have to add m_bExitEnabled as a data member, set and clear it to reflect the current context of the program with respect to exiting, and change the OnSysCommand function to recognize that setting as Listing Eleven shows.

This application really needs no Exit button. The X icon provides a way to click out of the application and Alt+F4 provides a way to exit with the keyboard. Which means that the application needs no buttons at all. You can get that effect by replacing the call to DoModal in the application class's InitInstance class with a call to Create: m_pCSheet->Create();.

You must also change the return in InitInstance from FALSE to TRUE. All the code that deals with button changes can be eliminated, although it doesn't cause any harm to leave it in.

At this point, you have an application with no buttons. I added a Toggle Exit pushbutton to simulate how an application might decide to suppress the exit operation.

The complete program is available electronically. As usual, I've stripped out all the bulk added by Developer Studio. For some reason, Developer Studio includes an option to suppress the generation of source code comments, but the option is ignored by the code generator. Then there are all those empty message maps, DDX/DDV maps, //{ entries, and so on.

First-Chance Exception?

One final note: 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.

I have no idea what that means. It does not seem to be a problem with the execution of the program outside of the debugger. If anyone recognizes this undocumented (I think) error message, would they please let me know? The online help glossary offers this vague definition:

first-chance notification
The first time the kernel notifies the debugger of an exception.

Now there's a cogent explanation. In times past, I would go online to CompuServe and ask for help to see what I should do. The support on CompuServe has deteriorated, in my experience, since Microsoft pulled out and tried to lure us over to MSN. I find that I get far better tech support by posting questions in this column and having readers respond with e-mail. Maybe it exposes some of my ignorance, but it sure gets results.

Listing One

CSheet::CSheet() : CPropertySheet("Test Property Sheets", 0, 0)
{
}

Listing Two
class CSheet;
class CPropSheetApp : public CWinApp
{

    CSheet* pCSheet;
public:
    CPropSheetApp();
};

Listing Three
BOOL CPropSheetApp::InitInstance()
{
    Enable3dControlsStatic();
    m_pCSheet = new CSheet;
    m_pMainWnd = m_pCSheet;
    m_pCSheet->DoModal();
    return FALSE;
}
int CPropSheetApp::ExitInstance() 
{
    delete m_pCSheet;
    m_pMainWnd = m_pCSheet = 0;
    return CWinApp::ExitInstance();
}
Listing Four
class CSheet;
class CPropSheetApp : public CWinApp
{
    CPage m_CPage;
    CSheet* pCSheet;
public:
    CPropSheetApp();
};

Listing Five
CSheet::CSheet() : CPropertySheet("Test Property Sheets", 0, 0)
{
    AddPage(&m_CPage);
}

Listing Six
BOOL CPage::OnInitDialog() 
{
    CPropertyPage::OnInitDialog();
    if (m_pCSheet == 0) {
        m_pCSheet = (CSheet*)GetParent();
        m_pCSheet->Initialize();
    }
    return TRUE;
}

Listing Seven
void CSheet::Initialize()
{
    CWnd* pWnd = GetDlgItem(IDOK);
    if (pWnd != 0)  {
        pWnd->ShowWindow(SW_HIDE);

        pWnd->EnableWindow(FALSE);
    }
    pWnd = GetDlgItem(IDCANCEL);
    if (pWnd != 0)
        pWnd->SetWindowText("Exit");
}

Listing Eight
BOOL CSheet::OnCommand(WPARAM wParam, LPARAM lParam) 
{
    if (wParam == IDCANCEL)
        if (AfxMessageBox("Quit?", MB_YESNO) == IDNO)
            return TRUE;
    return CPropertySheet::OnCommand(wParam, lParam);
}

Listing Nine
void CSheet::OnSysCommand(UINT nID, LPARAM lParam) 
{
    if (nID == SC_CLOSE)
        if (AfxMessageBox("Quit?", MB_YESNO) == IDNO)
            return;
    CPropertySheet::OnSysCommand(nID, lParam);
}

Listing Ten
void CSheet::EnableExit()
{
    CMenu* pCMenu = GetSystemMenu(FALSE);
    ASSERT(pCMenu != 0);
    pCMenu->EnableMenuItem(SC_CLOSE, MF_ENABLED);
    CWnd* pWnd = GetDlgItem(IDCANCEL);
    if (pWnd != 0)
        pWnd->EnableWindow(TRUE);
    m_bExitEnabled = TRUE;
}
void CSheet::DisableExit()
{
    CMenu* pCMenu = GetSystemMenu(FALSE);
    ASSERT(pCMenu != 0);
    pCMenu->EnableMenuItem(SC_CLOSE, MF_GRAYED);
    CWnd* pWnd = GetDlgItem(IDCANCEL);
    if (pWnd != 0)
        pWnd->EnableWindow(FALSE);
    m_bExitEnabled = FALSE;
}

Listing Eleven
void CSheet::OnSysCommand(UINT nID, LPARAM lParam) 
{
    if (nID != SC_CLOSE || m_bExitEnabled == TRUE)
        CPropertySheet::OnSysCommand(nID, lParam);
}



Related Reading


More Insights






Currently we allow the following HTML tags in comments:

Single tags

These tags can be used alone and don't need an ending tag.

<br> Defines a single line break

<hr> Defines a horizontal line

Matching tags

These require an ending tag - e.g. <i>italic text</i>

<a> Defines an anchor

<b> Defines bold text

<big> Defines big text

<blockquote> Defines a long quotation

<caption> Defines a table caption

<cite> Defines a citation

<code> Defines computer code text

<em> Defines emphasized text

<fieldset> Defines a border around elements in a form

<h1> This is heading 1

<h2> This is heading 2

<h3> This is heading 3

<h4> This is heading 4

<h5> This is heading 5

<h6> This is heading 6

<i> Defines italic text

<p> Defines a paragraph

<pre> Defines preformatted text

<q> Defines a short quotation

<samp> Defines sample computer code text

<small> Defines small text

<span> Defines a section in a document

<s> Defines strikethrough text

<strike> Defines strikethrough text

<strong> Defines strong text

<sub> Defines subscripted text

<sup> Defines superscripted text

<u> Defines underlined text

Dr. Dobb's encourages readers to engage in spirited, healthy debate, including taking us to task. However, Dr. Dobb's moderates all comments posted to our site, and reserves the right to modify or remove any content that it determines to be derogatory, offensive, inflammatory, vulgar, irrelevant/off-topic, racist or obvious marketing or spam. Dr. Dobb's further reserves the right to disable the profile of any commenter participating in said activities.

 
Disqus Tips To upload an avatar photo, first complete your Disqus profile. | View the list of supported HTML tags you can use to style comments. | Please read our commenting policy.