Êíèãà: Windows API Tutorials

Dialog Box

Dialog Box

Dialog box is for a Windows program what a function call is for a C program. First, a Windows programs passes some data to the dialog box in order to initialize it. Next, the dialog box solicits information from the user. When the user decides that the program's curiosity has been satisfied, he or she clicks the OK button. The new data is then returned back to the program.

A dialog box is usually spiked with all kinds of controls (widgets, gadgets, gizmos, whatever you call them). Dialog box code interacts with these gizmos by sending and receiving messages in its Dialog Box Procedure. There is a lot of common code in the implementation of various dialog boxes. We would like to write and test this code only once and then keep reusing it.

What varies from dialog to dialog is:

• The kind of data that is passed to and retrieved from the dialog box

• The kind of gizmos within the dialog box and

• The interactions between them.

This variability can be encapsulated in two custom client classes: the argument list class and the dialog controller class.

The argument list class encapsulates the in and out arguments and provides access for retrieving and changing them. The definition and the implementation of this class is under complete control of the client (the programmer who reuses our code).

The dialog controller class contains control objects for all the gizmos in the dialog and implements interactions between them. The client is supposed to derive this class from the abstract DlgController class and implement all its pure virtual methods.

The rest is just glue. We need to instantiate a template CtrlFactory class that is used by our generic implementation of the ModalDialog class. The controller factory creates the appropriate client-defined controller and returns a polymorphic pointer to it. Fig 1. shows the relationships between various classes involved in the implementation of a dialog box.


Fig 1. Classes involved in the dialog box design pattern.

Let's start with the client code. In our generic program we need a dialog box that lets the user edit a string. Using a resource editor, we create a dialog template that contains an edit control and two buttons, OK and CANCEL. The resource id of this dialog is IDD_EDITDIALOG. Next we define our own argument list class called EditorData and a special controller class called EditorCtrl. When the user selects the Edit menu item, the following code is executed:

void Controller::Edit (HWND hwnd) {
 EditorData data (_model.GetText ());
 ControllerFactory <EditorCtrl, EditorData> factory (& data);
 ModalDialog dialog (_hInst, hwnd, IDD_EDITDIALOG, & factory);
 if (dialog.IsOk ()) {
  _model.SetText (data.GetName ());
  // Force repaint
  InvalidateRect (hwnd, 0, TRUE);
 }
}

First, the EditorData object is created and initialized with a string. Next, the ControllerFactory template is instantiated. We parametrize it with the two client classes, EditorCtrl and EditorData. The factory object is initialized with a pointer to our data. Next, the ModalDialog object is created. it takes the pointer to our factory as an argument. it uses it to create the controller object and to retrieve data from the argument list. After the interaction with the user is done, we check whether the user blessed the results by clicking the ok button, and if so, we retrieve the result from the editor data object and incorporate it in our program. This is pretty much the standard way to set up a dialog box.

The EditorData class in our example is pretty simple.

class EditorData {
public:
 enum { maxLen = 128 };
 EditorData (char const * name) {
  SetName (name);
 }
 BOOL IsNameOK () { return (_name[0] != ''); }
 void SetName (char const *name) {
  strcpy (_name, name);
 }
 char const *GetName () { return _name; }
private:
 char _name [maxLen];
};

The controller class, EditorCtrl, is where all the action is. First of all, it has the Edit control embedded in it. This object is responsible for the interaction with the edit gizmo embedded in the dialog box. The gizmo has the id IDC_NAME_EDIT that we gave it in the resource editor. Second of all, the controller stores a pointer to EditorData. This pointer is retrieved from the base class DlgController. Three virtual methods of DlgController have to be overridden in our EditorControl. These are OnInitDialog, which is called immediately after the dialog has been initialized, OnCommand, which is called whenever any dialog box gizmo sends us a command and, finally, OnNotify, which is used by the new Windows95 controls.

class EditorCtrl : public DlgController {
public:
 EditorCtrl (HWND hwndDlg, EditorData *argList) : DlgController (argList), _nameEdit (hwndDlg, IDC_NAME_EDIT) {
  _dlgData = argList;
 }
 void OnInitDialog (HWND hwnd);
 bool OnCommand (HWND hwnd, int ctrlID, int notifyCode);
 bool OnNotify (HWND hwnd, int idCtrl, NMHDR *hdr);
private:
 Edit  _nameEdit;
 EditorData *_dlgData;
};

In the OnInitDialog method we retrieve the string that was passed in EditorData and use it to initialize the edit control.

void EditorCtrl::OnInitDialog (HWND hwnd) {
 char const * name = _dlgData->GetName ();
 _nameEdit.SetString (name);
}

OnCommand receives commands from various gizmos. The gizmos are identified by their control id. For instance, if the control id is IDC_NAME_EDIT, it means that something happend to the edit control. In our implementation we are a little overzelous and we react to every change by copying the whole string into EditorData object. There are cases though, when you need to keep validating the string or displaying it in some other control and then you really have to react to every change message.

When the user clicks the OK button, we get a command with the IDOK control id. We verify the string and, if it's okay, we end the dialog passing TRUE as the return code. When the control id is IDCANCEL (from the Cancel button) we end the dialog with the FALSE return code.

The OnNotify method does nothing in the case of pre-Windows95 controls, such as the edit control and the buttons.

bool EditorCtrl::OnCommand (HWND hwnd, int ctrlID, int notifyCode) {

 switch (ctrlID) {
 case IDC_NAME_EDIT:
  if (_nameEdit.IsChanged (notifyCode)) {
   char nameBuf [EditorData::maxLen];
   int len = _nameEdit.GetLen();
   if (len < EditorData::maxLen) {
    _nameEdit.GetString (nameBuf, sizeof (nameBuf));
   _dlgData->SetName (nameBuf);
   }
   return true;
  }
  break;
 case IDOK:
  if (_dlgData->IsNameOK ()) {
   EndDialog(hwnd, TRUE);
  } else {
   MessageBox (hwnd, "Please, enter valid name", "Name Editor", MB_ICONINFORMATION | MB_OK);
  }
  return true;
 case IDCANCEL:
  EndDialog(hwnd, FALSE);
  return true;
 }
 return false;
}
bool EditorCtrl::OnNotify (HWND hwnd, int idCtrl, NMHDR *hdr) {
 return false;
}

Now that you know how to use it, let's have a look at the implementation of the whole dialog box pattern. The controller factory is a very simple template class. All it does it to accept the generic argument list and store it as a void pointer. Only the client-defined controller knows what the actual class of the argument list is and it will perform a cast (see the constructor of EditorCtrl).

The code common to all controller factories is isolated in the class CtrlFactory from which the actual template is derived. The template overrides the MakeController method to create a new controller of the client-defined class ActualCtrl. Notice however that the method returns ActualCtrl as a pointer to its base class DlgController and that's what the rest of the implementation sees.

class CtrlFactory{
public:
 CtrlFactory (void *argList) : _argList (argList) {}
 virtual DlgController * MakeController (HWND hwndDlg) = 0;
protected:
 void *_argList;
};
template <class ActualCtrl, class ActualArgList>
class ControllerFactory : public CtrlFactory {
public:
 ControllerFactory (void *argList) : CtrlFactory (argList) {}
 DlgController * MakeController (HWND hwndDlg) {
  return new ActualCtrl (hwndDlg, (ActualArgList *) GetArgList ());
 }
};

Here is the definition of the abstract class DlgController that is used as a base for all client-defined controller classes. We've already seen how this derivation works in the case of the client class EditorCtrl.

class DlgController{
public:
 virtual ~DlgController () {}
 // In case derived class overrides
 virtual void OnInitDialog (HWND hwnd) = 0;
 virtual bool OnCommand (HWND hwnd, int ctrlID, int notifyCode) = 0;
 virtual bool OnNotify (HWND hwnd, int idCtrl, NMHDR *hdr) = 0;
};

The central reusable piece of software is the ModalDialog class. It does all the work in its constructor by calling the DialogBoxParam Windows API. The parameter that we are passing to the dialog box (actually, to its dialog procedure) is the pointer to the controller factory. The dialog procedure is defined as a static method (no this pointer — the dialog procedure is callable from Windows, so it has no access to the this pointer).

class ModalDialog{
public:
 ModalDialog (HINSTANCE hInst, HWND hwnd, int dlgResource, CtrlFactory *ctrlFactory)     {
  _result = DialogBoxParam (hInst, MAKEINTRESOURCE (dlgResource), hwnd, (DLGPROC) ModalDialogProc, (LPARAM) ctrlFactory);
 }
 bool IsOk () const { return (_result == -1)? false: _result != 0; }
 static BOOL CALLBACK ModalDialogProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam);
private:
 int   _result;
};

Finally, the dialog procedure, common to all types of dialogs, is implemented to respond to three kinds of messages: WM_INITDIALOG, WM_COMMAND and WM_NOTIFY. Actually, all it does is to direct these messages to the controller object. It obtains the pointer to the polymorphic controller object for the first time by calling the MakeController method of the factory.

Notice that, from that point on, we let Windows keep track of the pointer to the controller. We store it as GWL_USERDATA — a special long that is associated with every window, in particular with our dialog, and accessible through its window handle.

template <class T>
inline T GetWinLong (HWND hwnd, int which = GWL_USERDATA) {
 return reinterpret_cast<T> (::GetWindowLong (hwnd, which));
}
template <class T>
inline void SetWinLong (HWND hwnd, T value, int which = GWL_USERDATA) {
 ::SetWindowLong (hwnd, which, reinterpret_cast<long> (value));
}
 

We have to be careful, though. First of all, we need to deallocate the controller after we are done. We do it when processing WM_DESTROY.

Second of all, Windows has this unfortunate habit of sending WM_COMMAND and WM_NOTIFY messages before WM_INITDIALOG and after WM_DESTROY. What can I say — if I were the manager of the poor schmuck who's responsible for this "feature" things would have been different. As it is, we have to protect ourselves by testing if ctrl is non-zero before calling OnCommand and OnNotify.

BOOL CALLBACK ModalDialog::ModalDialogProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) {
 DlgController * ctrl = GetWinLong<DlgController *> (hwnd);
 switch (message) {
 case WM_INITDIALOG:
   {
    CtrlFactory *ctrlFactory = reinterpret_cast<CtrlFactory *> (lParam);
    ctrl = ctrlFactory->MakeController (hwnd);
    SetWinLong<DlgController *> (hwnd, ctrl);
    ctrl->OnInitDialog (hwnd);
   }
   return TRUE;
 case WM_COMMAND:
   if (ctrl && ctrl->OnCommand (hwnd, LOWORD(wParam), HIWORD (wParam))) {
    return TRUE;
   }
   break;
 case WM_NOTIFY:
  if (ctrl && ctrl->OnNotify (hwnd, wParam, (NMHDR *)lParam))  return TRUE;
  break;
 case WM_DESTROY:
  delete ctrl;
  SetWinLong<DlgController *> (hwnd, 0);
  break;
 }
 return FALSE;
}

Here's the beauty of polymorphism in action. The factory object is created by the client using a template class. This object is passed to the constructor of ModalDialog. ModalDialog passes it to the dialog procedure as a void pointer (that's because it has to go through Windows). Dialog procedure gets it inside the WM_INITDIALOG message as LPARAM. After going through the digestive tract of Windows it has to be restored to its original shape by casting it back to the pointer to CtrlFactory — the base class of all controller factories.

When we call its virtual method MakeController, we are calling the method overridden in the class template ControllerFactory. It creates a new object of the client-defined class ActualCtrl. But again, it returns this object to us disguised as a generic pointer to DlgController. So whenever we call any of the virtual methods of ctrl, we are executing client overrides defined in the class ActualCtrl. That's your polymorphism at its best: you write code using generic pointers, but when the code is executed, it is called with very specific pointers. When you call methods through these pointers, you are executing specific methods provided by the client of your code.

This is what happens to the object factory whose actual class is ControllerFactory<EditorCtrl, EditorData>

Passed to ModalDialog constructor as void *
Passed by Windows to ModalDialogProcedure as LPARAM
Cast in ModalDialogProcedure to CtrlFactory *

And this is what happens to the object data whose actual class is EditorData.

Passed to factory constructor as void *
Cast in the MakeController method of ControllerFactory<EditorCtrl, EditorData> to EditorData *
Passed to the constructor of EditCtrl as EditorData *

The object of class EditCtrl created in the MakeController method of ControllerFactory<EditorCtrl, EditorData> is returned from it as DlgController * and stored in this form as a static data member of ModalDialog.

If you have problems following my explanation, don't despair. The object oriented techniques I just described are difficult, but essential. They are called design patterns. I highly recommend reading the book by Gamma, Helm, Johnson and Vlissides called Design Patterns, Elements of Reusable Object-Oriented Software or look at the Patterns Home Page. it describes a lot of creative ways of using polymorphism, inheritance and templates to make software more reusable.

Now it's time to talk some more about the Canvas.

Îãëàâëåíèå êíèãè


Ãåíåðàöèÿ: 4.371. Çàïðîñîâ Ê ÁÄ/Cache: 3 / 0
ïîäåëèòüñÿ
Ââåðõ Âíèç