Sunday, May 23, 2010
Project Natal
I stumbled across this over at realsoftwaredevelopment.com and quickly found myself giggling like a little girl. How. Fucking. Cool?
Saturday, May 22, 2010
Creating a Window Wrapper Class in C++
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 this
The first thing we need to figure out is what our class (we'll call itWindow
) should do. Since we're trying to simplify the window creation process, a simple requirement is that Window
should 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
Window
contains an HWND
(mhWnd
). In this way, each instance represents a particular window. Note also the methods Subscribe
and 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
Window
is to write its constructors. We'll use constructors to simplify the window creation process by stuffing all of the WNDCLASSEX/CreateWindowEx
nonsense 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
Window
instance to receive its own messages. We don't want every Window
we 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 Window
uses 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.RegisterClassEx
expects 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
Window
instances. We can then use this link to look up the "target" Window
for each message and forward it the message details. In pseudocode, the process looks like this:What can we use as our link? Since each
Window
corresponds to a specific HWND
, and each message comes with an HWND
(see the WNDPROC type declaration), we can use HWND
s to go from messages to Window
s. We'll accomplish this by giving Window
a static map of HWND
s to Window
pointers (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/Window
map). 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
this
pointer. This is enough to make the compiler (and us) happy. Remember
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. WndProcThunk
is a static private member method of Window
. Its job is to take incoming messages, package them up, and send them to their "target" Window
instances. 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
DistributeMessages
. DistributeMessages
implements 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!
Subscribe to:
Posts (Atom)