When Windows and other graphically oriented user interfaces came along, programmers needed to find a new way to retrieve information from the computer user-a communication problem for the modern age. In the days of DOS, the program could simply print a prompt on-screen and direct the user to enter whatever value the program needed. With Windows, however, getting data from the user is not so simple. Because a window's client area (the area in which an application can display information) should be reserved for other purposes, a well-designed Windows application gets most user input through dialog boxes.
MFC includes several types of dialog boxes. In this chapter, you learn to use MFC to display and handle your own custom dialog boxes, which you design with Developer Studio's dialog-box editor.
As mentioned in this chapter's introduction, dialog boxes are one way a Windows application can get information from the user. Chances are that your Windows application will have several dialog boxes, each designed to retrieve a different type of information from your user. However, before you can add these dialog boxes to your program, you must create them. To make this job simpler, Developer Studio includes an excellent resource editor.
Because dialog boxes are used so extensively in Windows applications, MFC provides several classes that you can use to make dialog-box manipulation easier and more convenient. Although you can use MFC's CDialog class directly, you'll most often derive your own dialog-box class from CDialog in order to have more control over how your dialog box operates. However, MFC also provides classes for more specific types of dialog boxes, including CColorDialog, CFontDialog, and CFileDialog.
The minimum steps for adding a dialog box to your MFC application are as follows:
Although the preceding steps are all you need to display a simple dialog box, you usually need much more control over your dialog box than these steps allow. For example, the preceding method of displaying a dialog box provides no way to retrieve data from the dialog box, which means the method is really only useful for dialog boxes that display information to the user, sort of like a glorified message box. To create a dialog box that you can control, follow this second set of steps:
In the rest of this chapter, you see how to perform all of the steps listed here, including how to write your dialog-box class and how to provide this class with automatic data transfer and validation. In the next section, you learn to use Developer Studio's dialog-box editor.
NOTE |
When you create a dialog-box class using ClassWizard, the wizard generates most of the code you need to fully control your dialog box. The following sections describe not only how to add dialog boxes to your applications without using ClassWizard, but they also describe how the code generated by ClassWizard works, should you decide to use ClassWizard. When you've finished with this chapter, you'll have a solid understanding of the dialog-box source code that AppWizard and ClassWizard generate. |
As you now know, the first step toward adding a dialog box to your MFC application is creating the dialog-box resource, which acts as a sort of template for Windows. When Windows sees the dialog-box resource in your program, it uses the commands in the resource to construct the dialog box for you. To learn how to create a dialog-box resource, just follow these steps:
Figure 7.2 : The dialog box here shows controls placed on the dialog-box form.
Because dialog boxes are often unique to an application (with the exception of the common dialog boxes), you almost always create your own IDs for both the dialog box and the controls it contains. You can, if you like, accept the default IDs that the dialog-box editor creates for you. However, these IDs are generic (for example, IDD_DIALOG1, IDC_EDIT1, IDC_RADIO1, and so on), and so you may probably want to change them to something more specific. In any case, as you can tell from the default IDs, a dialog box's ID usually begins with the prefix IDD and control IDs usually begin with the prefix IDC_. You can, of course, use your own prefixes if you like, although sticking with conventions often makes your programs easier to read.
After creating a resource, Developer Studio creates or modifies at least two files that you need to add to your application. The first file is called RESOURCE.H and contains the resource IDs that you've defined. You must include RESOURCE.H in any file that refers to these IDs.
The second file Developer Studio creates or modifies has the .RC extension and is the resource script that defines all your application's resources. A resource script is like a source-code file written in a language that the resource compiler understands. The resource compiler takes the .RC file and compiles it into a .RES file, which is the binary representation of your application's resources.
When you use Developer Studio's resource editors to create your resources, you don't usually need to modify the resultant resource script directly. However, there may be times when such direct editing is convenient, so it can't hurt to learn more about how resource scripts work. Check in your favorite Windows programming manual for more information on resource scripts.
This chapter's first sample program, called Dialog, demonstrates
two ways to create and display dialog boxes in your Windows applications.
You can find the program, along with all its source code, in the
CHAP07\DIALOG folder of this book's CD-ROM (or on your hard drive,
if you installed the contents of the CD-ROM). To run the program,
double-click the DIALOG.EXE file. When you do, you see the window
shown in Figure 7.3.
The data displayed in the window are the default values for information that can be collected by one of the program's dialog boxes. You'll take a look at that dialog box in a second, but first select the Help, About command to display the application's About dialog box. As you can see, the About dialog box only displays information to the user and does not enable the user to enter data. For this reason, the About dialog box can be created and displayed very easily in an MFC program.
To display the application's second, and much more complex, dialog
box, select the Dialog, Test command. When you do this, a dialog
box containing a number of controls appears. You can use these
controls to enter information about an imaginary employee (see
fig. 7.4). When you finish entering information, close the dialog
box by clicking OK. The Dialog application's main window then
displays the new information you entered into the dialog box's
controls.
Figure 7.4 : The Test command reveals a dialog box containing several types of controls.
This second dialog box is also capable of validating its data. For example, the Name text box does not let you enter more than 30 characters. In addition, the Age text box rejects any value that doesn't fall between 16 and 100. If you enter an invalid value into the Age text box, you see a warning and are not allowed to exit the dialog box with the OK button until the value is corrected. Notice also that, if you enter new data into the dialog box and then select the Cancel button, your changes are thrown away and do not appear in the application's window.
Now that you've had a chance to use the Dialog application, it's time to see how it works. You can find the complete listings for the Dialog application in the CHAP07 folder of this book's CD-ROM. The most pertinent source-code files for the following discussion are MAINFRM.H, MAINFRM.CPP, DLG1.H, and DLG1.CPP.
Before you get too deep into learning how to create sophisticated dialog boxes, it might be nice to see how easy it can be to display simple dialog boxes that require no complex interaction with the user. A good example is the Dialog application's About dialog box, which does nothing more than display program information to the user. The hardest part of getting the About dialog box up on the screen is creating the dialog-box template with the resource editor-and that task is just a matter of positioning a few static-text controls. If you look at the CMainFrame class's (the frame window class's) OnHelpAbout() function, you see that displaying the dialog box requires only two lines of code, like this:
CDialog dlg(IDD_ABOUTDIALOG, this); dlg.DoModal();
The first line creates a CDialog object that's associated with the About dialog-box template that was defined in the resource editor. The constructor's two arguments are the dialog box's resource ID and a pointer to the dialog box's parent window, which is the CMainFrame window. Once the dialog-box object has been constructed and associated with the template, a call to the dialog-box object's DoModal() member function displays the dialog box on the screen.
The DoModal() function handles all the user's interactions with the dialog box, which, in this case, amounts to little more than waiting for the user to click the OK button. When the user exits the dialog box, the DoModal() function returns and your program can continue. Because the dialog-box object is created locally on the stack, it is automatically deleted when it goes out of scope, which is when the OnHelpAbout() function exits. You can also create the dialog-box dynamically on the heap (that is, in the computer's unused memory), by using the new operator. In that case, you'd have to delete the object yourself. The following lines illustrate this technique:
CDialog* dlg = new CDialog(IDD_ABOUTDIALOG, this); dlg->DoModal(); delete dlg;
NOTE |
Remember that, when you need to access an object's members through a pointer to the object, you must use the "->" operator. On the other hand, when you want to access an object's members through a reference to the object, you must use the "." operator. This is exactly the same way you would handle a structure. In fact, a class is really nothing more than a fancy structure. |
NOTE |
When creating your dialog box's template with the resource editor, make sure that you turn on the dialog box's Visible attribute, which is found in the Dialog's property sheet on the More Styles page. If you fail to set this style attribute, your dialog box will not appear on the screen properly, making the application seem to lock up. |
Displaying a simple About dialog box is all well and good. Unfortunately, most dialog boxes are much more complex, requiring that your application be able to transfer information back and forth between the program and the dialog box. Often, you may want to initialize the dialog box's controls with default values before displaying the dialog box. After the user enters information into the dialog box, you then need to extract that information so that it can be used in your program. Accomplishing this means not only creating a dialog-box template with the resource editor, but also associating that template with your own custom dialog-box class derived from MFC's CDialog.
The main dialog box in the Dialog application has 10 controls that the user can manipulate. These controls are the Name text box, the Age text box, the Department combo box, the five check boxes for the days of the week, and the OK and Cancel buttons. You don't have to worry about the OK and Cancel buttons, because MFC handles them for you. When you write your dialog box's class, however, you do need to provide data members for storing the information the user enters into the other controls. In the Dialog application, the CDlg1 class, which is derived from CDialog, provides the needed data members as shown in Listing 7.1.
Listing 7.1 LST7_1.TXT-Declaring Data Members for the Dialog Box's Controls
public: CString m_department; BOOL m_monday; BOOL m_tuesday; BOOL m_wednesday; BOOL m_thursday; BOOL m_friday; CString m_name; UINT m_age;
Notice that these variables are declared as public data members of the class. This is important because if they were declared as private or protected, your program would not be able to access them outside of the class. (Yes, this is another case where MFC breaks the rules of strict object-oriented design, which dictates that a class's data member should never be accessed from outside the class.)
NOTE |
The variables you declare for your dialog box's controls must, of course, hold the appropriate type of data for the controls with which they're associated. For example, text boxes that return strings must be associated with string variables, whereas controls that return integers must be associated with integer variables. The following list shows the data types that go with each type of control: |
Edit box - Usually a string, but can also be other data types including int, float, and
Check box - int
Radio button - int
List box - string
Combo box - string
Scroll bar - int
.
Besides the data members, your dialog-box class must supply a constructor and must override the virtual DoDataExchange() function, which is where the data transfer occurs. The CDlg1 class's constructor is declared like this:
public: CDlg1(CWnd* pParent);
The constructor's single argument is a pointer to the dialog box's parent window. As you'll soon see, the constructor's implementation passes this pointer, as well as the dialog-box template's resource ID, on to the CDialog class's constructor.
The CDlg1 class overrides the DoDataExchange() function like this:
protected: virtual void DoDataExchange(CDataExchange* pDX);
As you can see, DoDataExchange()'s only argument is a pointer to a CDataExchange object, which is responsible for handling the actual transfer of the data between the dialog box's controls and the class's data members.
With the class's header file ready to go, you can start writing the class's implementation. As always, the first step is to include the header files required to compile the code, as shown in Listing 7.2.
Listing 7.2 LST7_2.TXT-Including the Appropriate Header Files
#include <afxwin.h> #include "dialog.h" #include "resource.h" #include "dlg1.h"
The first line of Listing 7.2 includes the general header file for MFC programs. The second line brings in the Dialog class's declaration, the third is your resource IDs, and the fourth is the CDlg1 class's declaration.
The CDlg1 class's constructor is responsible for initializing the dialog-box object, as shown in Listing 7.3.
Listing 7.3 LST7_3.TXT-Constructing a CDlg1 Object
CDlg1::CDlg1(CWnd* pParent) : CDialog(IDD_TESTDIALOG, pParent) { // Initialize data transfer variables. m_department = "Sales"; m_monday = TRUE; m_tuesday = TRUE; m_wednesday = TRUE; m_thursday = TRUE; m_friday = TRUE; m_name = ""; m_age = 16; }
The CDlg1 constructor receives a single parameter, which is a pointer to the dialog box's parent window. The constructor passes this pointer, along with the dialog box's resource ID, to the base class's constructor. It is the resource ID that links the correct dialog-box template to the dialog-box class. Because you've built the ID into the class's constructor, you don't need to worry about it when you create the dialog-box object. You need only supply the pointer to the parent window.
Inside the constructor, the program initializes the data members that represent the contents of the dialog box's controls. The values stored in these variables are automatically copied to the dialog box's controls when the dialog box is displayed. When the user dismisses the dialog box, MFC copies the data from the controls into these variables, where they can be accessed by other parts of your program.
The only other function in the dialog-box class is the overridden DoDataExchange() function, which is shown in Listing 7.4.
Listing 7.4 LST7_4.TXT-Overriding the DoDataExchange() Function
void CDlg1::DoDataExchange(CDataExchange* pDX) { // Call the base class's version. CDialog::DoDataExchange(pDX); // Associate the data transfer variables with // the ID's of the controls. DDX_Text(pDX, IDC_NAME, m_name); DDV_MaxChars(pDX, m_name, 30); DDX_Text(pDX, IDC_AGE, m_age); DDV_MinMaxUInt(pDX, m_age, 16, 100); DDX_CBString(pDX, IDC_DEPARTMENT, m_department); DDX_Check(pDX, IDC_MONDAY, m_monday); DDX_Check(pDX, IDC_TUESDAY, m_tuesday); DDX_Check(pDX, IDC_WEDNESDAY, m_wednesday); DDX_Check(pDX, IDC_THURSDAY, m_thursday); DDX_Check(pDX, IDC_FRIDAY, m_friday); }
It is the DoDataExchange() function that sets up MFC's capability to transfer data between the dialog-box class's data members and the dialog box's controls. The function receives a single parameter, which is a pointer to a CDataExchange object. This pointer is supplied by MFC when it calls your overridden version of the function. You don't have to do anything with this pointer except pass it on to the base class's DoDataExchange() function, as well as to various DDX and DDV functions you call to set up the dialog box's data transfer. The first line of your DoDataExchange() function should call the base class's version, passing along the pointer to the CDataExchange object.
What are DDX and DDV functions? An excellent question! MFC's DDX functions set up the link between a data member and a control. For example, in Listing 7.4, the call to DDX_Text() links the edit control whose ID is IDC_NAME to the data member m_name. This tells MFC to copy the contents of m_name to the control when the dialog box is displayed and to copy the data in the control back to m_name when the dialog box is dismissed. Notice that a DDX function's first argument is the CDataExchange pointer passed into the DoDataExchange() function. There is a DDX function for each type of control you want to include in a data transfer. These functions are listed in Table 7.1. To learn about each function's parameters, look them up in your Visual C++ online documentation.
Function | Description |
DDX_CBIndex() | Links a combo box's index to an integer variable |
DDX_CBString() | Links a combo box's string to a string variable |
DDX_CBStringExact() | Links a combo box's selected string to a string variable |
DDX_Check() | Links a check box with an integer variable |
DDX_LBIndex() | Links a list box's index with an integer variable |
DDX_LBString() | Links a list box's string to a string variable |
DDX_LBStringExact() | Links a list box's selected string to a string variable |
DDX_Radio() | Links a radio button with an integer variable |
DDX_Scroll() | Links a scroll bar to an integer variable |
DDX_Text() | Links a text box to a string variable |
MFC's DDV functions perform data validation for controls. For example, in Listing 7.4, the call to DDV_MaxChars() tells MFC that a valid entry in the edit control linked to m_name should be no longer than 30 characters. When the text-box control reaches 30 characters, it accepts no more input from the user. On the other hand, the call to DDV_MinMaxUInt() tells MFC that the m_age variable, which is associated with the edit control whose ID is IDC_AGE, should not accept values outside the range of 16 to 100. If the user enters an invalid number in the IDC_AGE edit box, MFC displays a message, warning the user of his mistake. The user cannot exit via the dialog box's OK button until the value is corrected. Table 7.2 shows the DDV functions you can call. For more information on their parameters, please consult your Visual C++ online documentation.
Function | Description |
DDV_MaxChars() | Limits the length of a string |
DDV_MinMaxByte() | Limits a byte value to a specific range |
DDV_MinMaxDouble() | Limits a double value to a specific range |
DDV_MinMaxDWord() | Limits a DWORD value to a specific range |
DDV_MinMaxFloat() | Limits a floating-point value to a specific range |
DDV_MinMaxInt() | Limits an integer value to a specific range |
DDV_MinMaxLong() | Limits a long-integer value to a specific range |
DDV_MinMaxUInt() | Limits an unsigned-integer value to a specific range |
NOTE |
It's important that you call DDV functions immediately after the DDX function that sets up the data exchange for a control. When you do this, MFC can set the input focus to the control that contains invalid data, not only showing the user exactly where the problem is, but also enabling him to enter a new value as conveniently as possible. |
Now that you have your dialog-box class written, you can create objects of that class within your program and display the associated dialog-box element. The first step in using your new class is to include the dialog-box class's header file in any class that will access the class. Failure to do this causes the compiler to complain that it doesn't recognize the dialog-box class. Also, when you develop the window class that will display the dialog box, you usually want to create data members that mirror the data members of the dialog-box class. This gives you a place to store information that you transfer from the dialog box. In the Dialog application, the CMainFrame class declares (in its header file) this set of member variables as shown in Listing 7.5.
Listing 7.5 LST7_5.TXT-Declaring Storage for Dialog-Box Data
protected: CString m_name; UINT m_age; CString m_department; BOOL m_monday; BOOL m_tuesday; BOOL m_wednesday; BOOL m_thursday; BOOL m_friday;
Notice that the data members declared in Listing 7.5 have the same names as the equivalent data members in the CDlg1 class. This convention makes it easier to keep track of what the variables do. Notice also that, unlike the equivalent variables in the CDlg1 class, the CMainFrame class's variables are declared as protected, rather than as public. This is because no other class requires access to these variables in the same way that other classes require access to the dialog-box class's variables.
Just as the dialog-box class initializes its data members in its constructor, so too does the CMainFrame class, as shown in Listing 7.6.
Listing 7.6 LST7_6.TXT-Initializing the Frame-Window's Data Members
m_name = ""; m_age = 16; m_department = "Sales"; m_monday = TRUE; m_tuesday = TRUE; m_wednesday = TRUE; m_thursday = TRUE; m_friday = TRUE;
The constructor initializes these data members to the same values as their counterparts in the CDlg1 class. This is because, as you'll soon see, the contents of these variables will be copied into the dialog-box variables before the dialog box is displayed. Then why bother to initialize the dialog-box class's variables? To be sure that an object of the class is always created in a fully initialized state. After all, there's no guarantee that someone who uses the class will copy values into the dialog-box class before displaying the dialog box.
In the CMainFrame class, the program displays the dialog box in the OnDialogTest() function, which MFC calls when the user selects the Dialog, Test command from the application's menu bar. The OnDialogTest() function first creates a dialog-object of your new class, like this:
CDlg1 dlg(this);
With the dialog-box object created, the program can now access the dialog box's data members, and so initialize them to the current contents of the values stored in the CMainFrame class, as shown in Listing 7.7.
Listing 7.7 LST7_7.TXT-Transferring Data to the Dialog-Box Object
dlg.m_name = m_name; dlg.m_age = m_age; dlg.m_department = m_department; dlg.m_monday = m_monday; dlg.m_tuesday = m_tuesday; dlg.m_wednesday = m_wednesday; dlg.m_thursday = m_thursday; dlg.m_friday = m_friday;
You may wonder why the mainframe window class bothers to initialize the dialog box's data members before displaying the dialog box. In this program, the dialog box always appears with the last entered data in the dialog box. Because the dialog box's data disappears along with the dialog box when the dialog box is deleted (either by the object going out of scope or by being explicitly deleted with the delete operator). The only place the current dialog-box data is saved is in the CMainFrame class. If you want the dialog box to always appear with its original default values, don't bother to copy the stored values into the dialog box's data members, and instead stick with the values supplied by the dialog box's own constructor.
After the program initializes the dialog box's contents, the program calls the dialog-box object's DoModal() function to display the dialog box to the user, like this:
int result = dlg.DoModal();
At this point, the user has control until he dismisses the dialog box. If the user exits by pressing the OK button, DoModal() returns the value IDOK (the Cancel button causes DoModal() to return IDCANCEL). In the case of IDOK, the program must copy the new data from the dialog box to the CMainFrame class's variables, as shown in Listing 7.8.
Listing 7.8 LST7_8.TXT-Retrieving Data from the Dialog Box
if (result == IDOK) { // Copy the dialog's data into this class's data members. m_name = dlg.m_name; m_age = dlg.m_age; m_department = dlg.m_department; m_monday = dlg.m_monday; m_tuesday = dlg.m_tuesday; m_wednesday = dlg.m_wednesday; m_thursday = dlg.m_thursday; m_friday = dlg.m_friday; // Force the window to show the new data. Invalidate(); }
The call to Invalidate() in Listing 7.8 forces the window's contents to be redrawn, this time using the new values retrieved from the dialog box. When the OnDialogTest() function ends, the dialog-box object goes out of scope and disappears, along with the data the user entered into it. Good thing you saved that data in the CMainFrame class!
Dialog boxes are the most common object used to get information from the user in a Windows application. Of course, dialog boxes aren't much good without the many controls that can be placed in them. Edit controls, list boxes, combo boxes, radio buttons, check boxes, and other types of controls all work together to provide the application's user with convenient ways to enter data into a program. Without these controls, a dialog box is about as useful as a telephone without number buttons. In this section, you learn to program window controls in dialog boxes. You also learn new ways to extract information from a window's controls.
Until now, the programs in this chapter have featured dialog boxes containing basic controls such as edit controls and buttons. Edit controls and buttons are probably the most important types of controls at your disposal. However, often you can give your program's user easier methods of selecting the data that must be entered into the program.
There are several types of controls you can place in a dialog box, including check boxes, radio buttons, list boxes, combo boxes, and scroll bars. You should already be familiar with how these controls work, both from a user's and a programmer's point of view. If you've never programmed with Microsoft Foundation Classes, you may not be familiar with the classes with which MFC encapsulates each of the window controls. These classes include CButton, CEdit, CStatic, CListBox, and CComboBox.
Although MFC supplies classes that encapsulate the many window controls, Windows provides most of the services needed to handle these controls. A description of each window control follows:
This chapter's second sample program is called Control1. You can
find the source code and executable file for this program in the
CHAP07\CONTROL1 folder on this book's CD-ROM. When you run Control1,
you see the window shown in Figure 7.5. This window shows the
data that the user has currently selected from the application's
dialog box. At the start of the program, this data is set to default
values. The EDIT CONTROL field of the display shows the current
string copied from the dialog box's edit control. The RADIO BUTTON
field shows which radio button in the dialog box is selected.
The CHECK BOX fields show the status of each of the three check
boxes. The LIST BOX field shows the string currently selected
in the list box, and the COMBO BOX field shows the currently selected
string in the combo box.
To find out where all these controls are, select the Dialog, Test
command to bring up the Control Dialog dialog box shown in Figure
7.6. This dialog box contains the controls whose data is shown
in the main window. You can change the controls' setting any way
you want. When you exit the Control Dialog dialog box via the
OK button, the main window displays the controls' new settings.
Exiting the dialog box by clicking the Cancel button has no effect
on the main window's display, because any changes made to the
data in the dialog box are ignored.
Figure 7.6 : The Control1 application's main dialog box.
As you experiment with the Control1 application, it may seem that the program doesn't do a heck of a lot more than the dialog-box sample you created in the previous chapter. The truth is, it's not so much what the Control1 application does, but rather how it does it. Specifically, the controls in the application's dialog box are associated with MFC control classes so that the program can directly manipulate the controls. In this section, you see how this bit of MFC trickery is accomplished.
In the CHAP07\CONTROL1 folder of this book's CD-ROM, you find the complete source code for the Control1 application. The most pertinent source-code files are MAINFRM.H, MAINFRM.CPP, CNTLDLG.H, and CNTLDLG.CPP.
In a previous chapter, you may recall that I mentioned two ways for a window class to get access to another class's data members. The first is to declare the class's data members as public. While this solution is easy to accomplish, it's not as elegant as it can be, because it grants access not only to the window class that must access the other class's variables, but also to any other class that might try to gain such access. I also mentioned that you can make the data members private and provide public member functions for manipulating those data members.
Another good way to limit uncontrolled access is to make the window class that needs to read information from another class a friend of the class. A class declared as a friend of another class has access to that class's public, protected, and even private data members. However, the access is limited to the friend class. No other class can access the protected and private data members.
The Control1 application uses the friend access method to share the dialog-box class's variables with the main window class. If you look at the top of the CCntlDlg class's header file (CNTLDLG.H), you see this line:
class CMainFrame;
This line tells the compiler that the identifier CMainFrame represents a class. Near the end of the CCntlDlg class's header file, you see why the compiler needs this information. The line
friend CMainFrame;
tells the compiler to make the CMainFrame class a friend of the CCntlDlg class, giving CMainFrame access to all of CCntlDlg's data members. This is all the code it takes-just two lines-to limit outside access of CCntlDlg's variables to the CMainFrame class.
As you know, MFC features many classes for window controls. Up until now, however, you haven't used these classes with the controls you added to your dialog boxes. This is because, in many cases, you don't need to manipulate a control at that level. You just display your dialog box and let the controls take care of themselves. There are times, though, when it's handy to be able to manipulate a control directly, which is what the many control classes, such as CEdit, CButton, and CListBox, enable you to do.
Each of the control classes feature member functions that do everything from initialize the contents of the control to respond to Windows messages. But to use these member functions, you must first associate the control with the appropriate class. For example, to manipulate an edit box through MFC, you must first associate that control with the CEdit class. Then you can use the CEdit class's member functions to manage the control.
To associate a control with a class, you must first get a pointer to the control. You can do this easily by calling the GetDlgItem() member function, which a dialog-box class inherits from CWnd. You call GetDlgItem() like this:
CEdit* pEditControl = (CEdit*)GetDlgItem(IDC_EDIT1);
The GetDlgItem() function returns a pointer to a CWnd object. (Controls are, after all, special types of windows. Every control class has CWnd as a base class.) Its single argument is the resource ID of the control for which you want the pointer. To gain access to the member functions of a control class, the returned CWnd pointer must be cast to the appropriate type of pointer. In the preceding line, you can see that GetDlgItem()'s return value is being cast to a CEdit pointer.
When you have the pointer, you can access the class's member functions through that pointer. For example, to set the contents of the edit control for which the previous code line got a pointer, you use a line like this:
pEditControl->SetWindowText("Text for the edit control");
In the Control1 application, the program uses the techniques described in the previous section to create a simple data-transfer mechanism for the dialog box, without calling upon MFC's DDX functions. The dialog-box class first declares, in its header file, data members for holding the information the user entered into the dialog box, as shown in Listing 7.9.
Listing 7.9 LST7_8.TXT-Declaring Data Members for the Dialog-Box Class
protected: CString m_edit1; int m_radio1; int m_radio2; int m_radio3; int m_check1; int m_check2; int m_check3; CString m_list1; CString m_combo1;
As you can see, there is one variable for each dialog-box control with which the user can interact. Notice also that these data members are declared as protected. However, in spite of their protected status, the CMainFrame class can access them, because CMainFrame is a friend class of CCntlDlg.
If you recall, the Control1 application's dialog box appears with default values already selected in its controls. For example, the edit box appears with the text "Default," and the first radio button in the radio-button group is selected. Obviously, these controls are being initialized somewhere in the program-and that somewhere is the CCntlDlg class's OnInitDialog() function. The OnInitDialog() function gets called as part of the dialog-box creation process (specifically, it responds to the WM_INITDIALOG Windows message). Because OnInitDialog() is a virtual function of the CDialog class, however, you don't need to create an entry in a message map. Just override the function in your custom dialog-box class.
In your overridden OnInitDialog(), you must first call the base class's version, like this:
CDialog::OnInitDialog();
You call the base class's version of OnInitDialog() so that the base class can perform its default initialization. Then, you can perform whatever special initialization is required by your dialog-box class. In the CCntlDlg class, that initialization is setting the various controls to their default values. First, the program sets the edit box, like this:
CEdit* pEdit1 = (CEdit*)GetDlgItem(IDC_EDIT1); pEdit1->SetWindowText("Default");
Next, the program sets the radio-button group to its default state, which is the first button selected, like this:
CButton* pRadio1 = (CButton*)GetDlgItem(IDC_RADIO1); pRadio1->SetCheck(TRUE);
The SetCheck() member function of the CButton class determines the check state of a button. Although the preceding example uses TRUE as the function's argument, there are actually three possible settings: 0 turns off the checkmark, 1 turns on the checkmark, and 2 sets the button control as to its "indeterminate" state. (An example of an indeterminate state might be when you highlight text that contains both underlined and normal text. Then a control that displays the state of the underlined attribute cannot be on or off, but rather must be indeterminate.) However, you can use 2 only when you've given the button the BS_3STATE or BS_AUTO3STATE style.
After setting the radio buttons, the program performs similar initialization on the first check button, like this:
CButton* pCheck1 = (CButton*)GetDlgItem(IDC_CHECK1); pCheck1->SetCheck(TRUE);
Setting the checked state of buttons is pretty easy. Initializing a list box, however, takes a little extra work, as shown in Listing 7.10.
Listing 7.10 LST7_10.TXT-Initializing a List Box
CListBox* pList1 = (CListBox*)GetDlgItem(IDC_LIST1); pList1->AddString("ListString1"); pList1->AddString("ListString2"); pList1->AddString("ListString3"); pList1->AddString("ListString4"); pList1->AddString("ListString5"); pList1->AddString("ListString6"); pList1->SetCurSel(0);
The AddString() member function of the CListBox class adds a string to the contents of the list box. So, the code in Listing 7.10 adds the six selections from which the user can choose from the list box. The SetCurSel() function, also a member of the CListBox class, determines which of the entries on the list box are selected. The function's single argument is the zero-based index of the item to select. So, an index of 0 selects the first item in the list. An index of 1 initializes the list box without a selection.
A combo box is not unlike a list box when it comes to initialization, as you can see in Listing 7.11.
Listing 7.11 LST7_11.TXT-Initializing a Combo Box
CComboBox* pCombo1 = (CComboBox*)GetDlgItem(IDC_COMBO1); pCombo1->AddString("ComboString1"); pCombo1->AddString("ComboString2"); pCombo1->AddString("ComboString3"); pCombo1->AddString("ComboString4"); pCombo1->AddString("ComboString5"); pCombo1->AddString("ComboString6"); pCombo1->SetCurSel(0);
The lines in Listing 7.11 work exactly like the similar lines used to initialize the list box, adding six strings to the combo box's list, and then making the first string the default selection in the list. That is, the first string is already entered into the combo box's edit box when the dialog box appears.
NOTE |
If you want to create a dialog-box class that retains the most recently entered data, use OnInitDialog() to copy the contents of the appropriate class data members to the controls rather than using "hard-coded" data as is done in the Control1 application. Using this method, you enable the Windows class that creates the dialog-box object to initialize the dialog box's controls, by copying data into the dialog-box class's data members before displaying the dialog box. |
After the dialog box appears on the screen, the user can enter whatever data he likes into the dialog box's controls. The Control1 application doesn't regain control until the user exits the dialog box, by clicking either the Cancel or OK buttons. As with most dialog boxes, if the user clicks the Cancel button (indicated by a return value of IDCANCEL from DoModal()), the program simply ignores any changes that were made in the dialog box. However, if the user exits the dialog box via the OK button, the program must transfer the dialog box's data from the controls to the dialog-box class's data members. If the dialog box is allowed to close before the controls' contents are copied, the data disappears along with the dialog box.
So, to copy data from the dialog box's controls to the class's data members, a program must first know when the user has clicked the OK button. This is done by overriding the dialog-box class's OnOK() member function. In OnOK(), the program associates controls with the appropriate control classes, and then uses the class member functions to perform whatever tasks are required to process the dialog box's data before the dialog box is deleted.
NOTE |
If your dialog-box class needs to know, before the dialog box is removed from the screen, when the user has clicked the Cancel button, you can override the OnCancel() member function. After processing the Cancel button as required for your application, you'll usually want to call CDialog::OnCancel() to continue the cancel process. |
In the Control1 application, the OnOK() function first extracts the contents of the edit box, like this:
CEdit* pEdit1 = (CEdit*)GetDlgItem(IDC_EDIT1); pEdit1->GetWindowText(m_edit1);
The first line gets a pointer to the edit control and casts it to a CEdit pointer. The program can then use the pointer to call the CEdit member function GetWindowText(), whose single argument is a string object into which the edit box's contents should be copied.
The program copies the contents of the radio buttons similarly, as shown in Listing 7.12.
Listing 7.12 LST7_12.TXT-Storing the Status of the Radio Buttons
CButton* pRadio = (CButton*)GetDlgItem(IDC_RADIO1); m_radio1 = pRadio->GetCheck(); pRadio = (CButton*)GetDlgItem(IDC_RADIO2); m_radio2 = pRadio->GetCheck(); pRadio = (CButton*)GetDlgItem(IDC_RADIO3); m_radio3 = pRadio->GetCheck();
Here, after getting a pointer to each control and casting it to the CButton class, the program calls the CButton member function GetCheck() to obtain the status of each of the radio buttons. GetCheck() returns a 0, 1, or 2, depending on whether the control is in an unchecked, checked, or indeterminate state, respectively.
The program gets the contents of the check boxes in almost exactly the same way, as shown in Listing 7.13.
Listing 7.13 LST7_13.TXT-Storing the Status of the Check Boxes
CButton* pCheck = (CButton*)GetDlgItem(IDC_CHECK1); m_check1 = pCheck->GetCheck(); pCheck = (CButton*)GetDlgItem(IDC_CHECK2); m_check2 = pCheck->GetCheck(); pCheck = (CButton*)GetDlgItem(IDC_CHECK3); m_check3 = pCheck->GetCheck();
As you can see, the CButton class handles both radio buttons and check boxes. The only difference between Listing 7.12 and 7.13 is the resource IDs used to obtain pointers to the buttons.
The following lines show how the Control1 application extracts data from the list-box control:
CListBox* pList1 = (CListBox*)GetDlgItem(IDC_LIST1); int listIndex = pList1->GetCurSel(); pList1->GetText(listIndex, m_list1);
The first line obtains a pointer to the list-box control and associates it with the CListBox class. With the CListBox pointer in hand, the program can call the GetCurSel() member function, which returns the zero-based index of the selected item in the list box. Finally, a call to the CListBox member function GetText() copies the selected text string into the CCntlDlg class's m_list1 data member. GetText()'s two arguments are the index of the text item to get and a reference to a CString object into which to store the string. (The second argument can also be a pointer to a char array.)
As you may have guessed, the program can copy the contents of the combo box in almost exactly the same manner, like this:
CComboBox* pCombo1 = (CComboBox*)GetDlgItem(IDC_COMBO1); int comboIndex = pCombo1->GetCurSel(); pCombo1->GetLBText(comboIndex, m_combo1);
As always, the first line gets a pointer to the control, only this time the pointer is cast to a CComboBox pointer. The second line gets the zero-based index of the selected item in the combo box, whereas the third line copies the selected text line to the CCntlDlg class's m_combo1 data member. The CComboBox class's GetLBText() member function works just like the CListBox class's GetText(), the two arguments being the index of the text item to get and a reference to a CString object into which to store the string. (Again, the second argument can also be a pointer to a char array.)
At this point in the program, all the important data has been copied from the dialog box's controls into the dialog-box class's data members, which means it's now safe to remove the dialog box from the screen. Removing the dialog box is as simple as calling the base class's OnOK() member function, like this:
CDialog::OnOK();
You can also use the OnOK() member function to perform data validation. To do this, after extracting the contents of a control, determine whether the control's data is valid. If the data is not valid, display a message box to the user describing the problem, and then return from OnOK() without calling CDialog::OnOK(). Not calling CDialog::OnOK() leaves the dialog box on the screen so the user can correct the bad entry.
As you've seen, you can create your own data transfer and validation mechanisms by directly manipulating controls in a dialog box. Those DDX and DDV functions don't seem so mysterious now, do they? (You can, of course, do much more with your dialog box's controls than copy and validate data. In fact, after you have a pointer to a control, you can call any of the control's member functions.) The final step is to display your dialog box from within your main program, usually from a window class.
The Control1 application displays the dialog box in response to its Dialog, Test command, which is handled by the OnDialogTest() message-response function. In that function, the program first creates the dialog-box object and then calls DoModal() to display it, like this:
CCntlDlg dialog(this); int result = dialog.DoModal();
If the user exits the dialog box by clicking the OK button, the function copies the dialog box's data into data members of the window class, and then calls Invalidate() in order to repaint the window, which displays the current data from the dialog box. The code that handles these tasks is shown in Listing 7.14.
Listing 7.14 LST7_14.TXT-Copying the Dialog Box's Data
if (result == IDOK) { // Save the contents of the dialog box. m_edit1 = dialog.m_edit1; m_radio1 = dialog.m_radio1; m_radio2 = dialog.m_radio2; m_radio3 = dialog.m_radio3; m_combo1 = dialog.m_combo1; m_check1 = dialog.m_check1; m_check2 = dialog.m_check2; m_check3 = dialog.m_check3; m_list1 = dialog.m_list1; // Force the window to repaint. Invalidate(); }
The dialog-box object is automatically deleted when it goes out of scope, taking all of its data with it, which is why you must copy whatever data you need from the dialog box.
As you've learned, handling dialog boxes in an MFC program is much easier than handling them in a conventional Windows program. This is because MFC provides a powerful class, CDialog, from which you can derive custom dialog-box classes. These custom classes not only perform automatic data exchange, but also validate the contents of a dialog box's controls before the user is allowed to dismiss the dialog box.
In an MFC program, controls can be much more than just passive objects over which your program has no control. By associating a control with its MFC class, you can use the member functions of the class to handle the control any way you like. The most obvious use of the control classes is to create your own custom data transfer and validation functions. However, the creative programmer will find many ways to take advantage of the MFC control classes. If you'd like more information on these topics, check out the following chapters: