Creating a Reusable Dialog Class without MFC

from Germany.

Environment: Visual C++ 6

Introduction

In this article, I am going to explain how it is possible to create a Dialog Class for the Win32 API that will be nearly as flexible as the CDialog object of the MFC framework. As in my first article here on codeguru.com, I want to show the way to the final solution and not just some copy & paste-ready source code with some short explanation. So, be prepared for a long article.

Dialogs with the Win32 API

Working with modal dialogs, which are often used to let the end-user change options or display information about the programmer (essential for the ego of software developers), can be a real pain to work with if your application contains more than one dialog. You need to implement a dialog procedure that handles the messages the dialog could receive for every single dialog. Here is the source code for a simple modal dialog, assuming that the dialog is defined in the resource file as a template.


DialogBox(GetModuleHandle(NULL), /// instance handle
MAKEINTRESOURCE(IDD_ABOUT), /// dialog box template
hParentWnd, /// handle to parent
/// window

DialogProc); /// pointer to dialog
/// procedure

BOOL CALLBACK DialogProcStatic(HWND hDlg, UINT message,
WPARAM wParam, LPARAM lParam)
{
switch(message)
{
case WM_INITDIALOG:
{
/// initialize dialog
}
break;

case WM_COMAND:
{
switch(LOWORD(wParam))
{
case IDOK:
{
/// handle OK button click
EndDialog(hDlg, IDOK);
}
break;

case IDCANCEL:
{
/// handle Cancel button click
EndDialog(hDlg, IDCANCEL);
}
break;
}
}
break;
}

return TRUE;
}

If you are developing complex software, it is often necessary to have a lot of dialogs in your application. But, if you are using the above-mentioned way, the overview and efficiency of your code will suffer.

The First Approach

During last Christmas I got to know the Microsoft Foundation Classes (MFC). I was astonished how things in the Win32 API are simplified and encapsulated in classes. Especially, the dialogs are really easy to use. Here is a code snippet that displays a dialog with the help of a class that is derived from the MFC class CDialog.


CAboutDlg AboutDlg;
AboutDlg.doModal();

Everyone who is experienced with MFC knows that this not all of the code. You first have to derive a new class from the MFC Class CDialog, but, after that, it’s very easy to handle messages and add further functionality to the dialog. After some experiments with the MFC, I dove back into the Win32 API again to create a reusable Dialog class that is similiar to the MFC approach. The source code of my approach should be easy to understand for every intermediate to experienced programmer. Here is the first sketch of the Dialog class with the name CBaseDialog.


class CBaseDialog
{
public:

CBaseDialog(int nResId, HWND hParent=NULL);
~CBaseDialog();

int DoModal(void);

static BOOL CALLBACK DialogProcStatic(HWND hDlg, UINT message,
WPARAM wParam,
LPARAM lParam);

private:

HWND m_hParent;
int m_nResId;

HWND m_hWindow;
};

The first method of the class is the constructor that takes the resource id of the dialog and, optionally, a handle to the parent window of the dialog. The next important function is DoModal() because it will display the dialog and return, for example, the id of the button that has been clicked. This function will not return until the dialog is dismissed. The following function, DialogProcStatic, is very important for the class because it handles messages that our dialog receives. This has to be declared as static because it’s a callback function. But the static declaration leads us to another problem because it is not possible to access members of the class the conventional way.

Improvements

After some brainstorming, I thought that it must be possible to access class members in a static function somehow. But, how do I do it…?

Here is the solution: You can store the “this-pointer” of a class in a variable of the type long. So, I added a static variable of the type long with the name m_lSaveThis. This variable will be used to store the “this pointer” of our class during the construction of the class because the “this pointer” is not available in our static DialogProcStatic().

m_lSaveThis = (long)this;

I changed the class member m_hWindow to static because the DialogProcStatic() passes the window handle as an argument. After that, I added another function with the name DialogProc(), which is not static. This function will be called in the DialogProcStatic() function; I will show you how in a few seconds. Here is the implementation of the static DialogProc() function.


BOOL CALLBACK CBaseDialog::DialogProcStatic(HWND hDlg,
UNIT message,
WPARAM wParam,
LPARAM lParam)
{
if(m_hWindow == NULL)
{
m_Window = hDlg;
}

CBasicDialog *pThis = (CBasicDialog*)m_lSaveThis;

return(pThis->DialogProc(hDlg, message, wParam, lParam));
}

