Converting Large Dialogs into Property Sheets

 

Nancy Winnick Cluts
Microsoft Corporation

January 1995
Updated February 2004

Abstract

Does your application have dialog boxes that will not fit onto a 640 x 480 display? When your dialog boxes are displayed, do people run screaming from the room? Do you get headaches just looking at some of your dialog boxes because they are so large and confusing? Well, maybe you should think about converting those awe-inspiring dialog boxes into property sheets (also commonly known as tabbed dialog boxes). In this article, I take one such large dialog box and convert it into a property sheet using the built-in CPropertyPage and CPropertySheet classes of Microsoft® Foundation Class Library (MFC) 3.0 in Microsoft Visual C++™ 2.0. By the time you finish reading this article, you should be able to convert your dialog boxes with little effort.

Who Designed That?

If you are like most developers, you know quite a bit about software development, bits, bytes, optimization, and debugging. You may not, however, be the most expert when it comes to designing usable dialog boxes. You may also have run into some problems in your applications where you ended up with either a very large dialog box or a set of endlessly tunneling modal dialog boxes. These dialog boxes may have been the best that you could do without redesigning your application.

Well, it's no longer necessary for you to have to design these complex dialog boxes. The Microsoft® Foundation Class Library (MFC) 3.0 includes classes for creating and manipulating tabbed dialog boxes (also commonly known as property sheets). Property sheets are easier to use than tunneling dialog boxes and are used extensively in Microsoft's newest operating system, Windows® 95. If you want to add this functionality to your application now, you do not have to wait for the release of Windows 95—you can convert your dialog boxes to property sheets today using Microsoft Visual C++™ 2.0 and MFC 3.0.

Okay. Take a deep breath and then look at this dialog box for entering satellite tracking data.

ms997553.satprops_1(en-us,MSDN.10).gif

Figure 1. The Satellite Data dialog box

This is the tragic result of allowing developers to design dialog boxes without the aid of usability experts. I think that you will probably agree that the above dialog box is a bit, um, cumbersome. Not to mention that if you are running 640 x 480 screen resolution, you need to scroll to see all of it.

And Now for Something Completely Different

I am going to give you a sneak peek at the property sheet I created to replace the dialog box. As you can see in Figure 2, this property sheet is far easier on the eyes and much less confusing. If cleaning up your dialog boxes appeals to you, read on and follow the steps that I outline below. You will find that you, too, can have cool property sheets and look like an awesome programming stud.

ms997553.satprops_2(en-us,MSDN.10).gif

Figure 2. The General page of the Satellite Properties property sheet

Converting the Dialog Box

Converting the dialog box to a property sheet consisted of the following steps:

  1. Group the information in the dialog box based upon how the user will use the property sheet.
  2. Create each property page using the resource editor.
  3. Use ClassWizard to create a derived class for each page.
  4. Create and initialize your property page objects and create the property sheet itself.

Step One: Group the Information

To convert the existing dialog box into a series of pages for a property sheet, I first needed to take a look at the dialog box and group the settings based on how the user will need the information. When I was satisfied with the groupings, I was able to proceed to the next step.

Step Two: Create Each Property Page

Use the resource editor to create a dialog box for each property page. The first page you add to the property sheet determines how much space to allocate for the other property pages in the property sheet. Make sure it is as large as the largest page that you need. I first opened the original dialog box, deleted the OK and Cancel buttons, and selected the controls that I wanted for a page. Then I copied and pasted them into the dialog box. The identifiers for the controls in the original dialog box are the same as the identifiers for each control in the tabbed dialog box. And pasting them meant a lot less work for me later because I didn't have to keep track of each ID.

Set the dialog box styles to standard property page styles. In the Styles page, use the Style list box to set the style of the dialog box to Child, set the Border list box to Thin, and check the Disabled and Titlebar check boxes. Then switch to the General page, and enter the text for that tab into the Caption edit box. For example, I named a tab "Elements," as you'll see in Figure 6 later in this article.

ms997553.satprops_3(en-us,MSDN.10).gif

Figure 3. Dialog Properties property sheet

Step Three: Put ClassWizard to Work for You

I then used ClassWizard to create a CPropertyPage-derived class for each page. To create the class, select the dialog box resource, choose ClassWizard from the Project menu, and then choose CPropertyPage as the base class in ClassWizard. By default, ClassWizard will create a .CPP file and an .H file for each of these new classes. As a result, you will have two files for each new class. Include header files at the beginning of the file to make the call to create the property page. If you included a special header file for the original dialog box, remove it.

