Sunday, May 23, 2010
Saturday, May 22, 2010
So you want to...So you want to simplify the way your code interacts with the Windows API. You're not alone. The window creation process is pretty gruesome, and most of it won't change much between applications. It's a great candidate for code reuse through object-oriented techniques. So you decide to create a wrapper class for windowing functionality. And you soon discover that what appears to be a relatively straightforward example of C++ coding turns out not to be that simple at all.
The goal of this article is to provide an approach for wrapping Win32 window code in a C++ class. I'm not going to hammer you with five pages of source code; rather, my goal is to present a general methodology for attacking the wrapping process, and to guide you around some of the subtle (but nasty) pitfalls that you'll encounter along the way.
And it goes like thisThe first thing we need to figure out is what our class (we'll call it
Window) should do. Since we're trying to simplify the window creation process, a simple requirement is that
Windowshould save us from ever having to set eyes on a WNDCLASSEX struct or a CreateWindowEx call again. These are, simply put, way more complicated than is warranted by the notion of window creation. We therefore aim to kill them and bury them in a constructor where they'll never again see the light of day.
Futhermore, Window should OO-ify the Windows message loop. That is, it should permit individual Windows to receive messages and dispatch them to registered listeners. Think of an Input object that registers to a Window to receive input messages. This model is a better match for the object-oriented C++ environment.
The class interface will look something like the image above. Don't worry about what every method/variable does; I'll be explaining all the important ones, and the rest are either trivial or beyond the scope of this article. Do examine, however, the general structure of the class. Note that each
mhWnd). In this way, each instance represents a particular window. Note also the methods
Unsubscribe. These allow client objects to register to receive messages from particular windows. Finally, notice the pair
WndProcThunk/DistributeMessages. This sequence of methods will allow us to receive and dispatch messages in an object oriented fashion, as desired.
The first step in implementing
Windowis to write its constructors. We'll use constructors to simplify the window creation process by stuffing all of the
WNDCLASSEX/CreateWindowExnonsense inside of them and filling in as many default values as possible. 'But wait', I hear you cry. 'Won't that limit the flexibility of the windows we can create?' Well, yes and no. It's true that client code won't be able to set all the variables of the creation process. It's also true that, 95% of the time, this just isn't going to matter. A reasonable default configuration will almost always suffice. If you're really caught up about it, you can provide constructors with lots of parameters to permit whatever level of flexibility you require.
A typical constructor, therefore, should look something like the following:
What should the default values look like? It's up to you, really. Look here and here for information on what values are available.
Now for the fun stuff
This has all been relatively straightforward. Now we come to the interesting parts. I hope you noticed the ominous little comments I sprinkled in the source code above. If what I've shown so far were all there is to it, I wouldn't have even bothered to write this article. But it's not.
The problem is, we'd like our WndProc to be a member function. We'd like this because we want each
Windowinstance to receive its own messages. We don't want every
Windowwe create to receive the same messages. Yet this is exactly what will happen if we provide a typical free function as a WndProc to
RegisterClassEx: since each
Windowuses the same
WNDCLASSEX, each will necessarily have the same WndProc and therefore receive the same messages. The only way around this is for our WndProc to behave like (or be) a member function. The problem is, Windows doesn't like that.
RegisterClassExexpects functions of a certain calling convention - namely, stdcall. Member functions, however, use a different convention - thiscall. If you try to pass a member function to
RegisterClassEx, you'll get some hideous compiler error that will probably make you want to cry. But we need member function behavior. What can we do?
The answer is that we need to essentially re-create this behavior for ourselves. Don't worry - it sounds a lot scarier than it actually is. All we need is a way to link incoming messages to individual
Windowinstances. We can then use this link to look up the "target"
Windowfor each message and forward it the message details. In pseudocode, the process looks like this:
What can we use as our link? Since each
Windowcorresponds to a specific
HWND, and each message comes with an
HWND(see the WNDPROC type declaration), we can use
HWNDs to go from messages to
Windows. We'll accomplish this by giving
Windowa static map of
std::map<HWND, Window*>). Then, in
Window's constructor, we'll make every instance add itself (and its
HWND) to this registry. This way, whenever we receive a message in our WndProc, we can look up the "target" window and forward the message to it. This is what's happening in the final line of the constructor code in the prior section.
At this point, you might be a little confused. I told you that we can't use member functions as WndProcs, but then I suggested a method to bypass this that requires access to a member variable (the
HWND/Windowmap). What's the deal?
The deal is that I lied to you. Sort of. The way we'll get around the non-member-function limitation is that we'll use as our WndProc a static class method. This is allowed because static class methods follow the stdcall convention - that is, they don't receive a
thispointer. This is enough to make the compiler (and us) happy.
WndProcThunk? In addition to being a funny word, a thunk, in this context, is a free function that acts like a member function. This is useful because we require that our WndProc behaves like a member function.
WndProcThunkis a static private member method of
Window. Its job is to take incoming messages, package them up, and send them to their "target"
Windowinstances. A C++ implementation might look like this:
(Note the use of auto, a great little feature of VC++ 2010)
Notice that we forward messages to a member method named
DistributeMessagesimplements the actual behavior we want from our WndProc: it distributes incoming window messages to registered listeners, and returns whether the message is consumed in the process. In this way, we transform the Windows message loop into a traditional observer pattern implementation.
And that's pretty much it! We've successfully implemented an easy-to-use, object-oriented wrapper around Win32 windowing functionality. A simple usage might look like this:
I hope this article has been helpful. Please leave any questions, comments, or concerns; I'll answer them as quickly as possible. Till next time, happy hacking!