previous page
next page

CDialogImpl

Dialogs represent a declarative style of user interface development. Whereas normal windows provide all kinds of flexibility (you can put anything you want in the client area of a window), dialogs are more static. Actually, dialogs are just windows whose layout has been predetermined. The built-in dialog box window class knows how to interpret dialog box resources to create and manage the child windows that make up a dialog box. To show a dialog box modallythat is, while the dialog is visible, the parent is inaccessibleWindows provides the DialogBoxParam[6] function:

[6] The DialogBox function is merely a wrapper around DialogBoxParam, passing 0 for the dwInitParam argument.

int DialogBoxParam(                                         
  HINSTANCE hInstance,    // handle to application instance 
  LPCTSTR   lpTemplate,   // identifies dialog box template 
  HWND      hWndParent,   // handle to owner window         
  DLGPROC   lpDialogFunc, // pointer to dialog box procedure
  LPARAM    dwInitParam); // initialization value           

The result of the DialogBoxParam function is the command that closed the dialog, such as IDOK or IDCANCEL. To show the dialog box modelesslythat is, the parent window is still accessible while the dialog is showingthe CreateDialogParam[7] function is used instead:

[7] Likewise, CreateDialog is a wrapper around the CreateDialogParam function.

HWND CreateDialogParam(                                     
  HINSTANCE hInstance,    // handle to application instance 
  LPCTSTR   lpTemplate,   // identifies dialog box template 
  HWND      hWndParent,   // handle to owner window         
  DLGPROC   lpDialogFunc, // pointer to dialog box procedure
  LPARAM    dwInitParam); // initialization value           

Notice that the parameters to CreateDialogParam are identical to those to DialogBoxParam, but the return value is different. The return value from Create-DialogParam represents the HWND of the new dialog box window, which will live until the dialog box window is destroyed.

Regardless of how a dialog is shown, however, developing a dialog is the same. First, you lay out a dialog resource using your favorite resource editor. Second, you develop a dialog box procedure (DlgProc). The WndProc of the dialog box class calls the DlgProc to give you an opportunity to handle each message (although you never have to call DefWindowProc in a DlgProc). Third, you call either DialogBoxParam or CreateDialogParam (or one of the variants) to show the dialog. This is the same kind of grunt work we had to do when we wanted to show a window (that is, register a window class, develop a WndProc, and create the window). And just as ATL lets us work with CWindow-derived objects instead of raw windows, ATL also lets us work with CDialogImpl-derived classes instead of raw dialogs.

Showing a Dialog

CDialogImpl provides a set of wrapper functions around common dialog operations (such as DialogBoxParam and CreateDialogParam):

template <class T, class TBase /* = CWindow */>              
class ATL_NO_VTABLE CDialogImpl :                            
    public CDialogImplBaseT< TBase > {                       
public:                                                      
    // modal dialogs                                         
    INT_PTR DoModal(HWND hWndParent = ::GetActiveWindow(),   
        LPARAM dwInitParam = NULL) {                         
        BOOL result;                                         
                                                             
        result = m_thunk.Init(NULL,NULL);                    
        if (result == FALSE) {                               
            SetLastError(ERROR_OUTOFMEMORY);                 
            return -1;                                       
        }                                                    
                                                             
        _AtlWinModule.AddCreateWndData(&m_thunk.cd,          
            (CDialogImplBaseT< TBase >*)this);               
                                                             
        return ::DialogBoxParam(                             
            _AtlBaseModule.GetResourceInstance(),            
            MAKEINTRESOURCE(static_cast<T*>(this)->IDD),     
            hWndParent, T::StartDialogProc, dwInitParam);    
    }                                                        
                                                             
    BOOL EndDialog(int nRetCode) {                           
        return ::EndDialog(m_hWnd, nRetCode);                
    }                                                        
                                                             
    // modeless dialogs                                      
    HWND Create(HWND hWndParent, LPARAM dwInitParam = NULL) {
        BOOL result;                                         
                                                             
        result = m_thunk.Init(NULL,NULL);                    
        if (result == FALSE) {                               
            SetLastError(ERROR_OUTOFMEMORY);                 
            return NULL;                                     
        }                                                    
                                                             
        _AtlWinModule.AddCreateWndData(&m_thunk.cd,          
        (CDialogImplBaseT< TBase >*)this);                   
                                                             
    HWND hWnd = ::CreateDialogParam(_AtlBaseModule.          
                  GetResourceInstance(),                     
        MAKEINTRESOURCE(static_cast<T*>(this)->IDD),         
        hWndParent, T::StartDialogProc, dwInitParam);        
    return hWnd;                                             
    }                                                        
                                                             
    // for CComControl                                       
    HWND Create(HWND hWndParent, RECT&,                      
        LPARAM dwInitParam = NULL)                           
    {                                                        
        return Create(hWndParent, dwInitParam);              
    }                                                        
                                                             
    BOOL DestroyWindow()                                     
    {                                                        
        return ::DestroyWindow(m_hWnd);                      
    }                                                        
};                                                           