ms997553.satprops_4(en-us,MSDN.10).gif

Figure 4. Creating a class using ClassWizard

When I created these classes, I also created the member variables to hold the values for each control in the property page. In the original dialog box, I used member variables in each field. To make things a bit easier for conversion, use the exact same names for the member variables for each page.

ms997553.satprops_5(en-us,MSDN.10).gif

Figure 5. Creating member variables using ClassWizard

Step Four: Make Your Declarations and Go!

I then declared my property pages and my CPropertySheet object. The constructor for the CPropertySheet object can take three parameters: a caption to be displayed in the caption bar of the property sheet window, a pointer to the parent window, and the index of the page to be shown initially. The pointers to the parent window and the index are optional parameters. If you do not pass them to the constructor, the parent window will be the main window of the application, and the first page selected will be the first page added to the property sheet.

Add the pages to the property sheet via a call to ::AddPage. Then use ::DoModal to display and run the property sheet. This is all done in the following substeps:

  1. Remove the directive to include the previous dialog box header file. For example, I had to remove SATDLG.H.

  2. Add directives to include the header files for each new class for each page of the property sheet: GENERAL.H, ELEMENTS.H, MODES.H, TRACK.H, and NOTES.H.

    Substitute dialog initialization code with code to initialize each page:

    // Show the editing dialog.
    int CSatellite::DoEditDialog()
    {
       // Declare a property sheet.
       CPropertySheet dlgPropSheet("Satellite Properties");
    
       // Declare the pages.
       General genPage;
       Elements eltPage;
       Track trackPage;
       Modes modesPage;
       Notes notesPage;
    
       // Initialize the General page.
        genPage.m_strName = m_strName;
        genPage.m_dwCatalogNum = m_dwCatalogNum;
        genPage.m_iEpochYear = m_iEpochYear;
        genPage.m_iLaunchYear = m_iLaunchYear;
        genPage.m_dEpochDay = m_dEpochDay;
        genPage.m_iEpochRev = m_iEpochRev;
        genPage.m_iElementSet = m_iElementSet;
    
       // Initialize the Elements page.
        eltPage.m_dDecayRate = m_dDecayRate;
        eltPage.m_dInclination = m_dInclination;
        eltPage.m_dRightAsAsNode = m_dRightAsAsNode;
        eltPage.m_dEccentricity = m_dEccentricity;
        eltPage.m_dArgOfPerigee = m_dArgOfPerigee;
        eltPage.m_dMeanAnomaly = m_dMeanAnomaly;
        eltPage.m_dMeanMotion = m_dMeanMotion;
    
       // Intialize the Track page.
        trackPage.m_dBahnLat = m_dBahnLat;
        trackPage.m_dBahnLong = m_dBahnLong;
        trackPage.m_dBeaconFreq1 = m_dBeaconFreq1;
        trackPage.m_dBeaconFreq2 = m_dBeaconFreq2;
        trackPage.m_bTrack = m_bTrack;
    
       // Initialize the Notes page.
        notesPage.m_strNotes = m_strNotes;
    
       // Initialize the Modes page.
       int i;
        for (i=0; i<MAXMODES; i++) {
            if (m_Mode[i].m_iStartPhase != m_Mode[i].m_iEndPhase) {
                char buf[256];
                sprintf(buf, 
                        "% 3d % 3d  ",
                        m_Mode[i].m_iStartPhase,
                        m_Mode[i].m_iEndPhase);
                modesPage.m_strModes += buf;
                modesPage.m_strModes += m_Mode[i].m_strDescription;
                modesPage.m_strModes += "\r\n";
            }
        }
    
       // Add each page.
       dlgPropSheet.AddPage(&genPage);
       dlgPropSheet.AddPage(&eltPage);
       dlgPropSheet.AddPage(&trackPage);
       dlgPropSheet.AddPage(&modesPage);
       dlgPropSheet.AddPage(&notesPage);
    
       // Invoke the property sheet.
       int iRet;
       if ((iRet = dlgPropSheet.DoModal()) != IDOK)
          return iRet;
    
       // Update the satellite data.
        m_strName = genPage.m_strName;
        m_dwCatalogNum = genPage.m_dwCatalogNum;
        m_iEpochYear = genPage.m_iEpochYear;
        m_iLaunchYear = genPage.m_iLaunchYear;
        m_dEpochDay = genPage.m_dEpochDay;
        m_iEpochRev = genPage.m_iEpochRev;
        m_iElementSet = genPage.m_iElementSet;
        m_dDecayRate = eltPage.m_dDecayRate;
        m_dInclination = eltPage.m_dInclination;
        m_dRightAsAsNode = eltPage.m_dRightAsAsNode;
        m_dEccentricity = eltPage.m_dEccentricity;
        m_dArgOfPerigee = eltPage.m_dArgOfPerigee;
        m_dMeanAnomaly = eltPage.m_dMeanAnomaly;
        m_dMeanMotion = eltPage.m_dMeanMotion;
        m_dBahnLat = trackPage.m_dBahnLat;
        m_dBahnLong = trackPage.m_dBahnLong;
        m_dBeaconFreq1 = trackPage.m_dBeaconFreq1;
        m_dBeaconFreq2 = trackPage.m_dBeaconFreq2;
        m_bTrack = trackPage.m_bTrack;
        m_strNotes = notesPage.m_strNotes;
    
        // Reset the mode info.
        for (i=0; i<MAXMODES; i++) {
            m_Mode[i].m_iStartPhase = 0;
            m_Mode[i].m_iEndPhase = 0;
            m_Mode[i].m_strDescription = "";
        }
        // Extract the info from the mode string.
        i=0;
        const char *p = modesPage.m_strModes;
        while (i<MAXMODES) {
            int iS, iE;
            char buf[256];
            if (!p || (*p == '\0')) break;
            // Skip any white space
            while (*p && !isgraph(*p)) p++;
            // Get the start value
            iS = atoi(p);
            // Move on
            while (*p && isdigit(*p)) p++;
            while (*p && !isgraph(*p)) p++;
            // Get end value
            iE = atoi(p);
            while (*p && isdigit(*p)) p++;
            while (*p && !isgraph(*p)) p++;
            // Get comment
            char* pb = buf;
            while (*p && (*p != '\n') && (*p != '\r')) {
                if (pb < buf[255]) *pb++ = *p;
                p++;
            }
            *pb = '\0';
            if (iE != 0) {
                m_Mode[i].m_iStartPhase = iS;
                m_Mode[i].m_iEndPhase = iE;
                m_Mode[i].m_strDescription = buf;
            }
    
            if (*p == '\0') break;
            // Advance to start of the next line
            while (*p && ((*p == '\r') || (*p == '\n'))) p++;
            if (*p == '\0') break;
            i++;
        }
        ComputeFixedData();      
        ComputePosition(Today());
        return iRet;
    }
    

