Download demo project - 266 Kb
Problem
You've written some great new functionality for a CDialog
. Upon reflection,
it seems that this is functionality you can use over and over again in other projects. So you
quickly wrap it up in a base class, CMyCoolNewDlg
. You reuse this base class a
few times and then suddenly find yourself needing the same functionality for a CFormView
.
Sighing over the fact that you can't just reuse all of the code you already have, you start to cut
and paste all of the functionality from CMyCoolNewDlg
into a new
CMyCoolNewFormView
. Repeat these same steps later when you need a CDialogBar
with this functionality. Eventually, you have base classes for all of MFC's basic window types and you
feel relatively safe. Until… you have to use CXYZCompanyDlg
in a project and need this
class to have the same functionality as well. Boy, what an ugly can of worms.
Solution
So, how do we handle this problem? Actually, it's pretty simple. What we
want to do is create a base class that instead of representing a specific window
type it simply wraps event handling functionality. This base class needs some
mechanism to "hook" itself onto the event handling mechanism associated with
a window and from that point on it can process events for the window. For
specifics on how this is done, skip down to the "Programming Notes" section
below. For now, we'll just discuss how to use this base class.
Just for an example, let us suppose we have the need to restore some windows
to the same position each time they are created and destroyed. This is pretty
simple functionality, and we've all probably programmed it into specific windows
over and over again. This is a good candidate for a "pluggable" event handler.
Below I present the code for a CPersistantSize
class which will
easily allow us to add this functionality to any window.
class CPersistantSize : protected CSubclassWnd
{
public:
CPersistantSize(HWND hWnd, LPCTSTR szSection);
protected:
BEGIN_MSG_DISPATCH(CPersistantSize, CSubclassWnd)
DISPATCH_MSG(WM_DESTROY, OnDestroy)
END_MSG_DISPATCH()
void OnDestroy();
private:
CString m_sSection;
};
CPersistantSize::CPersistantSize(LPCTSTR szSection)
: m_sSection(szSection)
{
SubclassWindow(hWnd);
CRect rect;
if ((rect.left = theApp.GetProfileInt(m_sSection, "Left", -1)) == -1)
return;
if ((rect.right = theApp.GetProfileInt(m_sSection, "Right", -1)) == -1)
return;
if ((rect.top = theApp.GetProfileInt(m_sSection, "Top", -1)) == -1)
return;
if ((rect.bottom = theApp.GetProfileInt(m_sSection, "Bottom", -1)) == -1)
return;
MoveWindow(hWnd, rect.left, rect.top, rect.Width(), rect.Height(), TRUE);
}
void CPersistantSize::OnDestroy()
{
HWND hWnd = GetHandle();
CRect rect;
GetWindowRect(hWnd, &rect);
theApp.WriteProfileInt(m_sSection, "Left", rect.left);
theApp.WriteProfileInt(m_sSection, "Right", rect.right);
theApp.WriteProfileInt(m_sSection, "Top", rect.top);
theApp.WriteProfileInt(m_sSection, "Bottom", rect.bottom);
DefWindowProc();
}
That's it. With this class in hand you can modify a dialog to be persistent
by simply creating a new CPersistantSize
instance in the dialog's
OnInitDialog
(or some other appropriate message handler):
BOOL CMyDialog::OnInitDialog()
{
new CPersistantSize(GetSafeHwnd(), "MySection");
}
That's it. Now CMyDialog
will have a persistant size. This is an
extremely simple example, but you can see how you can extend the idea to other problems
using the CSubclassWnd
base class.
Message Reflection
Message reflection lets you handle messages for a control, such as WM_CTLCOLOR
,
WM_COMMAND
, and WM_NOTIFY
, within the control itself. This makes the
control more self-contained and portable.
Message reflection requires help from a control's parent, who must first "reflect" many messages
to the control before attempting to handle the message itself. MFC and ATL have their own ways of
handling message reflection, but for CSubclassWnd
we need our own mechanism for handling
this. CSubclassWnd
sets up things for message reflection for you automatically, though
you can disable this by passing an extra parameter to the SubclassWindow
method. Once
this is done the message handler will receive reflected messages through OCM_*
messages.
For instance, if you want to handle a reflected WM_CTLCOLORBTN
message, simply add a
handler for OCM_CTLCOLORBTN
. The example application that accompanies this article shows
how this is done by changing the color for a list box using the CSubclassWnd
derived class
CColorMgr
. See the source code for details.
Programming Notes
I must admit that I borrowed much of the code for this from various places. First of all, Paul
DiLascia wrote a wonderful article for Microsoft Systems Journal about this very topic called
"More Fun With MFC: DIBs, Palettes, Subclassing and a Gamut of Goodies, Part II" (this article is
now available on MSDN as well). He came up with a very similar class that he called CMsgHook
.
My implementation is quite different from his, even if the classes are very similar. In particular, the
problems I had with his implementation were that it relied heavily on MFC and paid little attention to
threading issues. I wanted my implementation to be useable even when MFC wasn't being used. Further, I
wanted to insure that you could easily and safely pass around these "message handling" objects from
thread to thread. After all, C programmers don't have to worry about threading issues when they
subclass a window, so why should C++ programmers?
Threading issues exist because the method Mr. DiLascia used was similar to the MFC method of retaining
a mapping from HWND
to CWnd
pointers (CMsgHook
pointers in his case)
so that the WNDPROC
, which is a C callback, could eventually make a call on an object method.
This is a simple and elegant technique, but it's not thread safe. MFC (and CMsgHook
) make it
thread safe by having the map reside in thread local storage. Unfortunately, this means that if you pass
the object to another thread, the mapping doesn't exist in that thread, which can cause some serious and
hard to understand bugs.
So, my implementation had to do without the mapping scheme. As a bonus, by eliminating the map I can
speed up the implementation since we don't have to do a map lookup every time a message comes in.
To eliminate the map I borrowed an idea from ATL (in fact, I came very close to just stealing code
from there). The CWindow
class and it's set of related classes in ATL use a unique method
of subclassing a window so that a WNDPROC
can map to an object's method. Basically, instead
of replacing a window's WNDPROC
with a new one, you replace it with an "assembly language
thunk" instead. It's the responsibility of the thunk to call the object's method. This thunk is, though
short, rather complex. Instead of going into detail about it here you should read the article "Thunking
WndProcs in ATL" by Fritz Onion, published in C++ Report and available (at least for now) online at
http://www.develop.com/hp/onion/Articles/cpprep0399.htm.
So, if ATL already has the capability to do this same thing, why didn't I just use CWindow
instead? I could have done this. In fact Mihai Filimon did just this for his CPictureWindow
class in the article "Adding a background image to any window" which can be found on
The Code Project. This works, but I think it's too heavy handed
for two reasons. First of all, it's an ATL class, so it requires that you include ATL within your
application. This isn't a big problem since ATL was designed to be small and lightweight, unlike MFC.
However, it does mean we have to create an ATL CComModule
instance. If you are truly using
ATL, this isn't a big deal. However, for a "pluggable" class it seems like a bit of a nuisance. Secondly,
the ATL approach defines a full framework for creating and using window classes. A "pluggable" message
handler doesn't need all of the extra baggage. So I preferred to just "borrow" the idea from ATL and
implement it in a standalone class.
Most of the implementation was borrowed directly from ATL, but was stripped down to the bare bones and
cleaned up a bit. The biggest area in which improvements were made was in the message dispatching
architecture. ATL's approach used a message map similar to MFC's. However, ATL's implementation of the
message map simply translated a few macros into an actual "if then else" construct within a virtual
function. This approach is cleaner and probably produces slightly faster code. However, ATL's
implementation has a few "warts". First of all, the MESSAGE_HANDLER
macro doesn't translate
the lParam
and wParam
parameters into more meaningful types (a process known as
"message cracking" to C programmers), so the handler functions all have the same unwieldy syntax. Secondly,
ATL's implementation requires you to explicitly chain these "message maps" with the macro
CHAIN_MSG_MAP
. This can be a source of error.
The header file <SubclassWnd.h> defines several message-dispatching macros similar to those found
in ATL that define the message handling dispatch code. You saw several of them in the example above (and
several more were used that you can't see directly). BEGIN_MSG_DISPATCH
is used to start a
message dispatch map and requires two parameters: the current class name and the base class name. If you
look at the definition of this macro you will notice that the class name is never used. This may seem
strange, but it's consistent with the ATL macro (which also doesn't use the class name parameter) and
allows us the possibility for expansion in a later release. Unlike the ATL macro, mine requires the base
class name, which will be used to chain the message map automatically. Because the chaining won't happen
until after we dispatch the messages to all handlers in the current class I had to somehow save this
parameter for use by the END_MSG_DISPATCH
macro.
You can't save parameters like this with the C preprocessor, so I had to resort to a typedef.
Unfortunately, you can't use the token-pasting macro in the preprocessor to create this typedef. The
following code might look logical, but it won't work.
typedef ##rootClass root_type;
The reason it won't work is that the token-pasting macro would paste the "rootClass" parameter
directly to the preceding token, resulting in the following code after preprocessing (assuming the
second parameter was CSubclassWnd
):
typedef CSubclassWnd root_type;
So, I had to find a work around. In this case I used a simple template class,
DISPATCH_TRAITS
. This template class has one purpose in life: to hold the typedef for
its generic type. This can then be used to create a local typedef within our code. For the
implementation look at the supplied code.
All that's left is the macro DISPATCH_MSG
and its helper macros. DISPATCH_MSG
is very similar to the HANDLE_MSG
macro found in <windowsx.h>, and through the help of
other macros it translates a message into a call to a meaningful member function call. In other words, it's
a C++ style message cracker. This approach simplifies message handling code and is easily extendable to
user defined message types. You just have to supply the corresponding DISPATCH_*
macro, where
*
is the symbolic message id such as WM_MYMESSAGE
.
That covers most of the interesting points about the implementation. I hope everyone can find a use
for this class.
Version | Date | Comments |
1.0 | 10 Dec 1999 - 7 Jun 2000 | First release version. General "plugin" capability, based on concepts
from ATL but with no reliance on any library other than the Win32 SDK. |
2.0 | 7 Jun 2000 | First major revision. Now integrates more directly with MFC, with overloaded
SubclassWindow that takes a CWnd* , while retaining full support
of compiling with out MFC. Uses
Debug.h
to eliminate any need for MFC debugging macros. Reimplemented UnsubclassWindow
to be more robust (no longer requires in-order calls to unsubclass multiple plugins). Added
support for message reflection. Added support for registered messages within the message map
macros. Updated the article to reflect changes in CPersistantSize that reflect
a "better" implementation. Modified comments in the code to use "JavaDoc" style comments,
which can be sent through DoxyGen to create
documentation on code useage. Documentation generated by DoxyGen is included in the ZIP file.
Note: This version was a major revision, and though all attempts were made to minimize
impact to the interface, full backwards compatibility may not exist. Work derived from this
library may require minor modification to use the new version. |
Windows developer with 10+ years experience working in the banking industry.