As you can see from the source code, the code first checks whether the static window handle equals NULL. If this is the case, the DialogProc() function is called the first time so we have the window handle of the dialog to our static handle. After that, we cast the earlier stored this pointer (m_lSaveThis) to a CBaseDialog Pointer. Now, the code executes the non-static DialogProcMsg() and returns its return value. Now, it’s possible to handle messages and access class members because the DialogProc() function is not static and can access class members due to this fact. But, there are some more improvements I have to offer.

Efficient Message Handling

While testing the dialog class, I thought about how I could design it to be as flexible as possible when deriving new classes from it in view of the message handling. I first tried to use virtual functions in the base class (e.g. virtual void On_WM_INITDIALOG()), but then I saw that they are not suitable for my approach because I would never know which messages a newly derived dialog class should handle during the design of the dialog base class. I will show you a small example to make clear what I mean.


CBaseDialog
has a virtual function OnInitDialog();
has a virtual function OnCommand();

CDerivedDialog
overrides OnCommand();
should handle WM_User+0x200

Here you can see the problem. I did not know that a derived dialog class should handle the user defined message, so I need to add another virtual member function to the base class (for example, On_WM_USER_200()), but that’s exactly what I do not want because, if you are using the dialog base class in a DLL, you have to rebuild the DLL. It would end up in a big mess if you have more than one dialog that should handle user-defined messages. I wanted to create a message-handling system similiar to the MFC. So, I came up with the idea to create a message map that contains the message (for example, WM_INITDIALOG) as the key and the message handler (for example, OnInitDialog()) as the link to the key or data.

Furthermore, the DialogProc should not contain a huge switch/case construct because I want to get a better overview of the source code. The code should just ask the message map whether there’s an appropiate message handler for the message and, if yes, execute it and return its return value. You need to know function pointers and some typecasting to add this implementation to the base class. I used the STL Map class for the message map, but you could also use a linked list or something similiar. Here is my implementation:


class CBaseDialog;

typedef void(CBaseDialog::*fpMessageHandler)(HWND hDlg,
WPARAM wParam,
LPARAM lParam);

struct t_MessageEntry
{
fpMessageHandler MsgHandler;

};

At first, I added the line “class CBaseDialog;” to the beginning of the class declaration because, if I didn’t, I would get an undefined type error in the next lines. Then, I declared a function pointer type that can point to member functions of CBaseDialog. After that, I declared a new type/struct with the name t_MessageEntry that contains the function pointer.

I created this type because it’s much easier to pass a struct to the STL Map than a pure function pointer. Another benefit of this new type is that you can add some variables to it later without heavy modification to the source code. After that, the STL Map comes into play. I am not really experienced with STL, so don’t expect professional advice, but I think I used it correctly because I got some of the code from an MSDN sample.


std::map<UINT,t_MessageEntry> m_MessageMap;
std::map<UINT,t_MessageEntry>::iterator m_MessageMapIterator;

Those two lines of code probably need some explanation but I can just tell you that the code instructs our message map to use an UINT as the key and the t_MessageEntry as the link to the key. I am going to show how it works later, in the actual implemantation. The next addition to the code is a function that simplifies the process of associating messages to message handlers. This is the prototype of the function.


void AddMessageHandler(UINT MessageId, void(CBaseDialog::*Handler)
(HWND hDlg,WPARAM wParam,LPARAM lParam));

If you look at this line of code, you will see that another problem will be coming up later if you want to add member functions of a derived dialog class. The function pointer (second argument of the function) only takes member functions of the CBaseDialog class, so you need to some typecasting to get this to work.

Now, to the implementation of the CBaseDialog class. The changed things are the non-static DialogProc() function and the newly added AddMessageHandler() function. As I mentioned above, the DialogProc() does not consist of a switch/case construct but of some lines of code with the STL Map.


