How do MFC message handlers work?
Whenever your window receives a message, MFC will call a member function of your class. But how
does MFC know what function to call?
MFC uses a technique called Message Maps. A Message Map is a table that associates messages with
functions. When you receive a message, MFC will go through your Message Map and search for a
corresponding Message Handler. I have showed in Part 1 how you add a Message Handler to the Message
Map by using ClassWizard, but what really happens code-wise?
MFC uses a large set of rather complicated macros that add the Message Map to your classes.
When you use ClassWizard to create a Message Handler, it will first add the function to your class,
and add the corresponding macro to your Message Map. For example, examine the following ClassWizard
generated WM_CLOSE handler:
Message Map: located in the class implementation
BEGIN_MESSAGE_MAP(CAboutDlg, CDialog)
ON_WM_CLOSE()
END_MESSAGE_MAP()
Function Declaration: located in the class declaration.
protected:
afx_msg void OnClose();
DECLARE_MESSAGE_MAP()
Function Implementation: located in the class implementation
void CAboutDlg::OnClose()
{
CDialog::OnClose();
}
By adding a DECLARE_MESSAGE_MAP statement to the class declaration, MFC adds the
required code to declare the message map. The BEGIN_MESSAGE_MAP tells MFC where the
Message Map begins, and identifies your class and it's base class. The reason it needs the base class
is because Message Handlers are passed through c++ inheritance, just like any other function.
END_MESSAGE_MAP obviously, tells MFC where the Message Map ends. In between these two
macros is where your declare the Message Map entry for your Message Handler. MFC has many predefined
macros, which associate messages with your member function. Take the the ON_WM_CLOSE
macro as an example: It associates the WM_CLOSE message with your OnClose()
member function. The macro takes no parameters since it always expects a function called
OnClose() which is prototyped as afx_msg void OnClose(). This
method gives you 2 advantages:
- It is easy to keep track of Message Handlers and the messages
they handle
- MFC screens out any irrelevant and will break up lParam and wParam to parameters
relevant to the message.
Also the return value is simplified, and the Message Handler is prototyped
according to the message. For example: If the value should always be zero, MFC simplifies the process
and allows you to declare the function as a void, and MFC will be responsible for returning
0. To find the name of the message handler that correlates with a given Message Handler macro you
should look it up in the MFC documentation.
There are some messages that ClassWizard doesn't support, but you can manualy add your message
handler by adding the function and Message Map macro as described above. If you add message-map
entries manually, you may not be able to edit them with ClassWizard later. If you add them outside
the bracketing comments //{{AFX_MSG_MAP(classname) and //}}AFX_MSG_MAP,
ClassWizard cannot edit them at all. Note that by the same token ClassWizard will not touch any entries
you add outside the comments, so feel free to add messages outside the comments if you do not want them
to be modified. Messages that are not recognized by ClassWizard, such as message-map ranges, must be
added outside the comments.
The all mighty ON_MESSAGE
Sometimes you will find yourself trying to handle a message that ClassWizard doesn't support,
and it doesn't have a Message Map macro. MFC has a generic macro just for this kind of situation
ON_MESSAGE. ON_MESSAGE allows you to handle any message that exists. The
prototype of Message Handlers that use ON_MESSAGE is
afx_msg LRESULT OnMessage(WPARAM wParam, LPARAM lParam);
where OnMessage is the name of your handler function. The ON_MESSAGE macro
takes 2 parameters: The address of the handler, and the message it should handle. For example: The
following statement Maps WM_GETTEXTLENGTH to OnGetTextLength():
ON_MESSAGE (WM_GETTEXTLENGTH, OnGetTextLength)
OnGetTextLength is prototyped as
afx_msg LRESULT OnGetTextLength(WPARAM wParam, LPARAM lParam);
User-defined messages
Sometimes, you will need to communicate between 2 windows in your application or between 2 windows
from different applications. An easy way to do this is by using User-defined messages. The name
"User-defined" can be confusing at first; you define a User-defined message and not the user of
your program. I have stated in Part 1 that messages are identified by numbers, and that Windows
predefines standard messages. The way of using predefined messages is to simply use a number. To
make sure that you don't conflict with the system defined messages you should use a number in the
range of WM_APP through 0xBFFF:
#define WM_DELETEALL WM_APP + 0x100
pYourDialog->SendMessage(WM_DELETEALL, 0, 0);
Handling a user-defined message is done with the
ON_MESSAGE macro:
#define WM_DELETEALL WM_APP + 0x100
ON_MESSAGE (WM_DELETEALL, OnDeleteAll)
afx_msg LRESULT OnDeleteAll(WPARAM wParam, LPARAM lParam);
LRESULT OnDeleteAll(WPARAM wParam, LPARAM lParam){
return somevalue;
}
Registered Windows Messages
The RegisterWindowMessage function is used to define a new window message that is
guaranteed to be unique throughout the system. The macro ON_REGISTERED_MESSAGE is used to
handle these messages. This macro accepts a name of a UINT variable that contains the
registered Windows message ID. For example:
class CMyWnd : public CMyParentWndClass
{
public:
CMyWnd();
afx_msg LRESULT OnFind(WPARAM wParam, LPARAM lParam);
DECLARE_MESSAGE_MAP()
};
static UINT WM_FIND = RegisterWindowMessage("YOURAPP_FIND_MSG");
BEGIN_MESSAGE_MAP(CMyWnd, CMyParentWndClass)
ON_REGISTERED_MESSAGE(WM_FIND, OnFind)
END_MESSAGE_MAP()
The range of user defined messages using this approach will be in the range 0xC000 to 0xFFFF.
And you send it using the regular SendMessage() method:
static UINT WM_FIND = RegisterWindowMessage("YOURAPP_FIND_MSG");
pFindWindow->SendMessage(WM_FIND, lParam, wParam);