Special Edition Using Visual C++ 6

Previous chapterNext chapterContents


- 12 -

Property Pages and Sheets


Introducing Property Sheets

One of the newest types of graphical objects is the tabbed dialog box, also known as a property sheet. A property sheet is a dialog box with two or more pages. Windows and NT are loaded with property sheets, which organize the many options that users can modify. You flip the pages by clicking labeled tabs at the top of the dialog box. Using such dialog boxes to organize complex groups of options enables users to more easily find the information and settings they need. As you've probably guessed, Visual C++ 6 supports property sheets, with the classes CPropertySheet and CPropertyPage.

Similar to property sheets are wizards, which use buttons instead of tabs to move from one page to another. You've seen a lot of wizards, too. These special types of dialog boxes guide users step by step through complicated processes. For example, when you use AppWizard to generate source code for a new project, the wizard guides you through the entire process. To control the wizard, you click buttons labeled Back, Next, and Finish.

Finding a sample property sheet is as easy as finding sand at the beach. Just click virtually any Properties command or bring up an Options dialog in most applications. For example, Figure 12.1 shows the dialog box that you see when you choose Tools, Options from within Visual C++. This property sheet contains 12 pages in all, each covering a different set of options.

FIG. 12.1 The Options properties sheet contains many tabbed pages.


NOTE: Many people forget the difference between a property sheet and a property page. A property sheet is a window that contains property pages. Property pages are windows that hold controls. They appear on the property sheet. 

As you can see, property sheets are a great way to organize many types of related options. Gone are the days of dialog boxes so jam-packed with options that you needed a college-level course just to figure them out. In the following sections, you'll learn to program your own tabbed property sheets by using MFC's CPropertySheet and CPropertyPage classes.

Creating the Property Sheet Demo Application

Now that you've had an introduction to property sheets, it's time to learn how to build an application that uses these handy specialized dialog boxes. You're about to build the Property Sheet Demo application, which demonstrates the creation and manipulation of property sheets. Follow the steps in the following sections to create the basic application and modify its resources.

Creating the Basic Files

First, use AppWizard to create the basic files for the Property Sheet Demo program, selecting the options listed in the following table. When you're done, the New Project Information dialog box appears; it will look like Figure 12.2. Click OK to create the project files.

Dialog Box Name Options to Select
New, Project tab Name the project Propsheet and then set the project path to the directory in which you want to store the project's files. Make sure that MFC AppWizard (exe) is highlighted. Leave the other options set to their defaults.
Step 1 Select Single Document.
Step 2 of 6 Leave set to defaults.
Step 3 of 6 Leave set to defaults.
Step 4 of 6 Turn off all application features.
Step 5 of 6 Leave set to defaults.
Step 6 of 6 Leave set to defaults.

FIG. 12.2 Your New Project Information dialog box looks like this.

Editing the Resources

Now you'll edit the resources in the application generated for you by AppWizard, removing unwanted menus and accelerators, editing the About box, and most importantly, adding a menu item that will bring up a property sheet. Follow these steps:

1. Select the ResourceView tab in the project workspace window. Developer Studio displays the ResourceView window (see Figure 12.3).

FIG. 12.3 The ResourceView tab displays the ResourceView window.

2. In the ResourceView window, click the plus sign next to Propsheet Resources to display the application's resources. Click the plus sign next to Menu and then double-click the IDR_MAINFRAME menu ID. Visual C++'s menu editor appears, displaying the IDR_MAINFRAME menu generated by AppWizard.

3. Click the Property Sheet Demo application's Edit menu (not Visual C++'s Edit menu) and then press Delete to delete the Edit menu. A dialog box asks for verification of the Delete command; click OK.

4. Double-click the About Propsheet... item in the Help menu to bring up its properties dialog box. Change the caption to &About Property Sheet Demo. Pin the properties dialog box in place by clicking the pushpin in the upper-left corner.

5. On the application's File menu, delete all menu items except Exit.

6. Select the blank menu item at the end of the File menu, and change the caption to &Property Sheet... and the command ID to ID_PROPSHEET (see Figure 12.4). Then use your mouse to drag the new command above the Exit command so that it's the first command in the File menu.

FIG. 12.4 Add a Property Sheet command to the File menu.

7. Click the + next to Accelerator in the ResourceView window and highlight the IDR_MAINFRAME accelerator ID. Press Delete to delete all accelerators from the application.

8. Click the + next to Dialog in the ResourceView window. Double-click the IDD_ABOUTBOX dialog box ID to bring up the dialog box editor.

9. Modify the dialog box by clicking the title so that the properties box refers to the whole dialog box. Change the caption to About Property Sheet Demo.

10. Click the first static text string and change the caption to Property Sheet Demo, Version 1.0. Click the second and add Que Books to the end of the copyright string.

11. Add a third static string with the text Special Edition Using Visual C++ 6 so that your About box resembles the one in Figure 12.5. Close the dialog box editor.

12. Click the + next to String Table in the ResourceView window. Double-click the String Table ID to bring up the string table editor.

13. Double-click the IDR_MAINFRAME string and then change the first segment of the string to Property Sheet Demo (see Figure 12.6). The meanings of these strings are discussed in Chapter 15, "Building an ActiveX Server Application," in the "Shortcomings of This Server" section. The one you just changed is the Window Title, used in the title bar of the application.

FIG. 12.5 The About box looks like this.

FIG. 12.6 The first segment of the IDR_MAINFRAME string appears in your main window's title bar.

Adding New Resources

Now that you have the application's basic resources the way you want them, it's time to add the resources that define the application's property sheet. This means creating dialog box resources for each page in the property sheet. Follow these steps:

1. Click the New Dialog button on the Resource toolbar, or press Ctrl+1, to create a new dialog box resource. The new dialog box, IDD_DIALOG1, appears in the dialog box editor. This dialog box, when set up properly, will represent the first page of the property sheet.

2. Delete the OK and Cancel buttons by selecting each with your mouse and then pressing Delete.

3. If the Properties box isn't still up, bring it up by choosing View, Properties. Change the ID of the dialog box to IDD_PAGE1DLG and the caption to Page 1 (see Figure 12.7).

FIG. 12.7 Change the caption and resource ID of the new dialog box.

4. Click the Styles tab of the dialog box's property sheet. In the Style drop-down box select Child, and in the Border drop-down box select Thin. Turn off the System Menu check box. Your properties dialog box will resemble Figure 12.8.

The Child style is necessary because the property page will be a child window of the property sheet. The property sheet itself will provide the container for the property pages.

FIG. 12.8 A property page uses styles different from those used in regular dialog boxes.

5. Add an edit box to the property page, as shown in Figure 12.9. In most applications you would change the resource ID from IDC_EDIT1, but for this demonstration application, leave it unchanged.

6. Create a second property page by following steps 1 through 5 again. For this property page, use the ID IDD_PAGE2DLG, a caption of Page 2, and add a check box rather than an edit control (see Figure 12.10).

FIG. 12.9 A property page can hold whatever controls you like.

FIG. 12.10 The second property page looks like this.

Associating Your Resources with Classes

You now have all your resources created. Next, associate your two new property-page resources with C++ classes so that you can control them in your program. You also need a class for your property sheet, which will hold the property pages that you've created. Follow these steps to create the new classes:

1. Make sure that the Page 1 property page is visible in the dialog box edit area and then double-click it. If you prefer, choose View, ClassWizard from the menu bar. The MFC ClassWizard property sheet appears, displaying the Adding a Class dialog box first discussed in Chapter 2, "Dialogs and Controls."

2. Select the Create New Class option and then click OK. The New Class dialog box appears.

