Dialogs and Controls
For most Windows applications, dialogs will provide the user interface for many of your application's functionalities. Dialogs can be as simple as a single text message displayed to the user with AfxMessageBox(), or more complicated, like a file open dialog. You can even use a dialog to form the main window of your application. In this chapter, you will see how to
Use your own dialogs
Use controls in your dialog
Use the Windows common dialogs
Create property sheets, wizards, and dialog bars
In Chapter 1, 'Visual C++ Environment,' you learned about creating dialog templates in the resource editor. You will look at this more closely when 626e45g you explore the controls that you can add to your dialog; for now, let's look at how to work with dialogs in your code.
Dialogs in MFC begin with a CDialog object. The CDialog class derives from the CWnd class, which gives it the functionality of a window, and the CCmdTarget class, which allows the dialog to have its own message map for handling messages. In most cases, you will want to derive a class from CDialog rather than using CDialog itself. Deriving your own class for each dialog allows you to use MFC to work with your controls and exchange data between your dialog and your application much easier than you can using only the Win32 API.
First, let's look at a simple dialog class. You may have noticed that AppWizard will create a class for your About box—nearly the simplest dialog. True, creating a class for the About box, as it is implemented, is a bit of overkill. In fact, if you simply created the template for the dialog, the only code needed to present it could go something like this:
CDialog myAboutBox(IDD_ABOUTBOX);This example brings us to the first thing you do to implement your dialog—create an object derived from CDialog. In this example, creating the CDialog object also associates the dialog template that you (or AppWizard) created in the resource editor to an instance of your CDialog class.
The first way to assign a dialog template to your class, as shown in the previous example, is to pass the resource ID of the dialog template to the constructor of CDialog. Because you will generally be deriving your own classes, this can also be done in the implementation of your class's constructor:
CAboutDlg::CAboutDlg() : CDialog(IDD_ABOUTBOX)or, as AppWizard does it:
CAboutDlg::CAboutDlg() : CDialog(CAboutDlg::IDD)The second example does exactly the same thing, but AppWizard has defined an enum type that will translate CAboutDlg::IDD to IDD_ABOUTBOX.
At some point in your application, you will want to present the dialog to the user. Before you do this, decide whether you want your dialog to be modal or modeless. Creating a modal dialog causes your application to wait until the user is finished with the dialog, whereas a modeless dialog is an independent window that can hang around for as long as it likes while your application does its thing.
Presenting a modal dialog is simple; you call the DoModal() function of your dialog object:
int nDlgReturn = myDialog.DoModal();This presents the dialog to users and waits until they are finished with it. For most dialogs, the user will finish with the dialog by clicking on the OK or Cancel button, which brings us to the return value of DoModal().
If your dialog uses the default OK or Cancel button, DoModal() will return either IDOK or IDCANCEL. In general, you accept any changes made in a dialog box only when DoModal() returns IDOK. Users expect that they can always press Cancel to back out of a dialog without changing anything. Of course, the implementation of your dialog may return any value you choose to call EndDialog() with—but more on this later.
If you want to leave your dialog active while your application is doing other things, you create a modeless dialog. This is often useful for things such as application settings that the user may be changing often, or for the Find and Replace dialog you will see later. However, modeless dialogs present a few unique challenges to you as a developer.
Most importantly, your application cannot wait for the user to dismiss a modeless dialog before dealing with the data. This means that you have to implement mechanisms to update your application's data from within the dialog while it is active. You may also need to work out how to update your dialog when certain information changes elsewhere in your application.
You need to make certain that the CDialog object is around throughout the life of the dialog window. To do this, you may want to consider making your dialog objects members of your main frame or application class. Alternatively, you may choose to create the pointers as members and create the dialog objects with the new operator.
Modeless dialogs are also created differently from modal dialogs. First, you call the CDialog constructor with no parameters, because you will attach a dialog template to the dialog object when you create the actual window. This should look something like this:
CDialog m_ModelessDlg;The call to Create() tells the dialog to use the dialog template with the ID IDD_MYDIALOG. The second parameter gives the parent window for the dialog. In this case, you specify NULL, which creates the dialog as a child of the main window.
When it is time to delete your modeless dialog, you should use the DestroyWindow() function:
m_ModelessDlg.DestroyWindow();If you provide the user with a way to close the modeless dialog from within the dialog (that is, a Close button), you should call DestroyWindow() in its handler to destroy the window. However, the dialog object is still lurking around in memory somewhere.
You have to decide how you want to handle the deletion of the object in your application. It may be useful to keep the object around so that you can access its data members, even after the dialog window is destroyed. In this case, you should make sure to clean up when you are finished with the dialog object, perhaps in CWinApp::ExitInstance().
If you prefer to assign a dialog template at runtime, you can also use CDialog::CreateIndirect() or CDialog::InitModalIndirect(). These involve more than the methods you have explored so far, but if you have used the Windows API versions of these, you will find that the CDialog versions are quite similar.
Now that you have seen how to create dialog objects and present their windows to the user, let's take a step back and see how to create a class for your dialog.
You learned how to create your dialog templates in Chapter 1. Once the template is created, you build a class around it. This is most easily done with ClassWizard.
You may choose to import a class from a file, select an existing class in your project, or create a new class. Choosing to create a new class will produce the dialog shown in Figure 5.1.
FIGURE 5.1. The Create New Class dialog box.
Here, you can choose a name and base class for your class. In most cases, your class will derive directly from CDialog. There are two exceptions: if you derive your own class from one of the standard Windows dialog classes (which you learn about shortly) or if you have developed your own class from CDialog that you wish to use as the base class for the dialogs in your application.
ClassWizard will assign filenames for the declaration and implementation of your class, based on the name of your class. If you want to change the filenames, click the Change button. This also allows you to add the declaration and implementation of your class to an existing file, which can be handy if you create several small dialog classes.
You can also choose to add the new class automatically to the Component Gallery for easy reuse in other applications. You learn about the OLE options available in Part III, 'Programming with ActiveX,' so for now let's choose None.
Once you decide on the options you like, clicking on Create will create the new class for you and return you to ClassWizard. From the Message Maps page, shown in Figure 5.2, you can add handlers for the notifications generated by each of the controls in your dialog. As you will soon see, each control may be capable to send many different notifications.
Alternatively, you can add your own message map entries and handler functions, as you saw in Chapter 3, 'MFC Messages and Commands.'
FIGURE 5.2. The Class Wizard Message Maps page
ClassWizard is also very useful in adding member variables to your class to handle the data and controls in your dialog. This is done with the Member Variables page shown in Figure 5.3.
FIGURE 5.3. The ClassWizard Member Variables page
From here, you can add member variables to represent the controls in your dialog by selecting the control ID and clicking on Add Variable to get the Add Member Variable dialog box shown in Figure 5.4.
FIGURE 5.4. The Add Member Variable dialog box
Here, you may choose the name for your new member variable. ClassWizard will add m_ to the dialog box automatically, but you don't need to use it if you don't want to. However, it is a good habit to get into for naming your member variables.
Next, you will notice the Category list. For many controls, you can choose to create a member variable to represent either the control itself or the data held in the control. Control variables may be used to manipulate the control in the dialog, and value variables will automatically create a member variable to be used in dialog data exchange (DDX). As you will see later in this chapter, this provides a very powerful mechanism for initializing the controls in your dialog and returning the data when the dialog is dismissed.
For some controls, such as buttons, you cannot create value variables, because these controls don't really represent a data value; they just provide a source for messages to your dialog.
In the Variable type field, you can choose a class or data type for your variable. ClassWizard will present a list of appropriate choices, based on the type of control you have selected. For example, if you are adding a value variable for an edit control, you can choose from several different data types here. Be certain to choose the type of data you will want to work with in your application code, because ClassWizard will allow you to add only one value variable for each control.
For control variables, you will see a list of classes designed to handle the type of control you have selected. For example, if you are adding a control variable for an edit control, you can choose the CEdit class as the type for your control variable. You also can choose any classes that you have included in your project that derive from CEdit. This is useful if you want to create a special class to add functionality to the standard controls.
Recall that the control member variables created by ClassWizard allow you to use almost all the functions associated with a control in your dialog by way of a member variable in your dialog. For example, if you had an edit control in your dialog and have created a CEdit member variable to represent it, you could set the text in the edit box like this:
m_myEditBox.SetWindowText('Hello');There are also many other functions available in MFC's control classes, some of which you will see in the sections about specific controls later in this chapter. For a full list of available member functions, see the online documentation.
If you don't want to create member variables to represent your controls, you can get a pointer to the CWnd object for a control in a dialog by using the GetDlgItem() function of your CDialog object:
CEdit* pNameBox;Note that you are free to cast the return pointer to whatever type you want. You may then use this pointer to operate on your control:
pNameBox->SetWindowText('Hello');Of course, the SetWindowText() function is really a member of CWnd, so you could use a CWnd pointer here and avoid the cast, but you can call all the functions of the CEdit class if you use the CEdit pointer.
Note that the Windows objects that you are familiar with from coding to the C API are still there if you need to work with them directly rather than going through the MFC Classes. If you have done much programming with the regular C Windows API, you are familiar with the GetDlgItem() function. You can also still use the global function ::GetDlgItem() or an overridden CWnd::GetDlgItem() to work with the actual window handles for your controls. In addition, every CWnd object has an m_hWnd member that holds the actual handle to its window.
Up to this point, you have learned how you can work with the controls in your dialog, but really haven't seen much about what you can really make your controls do for you. It's time to fix that. Here, you look at the classes that MFC provides for working with the Windows controls that you use in your dialogs. These classes provide a way for your C++ applications to deal with Windows controls as C++ objects.
For each control class, you will look at some of its styles that affect how the control works, the methods that allow you to work with the control, and the notification messages that the control will send back to your application when the user manipulates the control.
The styles for your control can be set in several different ways, starting with the properties dialog for the control when you are creating the dialog template in the Developer Studio. If you are creating controls without using dialog templates, the styles are specified in the call to CWnd::Create(). These may be changed at runtime by calls to CWnd::ModifyStyle().
The control methods specified allow you to affect the appearance and functions of the control, as well as manipulate the control's data.
The control notifications are Windows messages that are sent to the parent of the control. In most cases, this is a dialog. You add handlers for some or all of these notifications with ClassWizard or by adding your own message map entries, as you did in Chapter 4, 'Frames, Documents, and Views.'
In the examples so far, you have used edit controls, so let's look at them first. MFC uses the CEdit class to allow you to use C++ constructs to work with the functionality of an edit control.
There are many styles that affect how an edit control works, but perhaps the most important is the ES_MULTILINE style.
Setting the ES_MULTILINE style will make your edit control a multiline edit control. Without this style, the user will be able to enter only a single line of text.
If you have specified ES_MULTILINE, you may also use ES_AUTOVSCROLL to scroll the edit control automatically if the user tries to enter a new line at the end of the edit control. If you do not specify ES_AUTOVSCROLL, Windows will not allow entering lines past the bottom of the control.
ES_WANTRETURN will allow the user to use the Enter key to add a carriage return in a multiline edit control. Without this, the Enter key will trigger the default pushbutton. This does not affect single-line edit controls.
ES_READONLY prevents the user from changing the text.
ES_AUTOHSCROLL will automatically scroll the display if the user tries to enter text past the end of the edit control.
You can use the ES_CENTER, ES_LEFT, and ES_RIGHT styles to align the control's text.
ES_LOWERCASE and ES_UPPERCASE will automatically adjust user input to the desired case.
ES_NOHIDESEL will cause the control to keep the selected text in the edit control highlighted when the control loses focus. Without this, the selection is unhighlighted when the control loses the input focus.
ES_OEMCONVERT will convert entered text from ANSI to the OEM character set and back to ANSI, to ensure that the AnsiToOEM() function will work properly. This is most useful in working with filenames.
ES_PASSWORD causes the edit control to display all characters entered as an asterisk (*) to hide the real text. You can specify an alternative character with SetPassWordChar().
The CEdit class provides many methods for manipulating the edit control. You will explore several of the more common functions here, although you may need to consult the online documentation for other things you may want to do.
To work with the text in an edit control, you can use the CWnd::SetWindowText() and CWnd::GetWindowText() functions.
The LimitText() function allows you to set the maximum length that the edit control will accept.
The SetSel() and GetSel() members allow you to work with the character positions of the current selection in the edit control. ReplaceSel replaces the current selection with the text you pass it. You can use Clear() to remove any current selection.
The CEdit class supports the Cut(), Copy(), and Paste() members to work with the current selection and the clipboard.
The Undo() member will undo the last editing change to the text. You can use the CanUndo() member to determine whether the last change can be undone.
The GetModify() method tells you whether the text in the edit control has been modified. You can set or clear the modification flag with SetModify().
For multiline edit controls, you can also use the following methods:
The GetLineCount() function returns the number of lines in a multiline edit control.
The GetLine() function returns the text for a single line.
You may work directly with the memory that a multiline edit control uses by manipulating the local handles with GetHandle() and SetHandle().
The following notifications can be handled with an entry in the message map—for example, ON_EN_CHANGE handles the EN_CHANGE message.
EN_CHANGE is sent when the user may have changed the text.
EN_UPDATE is also sent when the user may have altered the text, but before it is changed on the display.
EN_ERRSPACE is sent if the edit control cannot allocate enough memory for an operation.
EN_HSCROLL and EN_VSCROLL are sent when the user has clicked on the appropriate scroll bar, before the screen is updated.
EN_KILLFOCUS is sent when the input focus shifts to another control.
EN_MAXTEXT is sent when inserting more than the maximum amount of text is attempted, either by insertion programmatically or by user input.
EN_SETFOCUS is sent when the control first receives the input focus.
Windows provides several different varieties of static controls. These are generally used as labels for your other controls or just to enhance the appearance of the dialog. This class of control includes static text and group boxes, which usually just add a fancy border around a group of controls, but it may also contain a bitmap or icon.
By default, the Developer Studio will assign static controls with the ID of IDC_STATIC, which is convenient, because you won't need to access most of these at runtime. However, if you do need to work with a static control at runtime, make sure that you assign it a unique ID.
You can assign control variables to static controls with ClassWizard or get a pointer to the object with GetDlgItem(), as you learned earlier.
If you simply want to change the text of a static control, you can do this with CWnd::SetWindowText(). If you want to selectively display or hide a static control, you can do so with the CWnd::ShowWindow() call, using SW_HIDE or SW_SHOWNA as a parameter.
For more complicated operations on static controls, you will want to work through an instance of the CStatic class. This will allow you to work with images displayed by the static control by using the GetBitmap() and SetBitmap() members to work with HBITMAP types, or GetIcon() and SetIcon() to work with HICON types, or GetEnhMetaFile() and SetEnhMetaFile() to work with HENHMETAFILE types.
Although static controls do not generally accept input, they will generate BN_CLICKED or BN_DOUBLECLICKED notifications if you set the SS_NOTIFY style.
Windows supports several different types of buttons, including simple pushbuttons, check boxes, and radio buttons. MFC uses the CButton class to support each of these, depending on the styles selected for the button. CButton is also the base class for CBitmapButton, which allows you to assign a separate bitmap for its up, down, focused, and disabled states.
All buttons may send the BN_CLICKED or BN_DOUBLECLICKED notifications, which may be handled with the ON_BN_CLICKED and ON_BN_DOUBLECLICKED message map macros.
The CButton::SetButtonStyle() function allows you to set the style of a button at runtime, and the CButton::GetButtonStyle()returns the current style of a button. In most cases, you do not need to use these, but the following sections mention a few cases that apply to the different classes of buttons.
When you add a control to a dialog template using the button from the toolbar, or when you create your own button control with the BS_PUSHBUTTON style, you are creating a pushbutton. This is the simplest of the button classes, and perhaps the most used. The OK and Cancel buttons that you see in most dialogs are of this type.
In most applications, you do not have to work directly with the button control. Instead, you just add handlers for the command message that it sends to its parent window, usually a dialog. By default, the CButton class provides a handler for the OK and Cancel buttons that will close the dialog and return IDOK or IDCANCEL.
The one thing that you would be most likely to tweak with a pushbutton is setting the BS_DEFPUSHBUTTON style (using the Styles page of the control's Properties dialog) so that this button will send its command message when the user types the Enter key in the dialog.
The other instance in which you would deal with your pushbutton objects directly is implementing owner-draw buttons, where your application is responsible for all the rendering tasks for the button.
For those of you who have CD players and digital tuners in your car stereo, the name for this class of buttons may seem a bit odd. The name stems from the old radios that had mechanical station presets. Whenever you pushed in one of the buttons, the button that was previously pushed in would pop back out. This is precisely how radio buttons work. You may select one, and only one, choice from a group of radio buttons. This is generally used in cases where the user has a handful of different mutually exclusive options from which to choose. Radio buttons are added to a dialog with the radio button tool in Developer Studio. If you are creating your controls at runtime, these buttons will have the BS_AUTORADIOBUTTON style set.
When you create a dialog with radio buttons, you will most likely want to group them. By default, all radio buttons you add to a dialog are in the same group. If you wish to use several different groups of radio buttons, this presents a problem. To divide your radio buttons into groups, you should first make sure that the buttons you want to include in a group are listed consecutively in the tab order. Next, you enable the WS_GROUP style (the Group check box in the properties dialog) for the first button of each group.
To initialize the settings of your radio buttons, you use CButton::SetCheck(). You may also query the current state of a button by using CButton::GetCheck(). Because of this, you may want to declare control member variables for your radio buttons.
Once you have your radio buttons grouped, and have initialized them if you want to have a default selection, Windows will handle the group for you, making sure that only the last button selected is selected. Additionally, MFC will allow you to query the state of an entire group of radio buttons with DDX_Radio(), which is covered later in this chapter.
The third class of buttons supported by CButton is check boxes. These are used to present options to the user that can be toggled on and off and are generally independent of any other selections. Check boxes have the BS_CHECKBOX style set and may be added to dialog templates by using the check box tool.
To set the state of your check boxes, use the CButton::SetCheck() function. If you pass 0 to this function, the box will not be checked; if you pass 1, the box will be checked. Additionally, if you have added the BS_3STATE style to your check box, you may pass a 2, which displays the check box in its indeterminate state. You can use the CButton::GetCheck() function to query the current state of your check boxes.
List box controls present a list of text items for the user to view and/or select. MFC support for list boxes is provided by the CListBox class. To use a list box in your dialog, use the list box tool to insert a list box into your dialog template, and use ClassWizard to create a control variable for the list box.
Alternatively, you can construct your own CListBox object and call Create() to create the Windows list box control and attach it to the CListBox object.
There are many styles associated with list box controls. These may be set when you add the list box control to a dialog template, when you call Create(), or by calling CWnd::ModifyStyle() after the control is created.
For starters, you will want to specify LBS_NOTIFY if you intend to handle messages from the list box. If you do not specify this style, the list box will not send messages.
LBS_SORT causes strings in the list box to be sorted alphabetically.
You can use the LBS_USETABSTOPS style to tell the list box to enable the expansion of tab characters in the strings listed.
The LBS_MULTIPLESEL style allows the user to select more than one item by using the Ctrl key.
For the entire list of styles, see the online documentation. There are many other styles related to format, scrolling, and special processing that you may be interested in.
The first thing you will probably want to do with your list box is add strings to it. This can be done with the AddString() method, which takes a pointer to a string. This adds the string to the list box according to the sort style you have specified. To insert a string in a particular spot, you can use InsertString(), which takes an integer index and a string pointer. If you want to add the string to the last position, you can pass -1 for the index.
You may automatically scroll the list so that a certain position appears at the top of the list box by calling SetTopIndex(). This does not affect the sort order of the list box; it just scrolls it.
To remove items from your list box, you can call DeleteItem() to delete a single item, or you can call ResetContent() to clear the whole list box.
To get the total number of items in a list box, you can call GetCount().
To retrieve the text for a given entry, you can call GetText().
If you have selected the LBS_USETABSTOPS style, you can use the SetTabStops() function to specify tab spacing in the list.
If the user has selected something from the list, you can call GetCurSel() to return the index of the item that is selected. If, however, you have set the LBS_MULTIPLESEL style, you call GetSelCount() to find out how many items are selected and then call GetSelItems() to return an array of integer indexes.
A list box also allows you to associate a DWORD value with each entry. This can be any data you like, including pointers. To work with this data, you can call the SetItemData() and GetItemData() methods. If you plan to store pointers in the data, you may want to use SetItemDataPtr() and GetItemDataPtr(). These functions work with LPVOID pointers, so you can avoid many casts and compiler warnings.
List boxes that have the LBS_NOTIFY style set send a LBN_SELCHANGE notification when the user changes the selection.
A list box also sends an LBN_DBLCLK notification if the user double-clicks on a selection. This is generally used in Windows applications to select an item and trigger the default pushbutton action. You will probably want to do the same in your applications.
Combo boxes share a great deal of functionality with list boxes. Actually a combo box is a list box with an edit control attached. The edit control is displayed to the user. The list attached to it may be displayed in different ways, depending on the style bits.
The CBS_DROPDOWN style has a list that drops down when the user clicks on the drop-down button. This also allows the user to type in a selection.
The CBS_DROPDOWNLIST style is similar, but the user will not be able to enter items not in the drop-down list.
The CBS_SIMPLE style specifies that the list will not drop down at all—it is always visible. The user may also enter a selection with the keyboard.
Combo boxes support most of the methods available for list boxes in working with the list and most of the methods available for single-line edit boxes for the edit box. For information on these, see the previous sections.
Combo boxes send a CBN_DROPDOWN notification when the user drops down the list.
Combo boxes also send notifications similar to those used by list boxes and edit controls. These include CBN_DBLCLK, CBN_EDITCHANGE, CBN_EDITUPDATE, CBN_SELCHANGE, CBN_SETFOCUS, and CBN_KILLFOCUS.
To help you initialize the data in your dialogs and return the data to your application, MFC implements dialog data exchange (DDX). This takes advantage of the fact that your CDialog objects are created before the actual window is created and are still around when the dialog window has gone away. DDX automates the process of loading your dialog object's data into the dialog window and retrieving the data from the dialog when it closes. This process can also include some validation.
Dialog data exchange is done in the DoDataExchange() function of your dialog class, which is called by the UpdateData() function. Your application may call UpdateData() at any time, with TRUE to save and validate the data from your controls or FALSE to update the controls with member data. The default CDialog::OnInitDialog() and CDialog::OnOK() functions will call UpdateData() for you.
If you have used ClassWizard to add member variables for your controls to your dialog class, this will be handled for you. You may also specify some range or length validation parameters in ClassWizard.
The code generated by ClassWizard will look something like this:
void CMyDlg::DoDataExchange(CDataExchange* pDX)This example shows only a few of the many DDX functions available. For a list of all the predefined functions for dialog data exchange, search for DDX in the online help. Note that the name of the function generally indicates the type of control with which it will exchange data. If you don't happen to find a function listed that suits your needs, you can create your own DDX functions.
Each of the standard DDX functions takes three arguments: a pointer to a CDataExchange object, the resource ID of a control, and a reference to a variable.
The CDataExchange class provides two member variables: The m_bSaveAndValidate flag will be TRUE if the data exchange is moving from the controls to the member variables and FALSE if moving data from the member variables to the controls. The m_pDlgWnd member points to the CWnd object (usually a dialog) with which the exchange is working.
The CDataExchange class also provides a few functions that you will use in your DDX function. These include PrepareCtrl() and PrepareEditCtrl(), which prepare a dialog control for data exchange, and the Fail() function, which you can use to throw an exception if something is wrong in your data exchange or validation function.
The following example shows how this is used:
void AFXAPI DDX_MyData(CDataExchange* pDX, int nIDC, int& nValue)In this example, you first call PrepareEditCtrl() to set up the control for data exchange. This returns the handle for the control specified by nID. If you are not working with an edit control, you should call PrepareCtrl(). You then check to see whether you are loading or storing the data and call the appropriate functions to work with the custom data. If you run into problems, you call Fail() to halt the data exchange and return quietly to the dialog. You need to notify the user in some way that the exchange failed—hence, the AfxMessageBox() call.
For additional examples of DDX functions, see DLGDATA.C in DevStudioVCmfcsrc. This file implements all the standard MFC DDX routines, including the DDX_TextWithFormat() routine, which will parse data for you, using a simplified version of sscanf().
In the previous example of DoDataExchange(), you will also notice several dialog data validation (DDV) functions. These are used to validate the length or range of the data before it is copied back to your member variables. Although your DDX calls may be placed in any order, you must call the DDV function for a control immediately after the DDX call for that control. You can implement your own DDV functions in a similar fashion. If you do this, keep in mind that data validation should occur only when the data is being stored; thus, you should check the m_bSaveAndValidate flag before doing anything. Your function should also provide a meaningful message to the user before calling Fail() if something doesn't pass the validation.
The DDV functions provided by MFC don't check to see whether a control is hidden or disabled. If you do not want to validate the value of a control in some conditions, you can place if blocks around DDV calls in your DoDataExchange() function; make sure that they are outside of ClassWizard's AFX_DATA_MAP blocks, or ClassWizard will get quite lost.
TIP |
You are not really limited to member data items in your DDX functions, you can actually use something like this: DDX_Text(pDX, IDC_MYFIELD, pDoc->m_nMyInt); which enables you to exchange data directly with your document or other more permanent locations. However, be careful with this solution, because the data will be updated even if the validation fails. You may also have to go out of your way to update the document, its views, and/or the dirty flag. |
There are several common functions that seem to appear in almost every Windows application, including file selection, printing and page setup, find and replace, and color and font selection. To help with these functions, the Windows operating systems provide the common dialogs. MFC in turn provides classes to help you use the common dialogs. Starting with NT 4.0, the same set of dialogs may be used on NT as on Windows 95.
Each of the common dialog classes is based on CCommonDialog, which derives from CDialog. This means that you can use the common dialogs just like any other dialog in your application. In addition, you can derive your own classes from the common dialog classes to suit the needs of your application.
The most common of the common dialogs is the File dialog. This is used by applications to open new files or to select files for 'save as' or any other function your application needs. If you need to do anything with files, you should look at a way to use the CFileDialog class. Even if you have to derive your own class from it, it sure beats having to do the whole thing yourself.
To use the CFileDialog class in your application, you first need to call the constructor, which takes several parameters. The first is a flag that, if TRUE, will create a File Open dialog, or if FALSE, will create a File Save As dialog.
The second parameter allows you to specify a default extension to be added if the user does not specify an extension. By default, this is NULL.
The third parameter specifies the initial filename to appear in the dialog. This also defaults to NULL, which will not display an initial filename.
The fourth parameter gives flags that will be set in the m_ofn.Flags member. Setting various flags here can customize the behavior of the dialog. The available flags are detailed in the online documentation under OPENFILENAME.
The fifth parameter allows you to specify filters to select the files that will appear in the file list. This string is composed of filter descriptions and their file extensions, separated by (|). The string ends with (||) and a null character. For example, if you wanted to work with C or C++ files, your string might look like this:
char szFilter[] = 'C Files (*.c)|*.xlc|C++ Files (*.cpp)|*.cpp||';The sixth parameter is a pointer to the dialog's parent or owner window.
Once you have constructed the CFileDialog object, you can initialize it by modifying the m_ofn member, which is an OPENFILENAME structure, before calling the DoModal() function to display the dialog.
NOTE |
If you wish to allow multiple selections in the file dialog, you need to specify the OFN_ALLOWMULTISELECT flag in m_ofn.Flags. You also need to replace m_ofn.lpstrFile with a pointer to a buffer you have allocated, and set m_ofn.nMaxFile to the size of the buffer, before calling DoModal(). |
If the DoModal() returns IDOK, you can use member functions such as CFileDialog::GetPathName() to get the name of the file the user has selected. You may also access the m_ofn member directly.
The MFC CPrintDialog class provides access to the Windows common dialogs for print and print setup, which allows the user to specify printer specific options. This automatically handles options specific to the printers that are installed on the user's system.
As with other common dialog classes, you must call the constructor for CPrintDialog before calling DoModal(). The constructor takes a Boolean parameter to indicate the type of dialog. If this is TRUE, the Print Setup dialog is shown. If it is FALSE, the normal Print dialog is shown, which will provide access to the Print Setup dialog by way of the Print Setup button.
The second, optional, parameter specifies the flags to be used in the m_pd.Flags member. The m_pd member is a PRINTDLG structure that you can modify to select several options about the behavior of the dialog. You may also specify a third parameter for the parent of the dialog.
If DoModal() returns IDOK, you can use members of CPrintDialog, such as GetPrinterDC(), to retrieve the device context for the selected printer, or GetFromPage() and GetToPage() to get the page numbers the user has chosen to print. You will learn more about how to use this information in the next chapter.
TIP |
You can use the CPrintDialog::GetDefaults() member to get information about the default printer without displaying the dialog to the user. This will fill in the m_pd member with information about the default printer. |
In addition to the Print and Print Setup dialogs, Windows 95 and Windows NT add the Page Setup dialog, which is intended to replace the Print Setup dialog. MFC supports the Page Setup dialog with the CPageSetupDialog class.
Once again, you must call the constructor before calling DoModal() to present the dialog to the user. The constructor allows you to specify flags to be placed in the m_psd member of type PAGESETUPDLG. These flags can be used to enable or disable options such as margins and orientation in the dialog.
If DoModal() returns IDOK, you can use the member functions of CPageSetupDialog to get information about the current printer and page setup. You can also access the m_psd member directly. Of most interest, you can use the CreatePrinterDC() function to create a device context for the current setup.
Windows provides a common dialog allowing the user to select colors. Conveniently, MFC also provides the CColorDialog class to help you use it. The constructor for CColorDialog takes a COLORREF structure, allowing you to specify an initial color selection, as well as a flags parameter, used to fill in the m_cc.Flags member.
These flags, as well as the rest of the m_cc structure, are defined in the online help for the CHOOSECOLOR structure. For instance, you may specify the CC_FULLOPEN or CC_PREVENTFULLOPEN to present the custom color section of the dialog or prevent the user from choosing custom colors.
Once you have called DoModal() to present the dialog, and it has returned IDOK, you can use CColorDialog::GetColor() to return a COLORREF structure for the color selected.
MFC provides the CFontDialog class to help you work with the Windows font selection dialog. This presents the user with the list of fonts currently installed on the system.
The constructor for CFontDialog takes an LPLOGFONT parameter to specify an initial font setting, as well as a flag's parameter used to initialize the m_cf CHOOSEFONT structure's flags. Optionally, you may specify a device context for the printer to select fonts from and a pointer to the parent window of the dialog.
The dialog is presented by calling DoModal(), which returns IDOK if the user closed the dialog with the OK button. At this point, you can use CFontDialog::GetCurrentFont() to return a pointer to a LOGFONT structure detailing the user's selection.
Windows also provides a standard Find and Replace dialog, which MFC wraps nicely in the CFindAndReplace class. Although this dialog will not actually perform the find or replace functions for you, it does help by providing the standard interface dialog that users expect.
The Find and Replace dialog is also different from the other common dialogs mentioned so far, because it is intended to be used as a modeless dialog. Therefore, you will want to use the new operator to create a new CFindAndReplace object on the stack. The constructor takes no parameters.
Instead of passing parameters to the constructor, you can modify the m_fr structure (of type FINDREPLACE) to initialize the dialog before calling Create() to present the dialog to the user.
The Create() function is also used to pass some initialization information to the dialog. The first parameter, if set to TRUE, will create a simple Find dialog, or a Find and Replace dialog if set to FALSE. You may also specify the string to find the default replace string, the flags, and the parent window. The default flags specify FR_DOWN to search down the document.
Because this is a modeless dialog, it is also a bit trickier to tie in to your application. To use the Find and Replace dialog, you define a handler for a registered message ID that you have received from the ::RegisterWindowMessage() function. To implement all this, you should have code that looks something like the following in your class declaration:
class CMyFrameWnd : public CFrameWndYou then create a registered message ID:
static UINT WM_FINDREPLACE = ::RegisterWindowMessage(FINDMSGSTRING);The message map entry then looks like this:
BEGIN_MESSAGE_MAP( CMyFrameWnd, CFrameWnd )Now you have to code your handler function to do the actual find or replace operation. Your handler can make use of several different CFindReplaceDialog member functions to retrieve information from the dialog. Your handler may look something like this:
afx_msg LONG CMyFrameWnd::OnMyFindReplace(WPARAM wparam, LPARAM lparam);This is just a simple example. You will want to be a bit more concerned with the results of things such as MatchCase() and SearchDown() in a real implementation.
MFC also provides many classes based on COLEDialog to handle dialogs specific to OLE implementations. These include those shown in Table 5.1.
Table 5.1. OLE common dialog classes
Dialog class |
Function |
COLEInsertDialog |
Displays the Insert Object dialog box, used to insert new linked or embedded objects |
COLEPasteSpecialDialog |
Displays the Paste Special dialog box, used for imple- menting Edit | Paste Special |
COLELinksDialog |
Displays the Edit Links dialog box, used to modify information about linked items |
COLEChangeIconDialog |
Displays the Change Icon dialog box, used to change the icon for an OLE object |
COLEConvertDialog |
Displays the Convert dialog box, used to convert an OLE object to a new type |
COLEPropertiesDialog |
Displays the Windows common OLE properties dialog, used to change settings for OLE documents |
COLEUpdateDialog |
Displays the Update dialog box, used to update all links in a document |
COLEChangeSourceDialog |
Displays the Change Source dialog box, used to change the source or destination of a link |
COLEBusyDialog |
Displays the Server Busy and Server Not Responding dialog boxes, for handling calls to busy applications |
Because any discussion of these dialogs is swamped in OLE terms and architecture, the dialogs are not discussed here. Suffice it to say that they work in much the same way as the other common modal dialogs discussed in this chapter.
Windows 95 and Windows NT support the use of property sheets to allow you to present several different pages of options, using tab controls to allow the user to switch between pages. Visual C++ uses this in many places, including the project settings and many properties dialogs. You can use MFC to create your own property sheets for use in your applications. This is becoming part of the standard look and feel of Windows, and your users will expect it.
Property sheets in MFC are actually made up of several different pages based on the CPropertyPage class. The pages are then incorporated in one sheet, supported by CPropertySheet.
Property pages are actually dialogs, although they are handled a little bit differently in a property sheet. To create property pages, you create a dialog template for each page. (True, you could create the dialogs in your code with separate Create() calls for each control, but why bother? I figure if you're reading this, you are enough of a masochist without my help, so I'll try to keep this simple.)
When you are designing the dialog templates for your property pages, you should keep in mind how they will appear in the property sheet. This means that you should try to keep all your pages the same size and try to align the first control in each page so that the user doesn't have to reorient to each new page while tabbing through them. To help with this, you can use one of the three dialog templates available when you insert a new dialog resource. You can get to these by expanding the dialog tree in the New Resource dialog. This will help keep all your pages the same size.
In addition, there are a few other things to do when setting up the dialog templates. Your template should have the Child style and a thin border. You should enable a title bar, but be aware that the caption will be used for the tab of the page—try to keep this short. You also should have the template be disabled by default—the property sheet will enable the appropriate page when it is time. If you use the templates intended for property pages, these settings will be set up for you.
Your property pages should not include OK or Cancel buttons; these will be provided by the property sheet.
Next you create a class for each page, based on CPropertyPage. This is best done with ClassWizard, which will automatically notice that you have added new dialog template resources and prompt you to create a new class.
TIP |
When ClassWizard enables you to edit the filename for the class, you can change this so that all your pages are in the same file. This can make keeping track of them a bit easier. |
Now that all the building blocks are laid out, you can put them together into one sheet. To do this, start by creating a class for your sheet, based on CPropertySheet. It's convenient to place this in the same file as your pages, just to keep things together.
Next you create an instance of your sheet class in the frame that will contain the property sheet by calling its constructor, which will look something like this:
CMySheetClass m_MySheet(_T('My Options'), this, 1);The constructor takes either the resource ID of an entry in the string table or a string pointer to the caption of the property sheet as its first parameter.
NOTE |
The _T macro creates a Unicode string, which is essential for proper localization. If the symbol _UNICODE is defined, this will generate a Unicode string, which can support character sets that use more than one byte per character. If _UNICODE is not defined, _T will generate a plain C-style string. |
The other parameters are for the parent window of the sheet and an index to the page that will be active when the sheet is shown.
If you are creating an array of different sheets, you call the constructor with no parameters, then call Construct() for each array element. This takes the same arguments as the constructor.
Next you construct an object for each of your property page classes. It makes sense in most cases to include the pages as members of the sheet; you can then call their constructors in the constructor of the sheet. Make sure that you add these to both versions of the constructor.
TIP |
As for any classes defined in separate files, make sure that you include the headers for your property page classes in any file where you use them. This really helps cut down on compile errors. |
Once the page objects are created, you call AddPage() for each page, something like this (this also fits well into the constructor for the sheet):
m_MySheet.AddPage(&m_PageOne);At this point, the property sheet is good to go. You can present it to the user as you would any other dialog. That is, you can call either DoModal() to create a modal property sheet or Create() to create a modeless property sheet.
I could add more detail here, but it would get a bit lengthy for this spot. Once the property sheet is up and running, you can use its member functions to work with the pages. The SetActivePage() member allows you to select the page the user sees, and GetActivePage() returns the active page object. There are also several other functions for working with pages, the number of pages, and their indexes.
Like all objects derived from CDialog, each property page will have its own DoDataExchange() function. The DoDataExchange() function is called by way of OnUpdate() each time the user changes pages.
If you want to do any special validation before the user is allowed to switch pages, you can override the OnKillActive() member for your pages. This function is called before DoDataExchange(), whenever the user tabs to a new page or clicks on the OK button. If OnKillActive() returns FALSE, the user is not allowed to change pages. By default, this does not give the user any reason why the change was disallowed, so make sure to spit out a message of some sort.
On the other side of the page change, OnSetActive() is called for a page before it is activated. If you override this function, make sure to call the base class implementation, which will set up the page and call UpdateData(). This in turn calls DoDataExchange().
If you are using a modal property page, you will probably just wait until the user has clicked on OK, whereby DoModal() returns IDOK, before copying any data into more permanent areas of your application. However, you may want to provide the user with a way to implement changes now, particularly in the case of modeless property sheets.
You may be thinking to yourself that the Apply button in the lower-right of the property sheet would be mighty handy for this, if you could just find a way to make it active. You can activate the Apply button by calling SetModified() from within any of the pages. You can call this in handlers in your page class, for instance, in response to EN_CHANGE messages from edit controls.
Once you have called SetModified(), the Apply button will remain active until the user clicks on it, even when the user changes pages. The default handler for the Apply button will call the OnKillActive() and DoDataExchange() functions for the visible page and set the modified flag to FALSE for each page. It then calls OnOK(), without closing the dialog.
In your OnOK() function, you have several options concerning how to update your application. It is generally a good practice to send a windows message back to the main window, which it will then use to update its data. You can do this with the following code:
void CMyPage1::OnOK()This will send a message to the main window and wait until it is handled before returning. The main window should then add an appropriate handler function to fetch the data and a message map entry:
ON_MESSAGE_MAP(WM_USER, FetchSheetData)Wizards seem to be popping up in just about everything Microsoft makes these days. Although 'revolutionary wizard technology' is a little hard to swallow as the renaissance of the computer world, wizards are very handy little gadgets. You can use them to guide the user through a series of steps involved in performing a task, such as creating a new file, or even a new project, the way AppWizard does. Note that ClassWizard just shares the buzzword; it doesn't really work like the wizards that you will create here, which present a series of steps.
Apart from all the hype and marketing propaganda, creating your own wizards is really quite simple. MFC wizards are simply property sheets with a few extra widgets added. In fact, all you have to do to make a wizard is create a property sheet as shown previously and add a call to CPropertySheet::SetWizardMode() between adding all the property pages and calling DoModal(). Just add a line like this after your calls to AddPage():
m_MyWizSheet.SetWizardMode();And, Presto! You've got a new wizard.
By calling SetWizardMode(), you tell the property sheet to implement the Next (or Finish for the last page), Back, and Cancel buttons instead of creating a tabbed dialog.
To finish your wizard, you probably will want to tweak a few things. Most notably, you will want to enable the Finish button at some point. By default, only the Back, Next, and Cancel buttons are displayed. These will allow the user to move between the pages, calling the OnSetActive() and OnKillActive() functions as a normal property sheet would when the user changes pages.
To change the function and appearance of the wizard buttons, you call CPropertySheet::SetWizardButtons(), which takes a combination (bitwise-OR) of several flags. This parameter may specify the PSWIZB_BACK and PSWIZB_NEXT to enable the Back and Next buttons, PSWIZB_FINISH to enable the Finish button, or PSWIZB_DISABLEDFINISH to add a disabled Finish button. Note that you can only call SetWizardButtons() after you have called DoModal().
There is one catch, however. You can have either a Next button or a Finish button, but not both. If you specify both flags, the Finish button will be displayed. (Yeah, AppWizard has both a Next and a Finish button, but then again I suppose a bit more work went into AppWizard than this example.)
There is also one more catch. The SetWizardButtons() function is a member of CPropertySheet, although you will probably want to call it from within the OnSetActive() function of a class derived from CPropertyPage. The following example shows how to deal with this, using GetParent() to set up the Finish button:
BOOL CLastStepPage::OnSetActive()This example also uses the SetFinishText() function to change the text for the Finish button. Note that the button settings will not change when the user changes pages unless you change them. Thus, you should add an OnSetActive() override to each page to set up the buttons appropriately. Otherwise, the example above will not allow users to return to the last page if they ever clicked on Back.
Finally, when users dismiss the wizard by clicking on the Finish button, DoModal() will return ID_WIZFINISH instead of the usual IDOK. The Cancel button will still return IDCANCEL.
You may have noticed that many applications include gadgets such as combo boxes in their toolbars, rather than just buttons. You can add this sort of functionality to your applications with another special sort of dialog known as a dialog bar. By using a dialog bar, you can add any sort of control you like to the toolbar. The user can also use the Tab key to move between controls in the dialog bar.
To create a dialog bar, start with a dialog template. It is generally best to use the IDD_DIALOGBAR template provided by expanding the Dialog tree in the dialog presented by the Insert | Resource command. Your dialog template should have the Child style and no other options selected. In particular, the Visible style should not be set. As long as you stick to these few rules, you can add any controls you like to the dialog bar template.
MFC supports dialog bars with the CDialogBar class, which is derived from CControlBar. In most cases, you do not need to derive your own class for a dialog bar, because the control notifications will be sent to the parent, usually the main frame, that should handle them. Thus, you use the base CDialogBar class. The constructor for CDialogBar takes no parameters.
To create the actual window for the dialog bar, call CDialogBar::Create(). You pass a pointer to the parent window, a resource ID of the dialog template to be used, the dialog bar style flags, and a control ID for the dialog bar. The style flags are used to pass alignment styles, like other control bars. These include CBRS_TOP, CBRS_BOTTOM, CBRS_LEFT, and CBRS_RIGHT, as well as CBRS_NOALIGN, which will keep the dialog bar in its current position when the frame is resized.
Once you have called Create(), you need not do anything else with the dialog bar but handle the control notifications that it sends to the frame. You may also wish to add ON_UPDATE_COMMAND_UI handlers for any of the controls in your dialog bar that you wish to disable or otherwise alter at runtime.
In this chapter, you took a look at dialogs and the things you can do with them in your applications. You learned how to create both modal and modeless dialogs and present them to the user. You have also seen how to work with the controls in your dialog and use Dialog Data Exchange and Dialog Data Validation to simplify the task of fetching data from your dialogs.
|