A couple of interesting things are going on in this small class. First, notice the use of the thunk. ATL sets up a thunk between Windows and the ProcessWindowMessage member function of your CDialogImpl-based objects, just as it does for CWindowImpl-based objects. In addition to all the tricks that WindowProc performs (see the section "The Window Procedure," earlier in this chapter), the static member function CDialogImpl::DialogProc manages the weirdness of DWL_MSGRESULT. For some dialog messages, the DlgProc must return the result of the message. For others, the result is set by calling SetWindowLong with DWL_MSGRESULT. And although I can never remember which is which, ATL can. Our dialog message handlers need only return the value; if it needs to go into the DWL_MSGRESULT, ATL puts it there.

Something else interesting to notice is that CDialogImpl derives from CDialogImplBaseT, which provides some other helper functions:

template <class TBase>                                           
class ATL_NO_VTABLE CDialogImplBaseT :                           
  public CWindowImplRoot<TBase> {                                
public:                                                          
  virtual WNDPROC GetDialogProc() { return DialogProc; }         
  static LRESULT CALLBACK StartDialogProc(HWND, UINT,            
    WPARAM, LPARAM);                                             
  static LRESULT CALLBACK DialogProc(HWND, UINT, WPARAM, LPARAM);
  LRESULT DefWindowProc() { return 0; }                          
  BOOL MapDialogRect(LPRECT pRect) {                             
    return ::MapDialogRect(m_hWnd, pRect);                       
  }                                                              
  virtual void OnFinalMessage(HWND /*hWnd*/) {}                  
};                                                               

Again, notice the use of the StartDlgProc to bootstrap the thunk and the DialogProc function that actually does the mapping to the ProcessWindowMessage member function. Also notice the DefWindowProc member function. Remember, for DlgProcs, there's no need to pass an unhandled message to DefWindowProc. Because the message-handling infrastructure of ATL requires a DefWindowProc, the CDialogImplBaseT class provides an inline, do-nothing function that the compiler is free to toss away.

More useful to the dialog developer are the CDialogImplBaseT member functions MapDialogRect and OnFinalMessage. MapDialogRect is a wrapper around the Windows function MapDialogRect, which maps dialog-box units to pixels. Finally, OnFinalMessage is called after the last dialog message has been processed, just like the CWindowImpl::OnFinalMessage member function.

You might wonder how far the inheritance hierarchy goes for CDialogImpl. Refer again to Figure 10.1. Notice that CDialogImpl ultimately derives from CWindow, so all the wrappers and helpers that are available for windows are also available to dialogs.

Before we get too far away from CDialogImpl, notice where the dialog resource identifier comes from. The deriving class is required to provide a numeric symbol called IDD indicating the resource identifier. For example, assuming a resource ID of IDD_ABOUTBOX, a typical "about" box would be implemented like this:

class CAboutBox : public CDialogImpl<CAboutBox> {
public:
BEGIN_MSG_MAP(CAboutBox)
  MESSAGE_HANDLER(WM_INITDIALOG, OnInitDialog)
  COMMAND_ID_HANDLER(IDOK, OnOK);
END_MSG_MAP()

  enum { IDD = IDD_ABOUTBOX };

private:
  LRESULT OnInitDialog(UINT, WPARAM, LPARAM, BOOL&) {
    CenterWindow();
    return 1;
  }
  LRESULT OnOK(WORD, UINT, HWND, BOOL&) {
    EndDialog(IDOK);
    return 0;
  }
};

CAboutBox has all the elements. It derives from CDialogImpl, has a message map, and provides a value for IDD that indicates the resource ID. You can add an ATL dialog object from the Add Item option in Visual Studio, but it's not difficult to do by hand.

Using a CDialogImpl-derived class is a matter of creating an instance of the class and calling either DoModal or Create:

LRESULT CMainWindow::OnHelpAbout(WORD, WORD, HWND, BOOL&) {
  CAboutBox dlg;
  dlg.DoModal();
  return 0;
}

Simple Dialogs

For simple modal dialogs, such as "about" boxes, that don't have interaction requirements beyond the standard buttons (such as OK and Cancel), ATL provides CSimpleDialog:

template <WORD t_wDlgTemplateID, BOOL t_bCenter /* = TRUE */>    
class CSimpleDialog : public CDialogImplBase {                   
public:                                                          
    INT_PTR DoModal(HWND hWndParent = ::GetActiveWindow()) {     
        _AtlWinModule.AddCreateWndData(&m_thunk.cd,              
           (CDialogImplBase*)this);                              
                                                                 
        INT_PTR nRet =                                           
            ::DialogBox(_AtlBaseModule.GetResourceInstance(),    
                MAKEINTRESOURCE(t_wDlgTemplateID), hWndParent,   
                StartDialogProc);                                
        m_hWnd = NULL;                                           
        return nRet;                                             
    }                                                            
                                                                 
    typedef CSimpleDialog<t_wDlgTemplateID, t_bCenter> thisClass;
    BEGIN_MSG_MAP(thisClass)                                     
        MESSAGE_HANDLER(WM_INITDIALOG, OnInitDialog)             
        COMMAND_RANGE_HANDLER(IDOK, IDNO, OnCloseCmd)            
    END_MSG_MAP()                                                
                                                                 
    LRESULT OnInitDialog(UINT, WPARAM, LPARAM, BOOL&) {          
        // initialize controls in dialog with                    
        // DLGINIT resource section                              
        ExecuteDlgInit(t_wDlgTemplateID);                        
        if(t_bCenter)                                            
            CenterWindow(GetParent());                           
        return TRUE;                                             
    }                                                            
                                                                 
    LRESULT OnCloseCmd(WORD, WORD wID, HWND, BOOL& ) {           
        ::EndDialog(m_hWnd, wID);                                
        return 0;                                                
    }                                                            
};                                                               

Notice that the resource ID is passed as a template parameter, as is a flag indicating whether the dialog should be centered. This reduces the definition of the CAboutBox class to the following type definition:

typedef CSimpleDialog<IDD_ABOUTBOX> CAboutBox;

However, the use of CAboutBox remains the same.

The call to ExecuteDlgInit in the OnInitDialog method is worth mentioning. When you build a dialog using the Visual Studio dialog editor and you add a combo box, the contents of that combo box are not stored in the regular Dialog resource. Instead, they're stored in a custom resource type named RT_DLGINIT. The normal dialog APIs completely ignore this initialization data. ExecuteDlgInit contains the code needed to read the RT_DLGINIT resource and properly initialize any combo boxes on the dialog.

Data Exchange and Validation

Unfortunately, most dialogs aren't simple. In fact, most are downright complicated. This complication is mostly due to two reasons: writing data to child controls managed by the dialog, and reading data from child controls managed by the dialog. Exchanging data with a modal dialog typically goes like this:

  1. The application creates an instance of a CDialogImpl-derived class.

  2. The application copies some data into the dialog object's data members.

  3. The application calls DoModal.

  4. The dialog handles WM_INITDIALOG by copying data members into child controls.

  5. The dialog handles the OK button by validating that data held by child controls. If the data is not valid, the dialog complains to the users and makes them keep trying until either they get it right or they get frustrated and click the Cancel button.

  6. If the data is valid, the data is copied back into the dialog's data members, and the dialog ends.

  7. If the application gets IDOK from DoModal, it copies the data from the dialog data members over its own copy.

If the dialog is to be shown modelessly, the interaction between the application and the dialog is a little different, but the relationship between the dialog and its child controls is the same. A modeless dialog sequence goes like this (differences from the modal case are shown in italics):

  1. The application creates an instance of a CDialogImpl-derived class.

  2. The application copies some data into the dialog object's data members.

  3. The applications calls Create.

  4. The dialog handles WM_INITDIALOG by copying data members into child controls.

  5. The dialog handles the Apply button by validating that data held by child controls. If the data is not valid, the dialog complains to the users and makes them keep trying until either they get it right or they get frustrated and click the Cancel button.

  6. If the data is valid, the data is copied back into the dialog's data members and the application is notified[8] to read the updated data from the dialog.

    [8] A custom window message sent to the dialog's parent is excellent for this duty.

  7. When the application is notified, it copies the data from the dialog data members over its own copy.

Whether modal or modeless, the dialog's job is to exchange data between its data members and the child controls, and to validate it along the way. MFC has something called DDX/DDV (Dialog Data eXchange/Dialog Data Validation) for just this purpose. ATL has no such support, but it turns out to be quite easy to build yourself. For example, to beef up our standalone windows application sample, imagine a dialog that allows us to modify the display string, as shown in Figure 10.5.

Figure 10.5. A dialog that needs to manage data exchange and validation


The CDialogImpl-based class looks like this:

class CStringDlg : public CDialogImpl<CStringDlg> {
public:
  CStringDlg() { *m_sz = 0; }

BEGIN_MSG_MAP(CStringDlg)
  MESSAGE_HANDLER(WM_INITDIALOG, OnInitDialog)
  COMMAND_ID_HANDLER(IDOK, OnOK)
  COMMAND_ID_HANDLER(IDCANCEL, OnCancel)
END_MSG_MAP()

  enum { IDD = IDD_SET_STRING };
  TCHAR m_sz[64];

private:
  bool CheckValidString() {
    // Check the length of the string
    int cchString =
      ::GetWindowTextLength(GetDlgItem(IDC_STRING));
    return cchString ? true : false;
  }

  LRESULT OnInitDialog(UINT, WPARAM, LPARAM, BOOL&) {
    CenterWindow();

    // Copy the string from the data member
    // to the child control (DDX)
    SetDlgItemText(IDC_STRING, m_sz);

    return 1; // Let dialog manager set initial focus
  }

  LRESULT OnOK(WORD, UINT, HWND, BOOL&) {
    // Complain if the string is of zero length (DDV)
    if( !CheckValidString() ) {
      MessageBox("Please enter a string", "Hey!");
      return 0;
    }
   // Copy the string from the child control
   // to the data member (DDX)
   GetDlgItemText(IDC_STRING, m_sz, lengthof(m_sz));

   EndDialog(IDOK);
   return 0;
  }

  LRESULT OnCancel(WORD, UINT, HWND, BOOL&) {
    EndDialog(IDCANCEL);
    return 0;
  }
};

In this example, DDX-like functionality happens in OnInitDialog and OnOK. OnInitDialog copies the data from the data member into the child edit control. Likewise, OnOK copies the data from the child edit control back to the data member and ends the dialog if the data is valid. The validity of the data (DDV-like) is checked before the call to EndDialog in OnOK by calling the helper function CheckValidString. I decided that a zero-length string would be too boring, so I made it invalid. In this case, OnOK puts up a message box and doesn't end the dialog. To be fair, MFC would have automated all this with a macro-based table, which makes handling a lot of DDX/DDV chores easier, but ATL certainly doesn't prohibit data exchange or validation. The WTL library mentioned earlier also has rich DDX/DDV support.

I can do even better in the data validation area with this example. This sampleand MFC-based DDX/DDVvalidates only when the user presses the OK button, but sometimes it's handy to validate as the user enters the data. For example, by handling EN_CHANGE notifications from the edit control, I can check for a zero-length string as the user enters it. If the string ever gets to zero, disabling the OK button would make it impossible for the user to attempt to commit the data at all, making the complaint dialog unnecessary. The following updated sample shows this technique:

class CStringDlg : public CDialogImpl<CStringDlg> {
public:
  ...
BEGIN_MSG_MAP(CStringDlg)
  ...
  COMMAND_HANDLER(IDC_STRING, EN_CHANGE, OnStringChange)
END_MSG_MAP()
private:
  void CheckValidString() {
    // Check the length of the string
    int cchString =
      ::GetWindowTextLength(GetDlgItem(IDC_STRING));

    // Enable the OK button only if the string
    // is of non-zero length
    ::EnableWindow(GetDlgItem(IDOK), cchString ? TRUE : FALSE);
  }

  LRESULT OnInitDialog(UINT, WPARAM, LPARAM, BOOL&) {
    CenterWindow();

    // Copy the string from the data member
    // to the child control (DDX)
    SetDlgItemText(IDC_STRING, m_sz);

    // Check the string length (DDV)
    CheckValidString();

    return 1; // Let dialog manager set initial focus
  }

LRESULT OnOK(WORD, UINT, HWND, BOOL&) {
    // Copy the string from the child control to the data member (DDX)
    GetDlgItemText(IDC_STRING, m_sz, lengthof(m_sz));

    EndDialog(IDOK);
    return 0;
  }

  LRESULT OnStringChange(WORD, UINT, HWND, BOOL&) {
    // Check the string length each time it changes (DDV)
    CheckValidString();
    return 0;
  }

  ... // The rest is the same
};

In this case, notice that OnInitDialog takes on some DDV responsibilities and OnOK loses some. In OnInitDialog, if the string starts with a zero length, the OK button is immediately disabled. In the OnStringChange handler for EN_CHANGE, as the text in the edit control changes, we revalidate the data, enabling or disabling the OK button as necessary. Finally, we know that if we reach the OnOK handler, the OK button must be enabled and the DDV chores must already have been done. Neither ATL nor MFC can help us with this kind of DDV, but neither hinders us from providing a UI that handles both DDX and DDV in a friendly way.


previous page
next page
Converted from CHM to HTML with chm2web Pro 2.75 (unicode)