3. In the Name box, type CPage1. In the Base Class box, select CPropertyPage. (Don't accidentally select CPropertySheet.) Then click OK to create the class.

You've now associated the property page with an object of the CPropertyPage class, which means that you can use the object to manipulate the property page as needed. The CPropertyPage class will be especially important when you learn about wizards.

4. Select the Member Variables tab of the MFC ClassWizard property sheet. With IDC_EDIT1 highlighted, click the Add Variable button. The Add Member Variable dialog box appears.

5. Name the new member variable m_edit, as shown in Figure 12.11, and then click OK. ClassWizard adds the member variable, which will hold the value of the property page's control, to the new CPage1 class.

FIG 12.11 ClassWizard makes it easy to connect controls on a dialog box to member variables of the class representing the dialog box.

6. Click OK on the MFC ClassWizard properties sheet to finalize the creation of the CPage1 class.

7. Follow steps 1 through 6 for the second property sheet. Name the class CPage2 and add a Boolean member variable called m_check for the IDC_CHECK1 control, as shown in Figure 12.12.

FIG. 12.12 The second property page needs a Boolean member variable called m_checkbox.

Creating a Property Sheet Class

At this point, you've done all the resource editing and don't need to have so many windows open. Choose Window, Close All from the menu bar and close the properties box. You'll now create a property sheet class that displays the property pages already created. Follow these steps:

1. Bring up ClassWizard and click the Add Class button. A tiny menu appears below the button; choose New. The New Class dialog box appears.

2. In the Name box, type CPropSheet, select CPropertySheet in the Base Class box, and then click OK.

3. ClassWizard creates the CPropSheet class. Click the MFC ClassWizard Properties sheet's OK button to finalize the class.

Mow you have three new classes--CPage1, CPage2, and CPropSheet--in your program. The first two classes are derived from MFC's CPropertyPage class, and the third is derived from CPropertySheet. Although ClassWizard has created the basic source-code files for these new classes, you still have to add code to the classes to make them work the way you want. Follow these steps to complete the Property Sheet Demo application:

1. Click the ClassView tab to display the ClassView window. Expand the Propsheet classes, as shown Figure 12.13.

2. Double-click CPropSheet to open the header file for your property sheet class. Because the name of this class (CPropSheet) is so close to the name of the application as a whole (PropSheet), you'll find CPropSheet in PropSheet1.h, generated by ClassWizard when you created the new class.

3. Add the following lines near the middle of the file, right before the CPropSheet class declaration:

#include "page1.h"
#include "page2.h"


These lines give the CPropSheet class access to the CPage1 and CPage2 classes so that the property sheet can declare member variables of these property page classes.

FIG. 12.13 The ClassView window lists the classes that make up your project.

4. Add the following lines to the CPropSheet class's //Attributes section, right after the public keyword:

CPage1 m_page1;
CPage2 m_page2;


These lines declare the class's data members, which are the property pages that will be displayed in the property sheet.

5. Expand the CPropSheet class in the ClassView pane, and double-click the first constructor, CPropSheet. Add these lines to it:

AddPage(&m_page1);
AddPage(&m_page2);


This will add the two property pages to the property sheet whenever the sheet is constructed.

6. The second constructor is right below the first; add the same lines there.

7. Double-click CPropsheetView in ClassView to edit the header file, and add the following lines to the //Attributes section, right after the line CPropsheetDoc* GetDocument();:

protected:
    CString m_edit;
    BOOL m_check;


These lines declare two data members of the view class to hold the selections made in the property sheet by users.

8. Add the following lines to the CPropsheetView constructor:

m_edit = "Default";
m_check = FALSE;


These lines initialize the class's data members so that when the property sheet appears, these default values can be copied into the property sheet's controls. After users change the contents of the property sheet, these data members will always hold the last values from the property sheet, so those values can be restored to the sheet when needed.

9. Edit CPropsheetView::OnDraw() so that it resembles Listing 12.1. The new code displays the current selections from the property sheet. At the start of the program, the default values are displayed.

Listing 12.1  CPropsheetView::OnDraw()

void CPropsheetView::OnDraw(CDC* pDC)
{
     CPropsheetDoc* pDoc = GetDocument();
     ASSERT_VALID(pDoc);
    pDC->TextOut(20, 20, m_edit);
    if (m_check)
        pDC->TextOut(20, 50, "TRUE");
    else
        pDC->TextOut(20, 50, "FALSE");
}
10. At the top of PropsheetView.cpp, after the #include of propsheet.h, add another include statement:

#include "propsheet1.h"
11. Bring up ClassWizard, click the Message Maps tab, and make sure that CPropsheetView is selected in the Class Name box. In the Object IDs box, select ID_PROPSHEET, which is the ID of the new item you added to the File menu. In the Messages box, select COMMAND. Click Add Function to add a function that will handle the command message generated when users choose this menu item. Name the function OnPropsheet(), as shown in Figure 12.14.

FIG. 12.14 Use ClassWizard to add the OnPropsheet() member function.

The OnPropsheet() function is now associated with the Property Sheet command that you previously added to the File menu. That is, when users select the Property Sheet command, MFC calls OnPropsheet(), where you can respond to the command.

12. Click the Edit Code button to jump to the OnPropsheet() function, and add the lines shown in Listing 12.2.

Listing 12.2  CPropsheetView::OnPropsheet()

void CPropsheetView::OnPropsheet() 
{
    CPropSheet propSheet("Property Sheet", this, 0);
    propSheet.m_page1.m_edit = m_edit;
    propSheet.m_page2.m_checkbox = m_check;
    int result = propSheet.DoModal();
    if (result == IDOK)
    {
        m_edit = propSheet.m_page1.m_edit;
        m_check = propSheet.m_page2.m_checkbox;
        Invalidate();
    }
     
}
The code segment in Listing 12.2, discussed in more detail later in this chapter, creates an instance of the CPropSheet class and sets the member variables of each of its pages. It displays the sheet by using the familiar DoModal function first discussed in Chapter 2, "Dialogs and Controls." If users click OK, it updates the view member variables to reflect the changes made on each page and forces a redraw with a call to Invalidate().

Running the Property Sheet Demo Application

You've finished the complete application. Click the Build button on the Build minibar (or choose Build, Build) to compile and link the application. Run it by choosing Build, Execute or by clicking the Execute button on the Build minibar. When you do, you see the window shown in Figure 12.15.

As you can see, the window displays two values--the default values for the controls in the application's property sheet. You can change these values by using the property sheet. Choose File, Property Sheet; the property sheet appears (see Figure 12.16). The property sheet contains two pages, each of which holds a single control. When you change the settings of these controls and click the property sheet's OK button, the application's window displays the new values. Try it!

FIG. 12.15 When it first starts, the Property Sheet Demo application displays default values for the property sheet's controls.

FIG. 12.16 The application's property sheet contains two pages.

Adding Property Sheets to Your Applications

To add a property sheet to one of your own applications, you follow steps very similar to those you followed in the previous section to create the demo application:

1. Create a dialog box resource for each page in the property sheet. These resources should have the Child and Thin styles and should have no system menu.

2. Associate each property page resource with an object of the CPropertyPage class. You can do this easily with ClassWizard. Connect controls on the property page to members of the class you create.

3. Create a class for the property sheet, deriving the class from MFC's CPropertySheet class. You can generate this class by using ClassWizard.

4. In the property sheet class, add member variables for each page you'll be adding to the property sheet. These member variables must be instances of the property page classes that you created in step 2.

5. In the property sheet's constructor, call AddPage() for each page in the property sheet.

6. To display the property sheet, call the property sheet's constructor and then call the property sheet's DoModal() member function, just as you would with a dialog box.

After you write your application and define the resources and classes that represent the property sheet (or sheets--you can have more than one), you need a way to enable users to display the property sheet when it's needed. In Property Sheet Demo, this is done by associating a menu item with a message-response function. However you handle the command to display the property sheet, the process of creating the property sheet is the same. First, you must call the property sheet class's constructor, which Property Sheet Demo does like this:

CPropSheet propSheet("Property Sheet", this, 0);

Here, the program creates an instance of the CPropSheet class. This instance (or object) is called propSheet. The three arguments are the property sheet's title string, a pointer to the parent window (which, in this case, is the view window), and the zero-based index of the first page to display. Because the property pages are created in the property sheet's constructor, creating the property sheet also creates the property pages.

After you create the property sheet object, you can initialize the data members that hold the values of the property page's controls, which Property Sheet Demo does like this:

propSheet.m_page1.m_edit = m_edit;
propSheet.m_page2.m_checkbox = m_check;

Now it's time to display the property sheet, which you do just as though it were a dialog box, by calling the property sheet's DoModal() member function:

int result = propSheet.DoModal();

DoModal() doesn't take any arguments, but it does return a value indicating which button users clicked to exit the property sheet. In a property sheet or dialog box, you'll usually want to process the information entered into the controls only if users clicked OK, which is indicated by a return value of IDOK. If users exit the property sheet by clicking the Cancel button, the changes are ignored and the view or document member variables aren't updated.

Changing Property Sheets to Wizards

Here's a piece of information that surprises most people: A wizard is just a special property sheet. Instead of tabbed pages on each sheet that allow users to fill in the information in any order or to skip certain pages entirely, a wizard has Back, Next, and Finish buttons to move users through a process in a certain order. This forced sequence makes wizards terrific for guiding your application's users through the steps needed to complete a complex task. You've already seen how AppWizard in Visual C++ makes it easy to start a new project. You can create your own wizards suited to whatever application you want to build. In the following sections, you'll see how easy it is to convert a property sheet to a wizard.

Running the Wizard Demo Application

To understand Wizards, this section will show you the Wizard Demo application, which is built in much the same way as the Property Sheet Demo application that you created earlier in this chapter. This chapter won't present step-by-step instructions to build Wizard Demo. You will be able to build it yourself if you want, using the general steps presented earlier and the code snippets shown here.

When you run the Wizard Demo application, the main window appears, looking very much like the Property Sheet Demo main window. The File menu now includes a Wizard item; choosing File Wizard brings up the wizard shown in Figure 12.17.

FIG. 12.17 The Wizard Demo application displays a wizard rather than a property sheet.

The wizard isn't too fancy, but it does demonstrate what you need to know to program more complex wizards. As you can see, this wizard has three pages. On the first page is an edit control and three buttons: Back, Next, and Cancel. The Back button is disabled because there's no previous page to go back to. The Cancel button enables users to dismiss the wizard at any time, canceling whatever process the wizard was guiding users through. The Next button causes the next page in the wizard to appear.

You can change whatever is displayed in the edit control if you like. However, the magic really starts when you click the Next button, which displays Page 2 of the wizard (see Figure 12.18). Page 2 contains a check box and the Back, Next, and Cancel buttons. Now the Back button is enabled, so you can return to Page 1 if you want to. Go ahead and click the Back button. The wizard tells you that the check box must be checked (see Figure 12.19). As you'll soon see, this feature of a wizard enables you to verify the contents of a specific page before allowing users to advance to another step.

FIG. 12.18 In Page 2 of the wizard, the Back button is enabled.

After checking the check box, you can click the Back button to move back to Page 1 or click Next to advance to Page 3. Assuming that you advance to Page 3, you see the display shown in Figure 12.20. Here, the Next button has changed to the Finish button because you are on the wizard's last page. If you click the Finish button, the wizard disappears.

FIG. 12.19 You must select the check box before the wizard will let you leave Page 2.

FIG. 12.20 This is the last page of the Wizard Demo Application's wizard.

Creating Wizard Pages

As far as your application's resources go, you create wizard pages exactly as you create property sheet pages--by creating dialog boxes and changing the dialog box styles. The dialog titles--Page 1 of 3, Page 2 of 3, and Page 3 of 3--are hardcoded onto each dialog box. You associate each dialog box resource with an object of the CPropertyPage class. Then, to take control of the pages in your wizard and keep track of what users are doing with the wizard, you override the OnSetActive(), OnWizardBack(), OnWizardNext(), and OnWizardFinish() functions of your property page classes. Read on to see how to do this.

Displaying a Wizard

The File, Wizard command is caught by CWizView's OnFileWizard() function. It's very similar to the OnPropSheet() function in the Property Sheet demo, as you can see from Listing 12.3. The first difference is the call to SetWizardMode() before the call to DoModal(). This function call tells MFC that it should display the property sheet as a wizard rather than as a conventional property sheet. The only other difference is that users arrange for property sheet changes to be accepted by clicking Finish, not OK, so this code checks for ID_WIZFINISH rather than IDOK as a return from DoModal().

Listing 12.3  CWizView::OnFileWizard()

void CWizView::OnFileWizard() 
{
     CWizSheet wizSheet("Sample Wizard", this, 0);
     wizSheet.m_page1.m_edit = m_edit;
     wizSheet.m_page2.m_check = m_check;
     wizSheet.SetWizardMode();
     int result = wizSheet.DoModal();
     if (result == ID_WIZFINISH)
     {
          m_edit = wizSheet.m_page1.m_edit;
          m_check = wizSheet.m_page2.m_check;
          Invalidate();
     }
}

Setting the Wizard's Buttons

MFC automatically calls the OnSetActive() member function immediately upon displaying a specific page of the wizard. So, when the program displays Page 1 of the wizard, the CPage1 class's OnSetActive() function is called. You add code to this function that makes the wizard behave as you want. CPage1::OnSetActive() looks like Listing 12.4.

Listing 12.4  CPage1::OnSetActive()

BOOL CPage1::OnSetActive() 
{
     CPropertySheet* parent = (CPropertySheet*)GetParent();
     parent->SetWizardButtons(PSWIZB_NEXT);
     return CPropertyPage::OnSetActive();
}

OnSetActive() first gets a pointer to the wizard's property sheet window, which is the page's parent window. Then the program calls the wizard's SetWizardButtons() function, which determines the state of the wizard's buttons. SetWizardButtons() takes a single argument, which is a set of flags indicating how the page should display its buttons. These flags are PSWIZB_BACK, PSWIZB_NEXT, PSWIZB_FINISH, and PSWIZB_DISABLEDFINISH. Because the call to SetWizardButtons() in Listing 12.4 includes only the PSWIZB_NEXT flag, only the Next button in the page will be enabled.

Because the CPage2 class represents Page 2 of the wizard, its call to SetWizardButtons() enables the Back and Next buttons by combining the appropriate flags with the bitwise OR operator (|), like this:

parent->SetWizardButtons(PSWIZB_BACK | PSWIZB_NEXT);

Because Page 3 of the wizard is the last page, the CPage3 class calls SetWizardButtons() like this:

parent->SetWizardButtons(PSWIZB_BACK | PSWIZB_FINISH);

This set of flags enables the Back button and provides a Finish button instead of a Next button.

Responding to the Wizard's Buttons

In the simplest case, MFC takes care of everything that needs to be done in order to flip from one wizard page to the next. That is, when users click a button, MFC springs into action and performs the Back, Next, Finish, or Cancel command. However, you'll often want to perform some action of your own when users click a button. For example, you may want to verify that the information that users entered into the currently displayed page is correct. If there's a problem with the data, you can force users to fix it before moving on.

To respond to the wizard's buttons, you override the OnWizardBack(), OnWizardNext(), and OnWizardFinish() member functions. Use the Message Maps tab of ClassWizard to do this; you'll find the names of these functions in the Messages window when a property page class is selected in the Class Name box. When users click a wizard button, MFC calls the matching function which does whatever is needed to process that page. An example is the way the wizard in the Wizard Demo application won't let you leave Page 2 until you've checked the check box. This is accomplished by overriding the functions shown in Listing 12.5.

Listing 12.5  Responding to Wizard Buttons

LRESULT CPage2::OnWizardBack() 
{
     CButton *checkBox = (CButton*)GetDlgItem(IDC_CHECK1);
     if (!checkBox->GetCheck())
     {
          MessageBox("You must check the box.");
          return -1;
     }
     return CPropertyPage::OnWizardBack();
}
LRESULT CPage2::OnWizardNext() 
{
     UpdateData();
     if (!m_check)
     {
          MessageBox("You must check the box.");
          return -1;
     }
     return CPropertyPage::OnWizardNext();
}

These functions demonstrate two ways to examine the check box on Page 2. OnWizardBack() gets a pointer to the page's check box by calling the GetDlgItem() function. With the pointer in hand, the program can call the check box class's GetCheck() function, which returns a 1 if the check box is checked. OnWizardNext() calls UpdateData() to fill all the CPage2 member variables with values from the dialog box controls and then looks at m_check. In both functions, if the box isn't checked, the program displays a message box and returns -1 from the function. Returning -1 tells MFC to ignore the button click and not change pages. As you can see, it is simple to arrange for different conditions to leave the page in the Back or Next direction.


Previous chapterNext chapterContents

© Copyright, Macmillan Computer Publishing. All rights reserved.