BOOL CALLBACK CBaseDialog::DialogProc(HWND hDlg, UINT message,
WPARAM wParam, LPARAM lParam)
{
m_MsgHandlerIterator = m_MsgHandler.find(message);

if(m_MsgHandlerIterator == m_MsgHandler.end())
{
return FALSE;
}
else
{
t_MessageEntry FuncPointer = (*m_MsgHandlerIterator).second;

void (CBaseDialog::*MessageHandlerBase)
(HWND hDlg,WPARAM wParam,LPARAM lParam);

MessageHandlerBase = FuncPointer.Handler;

return((this->*MessageHandlerBase)(hDlg, wParam, lParam));
}
}

Here you can see that the code checks whether there is a message handler for the received message available. If not, it returns FALSE to carry out default message processing for the dialog. By returning FALSE, the dialog class calls DefDlgProc() automatically. But, if the code has found a message entry, it calls the message handler that is linked to the message.

The AddMessageHandler() function:


void CBasicDialog::AddMessageHandler(UINT message, void
(CBasicDialog::*MsgHandler)
(HWND hDlg,WPARAM wParam,
LPARAM lParam))
{
t_MessageEntry MessageEntry;
MessageEntry.MsgHandler = MsgHandler;

m_MsgHandler.insert(std::map<UINT,t_MessageEntry>
::value_type(message, MessageEntry));
}

Here you can see how a message and the linked message handler will be added to the message. Look up the MSDN sample if you have problems with the code.

Macros

The CBaseDialog class is almost done. As the last step, I designed some macros that simplfly the use of the class. Here is a list of the macros, but you should look at the source code to see where the macros will be placed.

1. DECLARE_MESSAGE_HANDLER(<name of the derived class>)

Code:


void AddHandler(UINT MessageId, void(derived::*Handler)
(HWND hDlg,WPARAM wParam,LPARAM lParam));
void HandleManager(void);

Position: Definition of Derived Class
This macro will be replaced by two function prototypes that are responsible for adding all the message entries to the message map and typecasting between the different function pointers.

2. IMPLEMENT_MESSAGE_HANDLER(<name of the base class>,<name of the derived class>)

Code:


void derived::AddHandler(UINT MessageId, void(derived::*Handler)
(HWND hDlg,WPARAM wParam,LPARAM lParam))
{
AddMessageHandler(MessageId, (void(base::*)
(HWND hDlg,WPARAM wParam,LPARAM lParam))
Handler);
}

Position: Implementation of the Derived Class
This macro will be replaced by the function that is prototyped with the first macro and which does the typecasting.

3. BEGIN_MESSAGE_MAP(<name of the derived class>)

Code:

void derived::HandleManager(void){

Position: Implementation of the Derived Class
This macro will start the HandleMgr() function where all the message entries will be added to the message map.

4. ADD_MESSAGE_HANDLER()

Code:

AddHandler(message, handler);

Position: Implementation of Derived Class
This macro must be placed after the third macro. It adds a message entry to the message map.

5. END_MESSAGE_MAP()

Code:

}

Position: Implementation of Derived Class
This macro must be called after the fourth macro because it closes the Implementation of the HandleMgr() function.

6. ENABLE_MESSAGE_MAP()

Code:

HandleManager();

Position: Constructor of Derived Class
The last macro just executes the HandleMgr() function that adds all the message entries to the message map.

Here is a short snippet of how to use these macros, but you’d better look at the sample to see how it works and what the code looks like.


IMPLEMENT_MESSAGE_HANDLER(CBaseDialog, CAboutDlg)
BEGIN_MESSAGE_MAP(CAboutDlg)
ADD_MESSAGE_HANDLER(WM_COMMAND, OnCommand)
ADD_MESSAGE_HANDLER(WM_INITDIALOG, OnInitDialog)
END_MESSAGE_MAP()

CAboutDlg::CAboutDlg(int nResId, HWND hParent) :
CBaseDialog(nResId, hParent)
{
ENABLE_MESSAGE_MAP();
}

By the way, most of the code also can be used to encapsulate the main window into a class, but it should be very easy for you to archieve that by using the fundamentals of this article. I hope that I have helped some people with the article because I saw a lot of people on the message boards who wanted to know how it works.

I am looking forward to your comments, critiques, and questions about my article. If you want to contact me, write a message to daniel-bartsch@arcor.de.

Downloads

Download demo project – 17 Kb

Download demo executable – 16 Kb

More by Author

Get the Free Newsletter!

Subscribe to Developer Insider for top news, trends & analysis

Must Read