What You Get

After doing all this work, you are probably wondering what the new and improved property sheet looks like. Bear in mind that usability experts didn't design this. If your company has a usability department, I encourage you to get its feedback about the design of the property pages.

Following are the rest of the pages in the property sheet I created based upon groupings set up by someone who knows something about satellites (certainly not me!). One comment that I received from a usability expert is that the property sheet design should be organized around the way that the intended user will use the information. Preferably, all the information the user needs to finish a specific task should reside on one tab. The headings that I chose included a tab entitled "General." This heading is abstract and should be changed to something a bit more descriptive. A better heading might be "Identification." The point is that the heading should be descriptive enough so that the user of this dialog box (who does know something about satellites) would immediately be able to tab to the correct page by reading the headings.

ms997553.satprops_6(en-us,MSDN.10).gif

Figure 6. The Elements page of the Satellite Properties property sheet

ms997553.satprops_7(en-us,MSDN.10).gif

Figure 7. The Tracking page of the Satellite Properties property sheet

ms997553.satprops_8(en-us,MSDN.10).gif

Figure 8. The Modes page of the Satellite Properties property sheet

ms997553.satprops_9(en-us,MSDN.10).gif

Figure 9. The Notes page of the Satellite Properties property sheet

Great Job, MFC Designers!

This whole bit of conversion only took me a couple of hours. To some of you that may seem like a lot of time, but I didn't even write the original code. Had I originally written the code, the amount of time required to convert this dialog box would easily have been under half an hour.

The people who designed the CPropertySheet class obviously went to a great deal of effort to make it relatively painless to convert your dialog boxes into property pages. For one thing, the methods for gathering input that's entered into property sheets are the same as for dialog boxes. Also, if you have written code for dialog boxes before, you'll find that the code to create and use property sheets is nearly identical.

Summary

Converting large, unruly dialog boxes into small and elegant property sheets is painless and easy if you use the CPropertyPage and CPropertySheet built-in classes of MFC 3.0 and if you use the ClassWizard that is supplied with Visual C++ 2.0. You will not have to learn any new methods for handling property sheets rather than using dialog boxes. The MFC designers have made it as easy as possible to use property sheets rather than dialog boxes.