Introduction
Welcome back! In this, the second article in the series, we'll be covering the features of
CWindowImpl in much greater detail. As we cover each of the various topics we'll also be developing our own derivative of
CWindowImpl called
CDxWindowImpl.
CDxWindowImpl will be a reusable window implementation that will provide many of the features needed by DirectX application windows. Using an existing, tested, and feature rich framework will make it less buggy and more extendable than most alternatives.
You may recall
last time that I mentioned we'd be covering message loops and idle processing in this article. Well, I lied. Once I sat down and planned the content of this article I realized how poorly that topic fits in with the rest of this article, so I decided to bump it to part 3.
Requirements
I make many of the same assumptions in this article I made in the first. I assume that the reader has a basic understanding of Win32 programming and should be capable of creating a simple Win32 application, and of course, that he or she has read the
first article. Some familiarity with using C++ templates would be helpful as well. I also assume that the reader is using VC 6.0 SP 5 or VC .NET. Other compilers may work but again, I cannot make any guarantees. You'll also want to make sure you have the WTL 7.0 or 7.1 libraries and have added the WTL\Include path to your list of include directories. This is a minor change from the first article which compiled with the 3.1 library. The code included in this article will likely compile with the older library but I've not tested against it. And last but not least, you'll want to make sure you're building against the latest Platform SDK. Below are links to both the Microsoft Download Center where the 7.1 libraries can be obtained, and the Platform SDK Update site.
WTL
Platform SDK
A New Project
Before going any further, let's create a new project for this article. Make it an empty Win32 Application project. Add a new header file to the project called
CDxWindowImpl.h and a code file called
main.cpp. The source for these two files is listed below.
[listing 2.1]
CDxWindowImpl.h
#ifndef __ATLWIN_H__
#error CDxWindowImpl.h requires atlwin.h to be included first
#endif
template
class CDxWindowImpl : public CWindowImpl
{
public:
BEGIN_MSG_MAP(CDxWindowImpl)
MESSAGE_HANDLER(WM_DESTROY, OnDestroy)
END_MSG_MAP()
LRESULT OnDestroy( UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL &bHandled )
{
PostQuitMessage(0);
bHandled = FALSE;
return 0;
}
};
[listing 2.2]
Main.cpp
//#define _UNICODE
#include
#include
#include
#include
CAppModule _Module;
#include
#include
#include "CDxWindowImpl.h"
class CDxAppWindow : public CDxWindowImpl
{
public:
BEGIN_MSG_MAP(CDxAppWindow)
CHAIN_MSG_MAP(CDxWindowImpl)
END_MSG_MAP()
};
int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine,
int nCmdShow)
{
CMessageLoop messageLoop;
CDxAppWindow mainwnd;
_Module.Init(NULL, hInstance);
_Module.AddMessageLoop(&messageLoop);
mainwnd.Create(NULL,CWindow::rcDefault,_T("Main Window"));
if(mainwnd == NULL)
return 0;
mainwnd.ShowWindow(nCmdShow);
mainwnd.UpdateWindow();
int nRet = messageLoop.Run();
_Module.RemoveMessageLoop();
_Module.Term();
return nRet;
}
The first thing that needs to be pointed out is that although this code will compile, it won't actually create a window. This is because I've not passed any window styles to the call to
Create(). I've done this on purpose and my reason for doing so will become clear in the next section.
There's not much in these two files that's new. In
CDxWindowImpl.h we create our new template class called
CDxWindowImpl which derives directly from
CWindowImpl and adds a message handler for
WM_DESTROY. In
Main.cpp we create another class called
CDxAppWindow which derives from
CDxWindowImpl (similar to
Mainwnd and
CWindowImpl from the previous article).
CDxAppWindow is then instantiated, and a call to
Create() finishes the setup. What's different this time around is that
CDxAppWindow's message map contains the
CHAIN_MSG_MAP macro. We'll cover this in more detail in the next article. All we really need to know at this point is that
CHAIN_MSG_MAP will make all the message handlers in the message map of the class passed to it available to this class's message map. Now, all message handlers created in
CDxWindowImpl will be available to
CDxAppWindow.
Window Traits
In the previous article we specified window styles by passing the style and extended style information to
CWindowImpl's
Create() function.
[listing 2.3]
mainwnd.Create(NULL,CWindow::rcDefault,_T("Main Window"),
WS_OVERLAPPEDWINDOW);
The Windows Template Library provides another method of specifying these styles: window traits. Window traits work by taking style information and storing it in an instance of a template class. This is done though a template class called
CWinTraits. The template takes two
DWORD parameters, the style and extended style, and provides methods for accessing them. The definition of
CWinTraits is listed below.
[listing 2.4]
template
class CWinTraits
{
public:
static DWORD GetWndStyle(DWORD dwStyle)
{
return dwStyle == 0 ? t_dwStyle : dwStyle;
}
static DWORD GetWndExStyle(DWORD dwExStyle)
{
return dwExStyle == 0 ? t_dwExStyle : dwExStyle;
}
};
The purpose of the
GetWndStyle() and
GetWndExStyle() functions is rather obvious but their implementation is a bit cryptic. If the value passed to
GetWndStyle() and
GetWndExStyle() is zero they return the stored style information. If the value passed is not zero, they return the passed value. Admittedly, this seems a little silly. Why not simply return the stored values, drop the parameter, and be done with it? The answer lies in the way the class is used in the WTL libraries. Below is
CWindowImpl's
Create method.
[listing 2.5]
HWND Create(HWND hWndParent, RECT& rcPos, LPCTSTR szWindowName = NULL,
DWORD dwStyle = 0, DWORD dwExStyle = 0,
UINT nID = 0, LPVOID lpCreateParam = NULL)
{
if (T::GetWndClassInfo().m_lpszOrigName == NULL)
T::GetWndClassInfo().m_lpszOrigName = GetWndClassName();
ATOM atom = T::GetWndClassInfo().Register(&m_pfnSuperWindowProc);
dwStyle = T::GetWndStyle(dwStyle);
dwExStyle = T::GetWndExStyle(dwExStyle);
return CWindowImplBaseT< TBase, TWinTraits >::Create(hWndParent, rcPos,
szWindowName, dwStyle, dwExStyle, nID, atom, lpCreateParam);
}
Notice that the
dwStyle and
dwExStyle parameters have a default value: zero. Also notice that
dwStyle and
dwExStyle are passed to
GetWndStyle() and
GetWndExStyle(), set to the value returned by
GetWndStyle() and
GetWndExStyle(), and then passed on to
CWindowImplBaseT's
Create() method. With this set up, if no styles are specified in the call to
CWindowImpl::Create() the styles stored in the trait class are used. If styles are passed to
CWindowImpl::Create() they override the traits. This useful feature allows us to attach a set of default window styles to our newly implemented window, while users of this new window retain the ability easily override these styles if they happen to not suit the particular instance he or she is creating.
There are times, however, when you don't necessarily want users to replace the trait styles. You may wish to use the trait styles as a set of base styles and only give users of your window class the ability to add a style or two to this minimum set. The
CWinTraitsOR class provides this functionality. Simply use it in place of
CWinTraits.
To sum up,
CWindowImpl derived classes that use a
CWinTraits instance for the
TWinTraits template parameter will override the stored styles if styles are passed to
Create().
CWindowImpl derived classes that use a
CWinTraitsOR instance for the
TWinTraits template parameter will add any styles passed via
Create() to the stored styles and this combined set of styles will be used to create the window.
The WTL library also provides a handful of predefined trait classes. You'll find them used by various window implementation classes provided by the library. For the sake of completeness I've listed their definition below.
[listing 2.6]
typedef CWinTraits CControlWinTraits;
typedef CWinTraits CFrameWinTraits;
typedef CWinTraits CMDIChildWinTraits;
typedef CWinTraits<0, 0> CNullTraits;
Now that we have a thorough understanding of window traits, let's create a trait class for
CDxWindowImpl. Make the following changes to
CDxWindowImpl.h:
[listing 2.7]
#ifndef __ATLWIN_H__
#error CDxWindowImpl.h requires atlwin.h to be included first
#endif
typedef CWinTraits CDxAppWinTraits;
template
class CDxWindowImpl : public CWindowImpl
{
public:
BEGIN_MSG_MAP(CDxWindowImpl)
MESSAGE_HANDLER(WM_DESTROY, OnDestroy)
END_MSG_MAP()
LRESULT OnDestroy( UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL &bHandled )
{
PostQuitMessage(0);
bHandled = FALSE;
return 0;
}
};
With these changes in place the call to
Create() in
Main.cpp no longer needs window styles passed to it. At this point rebuilding the project should produce an executable that creates and displays a window.
Window Class Registration
Window styles (and trait classes that store styles) are a means of customizing the appearance of an individual instance of a window class. Window styles can do a great deal to change the look of a given window, but we require more control over the look of the window than window styles alone provide. Specifically, we'd like to change the background color to black. To do this we need to customize the window class itself.
So, what window class does an instance of our
CDxWindowImpl belong to, and more importantly, how can we change the class styles and attributes to suit our needs? If you'll recall from the previous article we could choose a specific name for the window class used by our implementation by including the
DECLARE_WND_CLASS macro in the class definition. Doing that gives us a definite name but doesn't tell us much else or give us any more control. Besides,
CDxWindowImpl doesn't yet include the
DECLARE_WND_CLASS macro. So what's the name of the class it's using? The answer to that last question can be found in a function called
AtlModuleRegisterWndClassInfo()[sup]1[/sup] which is defined in
atlwin.h. A bit of code in that function assigns a (pseudo) random name to the window class if one is not specifically provided elsewhere[sup]2[/sup]. The answer to the first two questions lies in the
DECLARE_WND_CLASS macro itself, but first we need to cover a structure called
CWndClassInfo.
CWndClassInfo
The
CWndClassInfo structure is used by the WTL libraries to store information needed to register a window class. Below are the members of
CWndClassInfo[sup]1[/sup].
[listing 2.8]
WNDCLASSEX m_wc;
LPCSTR m_lpszOrigName;
WNDPROC pWndProc;
LPCSTR m_lpszCursorID;
BOOL m_bSystemCursor;
ATOM m_atom;
CHAR m_szAutoName[13];
ATOM Register(WNDPROC* p)
{
return AtlModuleRegisterWndClassInfo(&_Module, this, p);
}
As you can see the first member of
CWndClassInfo is a
WNDCLASSEX structure. This is the same structure passed to the Windows API function
RegisterClassEx(). The
m_lpszOrigName is used to support super classing. The
pWndProc is a pointer to the window procedure for the class. The
m_lpszCursorID and
m_bSystemCursor support the use of cursor resources. The
m_atom member stores the
ATOM value returned by
RegisterClassEx(), and
m_szAutoName stores the pseudo random class name, if used. The sole method,
Register(), registers the class defined by this structure by calling
AtlModuleRegisterWndClassInfo(). Super classing and using custom cursors won't be covered in this series[sup]3[/sup].
DECLARE_WND_CLASS and DECLARE_WND_CLASS_EX
The [tr]DECLARE_WND_CLASS[/tr] macro does nothing more than create a function that returns an instance of a
CWndClassInfo class [1]. Below is the definition of the
DECLARE_WND_CLASS macro.
[listing 2.9]
#define DECLARE_WND_CLASS(WndClassName) \
static CWndClassInfo& GetWndClassInfo() \
{ \
static CWndClassInfo wc = \
{ \
{ sizeof(WNDCLASSEX), CS_HREDRAW | CS_VREDRAW | CS_DBLCLKS, StartWindowProc, \
0, 0, NULL, NULL, NULL, (HBRUSH)(COLOR_WINDOW + 1), NULL, WndClassName, NULL }, \
NULL, NULL, IDC_ARROW, TRUE, 0, _T("") \
}; \
return wc; \
}
As you can see the function created is a static one called
GetWndClassInfo(). The macro is a bit of a mess to look at but the class styles (
CS_HREDRAW | CS_VREDRAW | CS_DBLCLKS) and background color (
COLOR_WINDOW + 1) jump right out.
Since all the
DECLARE_WND_CLASS macro does is define a static function, we could omit the macro and define the function ourselves. By doing this we'd gain complete control over every item in the
WNDCLASSEX structure. Such a function might look like the one below, which changes the background color to black by using the helper function
AtlGetStockBrush()[sup]4[/sup].
[listing 2.10]
static CWndClassInfo& GetWndClassInfo()
{
static CWndClassInfo wc =
{
{sizeof(WNDCLASSEX), CS_HREDRAW | CS_VREDRAW | CS_DBLCLKS,
StartWindowProc,
0, 0, NULL, NULL, NULL, AtlGetStockBrush(BLACK_BRUSH), NULL,
GetWndClassName() , NULL}, NULL, NULL, IDC_ARROW, TRUE, 0, _T("")
};
return wc;
}
But if all we'd like to do is change the background color and perhaps alter the class styles we may not need go though the trouble of creating our own
GetWndClassInfo() function. The WTL provides another macro called
DECLARE_WND_CLASS_EX which allows us to set these often customized properties. The definition of
DECLARE_WND_CLASS_EX is almost identical to
DECLARE_WND_CLASS except that is adds two additional parameters,
style and
bkgnd, which as you might expect, set the class styles and background color.
Unfortunately this macro, though worth mentioning, won't do. The value passed to
bkgnd must be one of the predefined brush constants like
COLOR_WINDOW,
COLOR_WINDOWTEXT, etc. Using
AtlGetStockBrush() here won't work, so we'll stick with defining our own
GetWndClassInfo() function. This has the added advantage of giving us complete control over the class.
The GetWndClassName() Function
One of the features I'd like for
CDxWindowImpl to have is the ability to detect and prevent multiple instances of itself. To do this we'll need to be able to easily change the window class name. Each project based on
CDxWindowImpl will need to use a unique class name. If two projects use the same class name there's a chance Game A would 'see' an instance of Game B as an instance of itself.
We need a means of easily setting the class name, and there are a few options to choose from. We could simply require that each class that derives from
CDxWindowImpl define it's own
GetWndClassInfo() function. This would work well, but unless we need to change some item other than the class name it'd be nice to be able to use the
GetWndClassInfo() function inherited from
CDxWindowImpl. We could use
#define to assign a token to a string literal and use that inside
GetWndClassInfo(), but I dislike this approach. Instead we'll override a static function inherited from
CWindow called
GetWndClassName().
CWindow's implementation of
GetWndClassName() simply returns
NULL. If this function is overridden to return a non-
NULL value that value will be used by the WTL library to name the window class. This approach also allows us to call
GetWndClassName() whenever we need the name of the window class. We can even use it in
GetWndClassInfo(), and I do for the sake of clarity. Technically this isn't necessary so long as the class name used in
GetWndClassInfo() is
NULL. If not, the name set in
GetWndClassInfo() is used in place of the name returned by
GetWndClassName().
Let's give
CDxWindowImpl a definite class name and a black background color. We'll also add some code to prevent multiple instances of the same application from running on the same machine. Make the following changes to
CDxWindowImpl.h:
[listing 2.11]
#ifndef __ATLWIN_H__
#error CDxWindowImpl.h requires atlwin.h to be included first
#endif
typedef CWinTraits CDxAppWinTraits;
template
class CDxWindowImpl : public CWindowImpl
{
public:
static CWndClassInfo& GetWndClassInfo()
{
static CWndClassInfo wc =
{
{ sizeof(WNDCLASSEX), CS_HREDRAW | CS_VREDRAW | CS_DBLCLKS,
StartWindowProc,0, 0, NULL, NULL, NULL,
(AtlGetStockBrush(BLACK_BRUSH),NULL,GetWndClassName(), NULL
},
NULL, NULL, IDC_ARROW, TRUE, 0, _T("")
};
return wc;
}
static LPCTSTR GetWndClassName()
{
return _T("CDxWindowImpl");
}
BOOL AllowMultipleInstances()
{
return false;
}
BOOL PreviousInstanceFound(LPCTSTR lpClassName, LPCTSTR lpWindowName)
{
HWND hwnd = FindWindow(lpClassName,lpWindowName);
if(hwnd)
{
if(!T::AllowMultipleInstances())
{
// Flash the existing window
FLASHWINFO flashinfo;
flashinfo.cbSize = sizeof(flashinfo);
flashinfo.hwnd = hwnd;
flashinfo.dwFlags = FLASHW_ALL;
flashinfo.uCount = 2;
flashinfo.dwTimeout = 0;
FlashWindowEx(&flashinfo);
}
return true;
}
return false;
}
HWND Create(HWND hWndParent, RECT& rcPos, LPCTSTR szWindowName = NULL,
DWORD dwStyle = 0, DWORD dwExStyle = 0,
UINT nID = 0, LPVOID lpCreateParam = NULL)
{
if(PreviousInstanceFound(GetWndClassInfo().m_wc.lpszClassName,szWindowName)
& !AllowMultipleInstances())
return NULL;
HWND hwnd = CWindowImpl::Create(hWndParent,
rcPos,szWindowName,dwStyle,dwExStyle,nID,lpCreateParam);
return hwnd;
}
BEGIN_MSG_MAP(CDxAppWindow)
MESSAGE_HANDLER(WM_DESTROY, OnDestroy)
END_MSG_MAP()
LRESULT OnDestroy( UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL &bHandled )
{
PostQuitMessage(0);
bHandled = FALSE;
return 0;
}
};
We now have functions for detecting and preventing multiple instances and a
Create() function to bring them all together. Each new project based on
CDxWindowImpl now just needs to return a different string from
GetWndClassName() to make the functionality work. And if you wish to allow multiple instances just change
AllowMultipleInstances() to return true. Let's add a
GetWndClassName() override to
CDxAppWindow in
main.cpp.
[listing 2.12]
//...
class CDxAppWindow : public CDxWindowImpl
{
public:
static LPCTSTR GetWndClassName()
{
return _T("CDxAppWindow");
}
BEGIN_MSG_MAP(CDxAppWindow)
CHAIN_MSG_MAP(CDxWindowImpl)
END_MSG_MAP()
};
//...
Full Screen and Back
Another feature we'll want from our
CDxWindowImpl class is the ability to support both windowed and full screen applications. We'll also want the ability to switch between the two at runtime. I won't go into the details of how this is accomplished, it's just basic Windows API programming, but I'll list the functions here. Simply make the following changes to the
CDxWindowImpl class.
[listing 2.13]
typedef CWinTraits CDxAppWinTraits;
template
class CDxWindowImpl : public CWindowImpl
{
public:
// ... Some code left out to save space
HWND Create(HWND hWndParent, RECT& rcPos, LPCTSTR szWindowName = NULL,
DWORD dwStyle = 0, DWORD dwExStyle = 0,
UINT nID = 0, LPVOID lpCreateParam = NULL)
{
if(PreviousInstanceFound(GetWndClassInfo().m_wc.lpszClassName,szWindowName) &&
!AllowMultipleInstances())
return NULL;
HWND hwnd = CWindowImpl::Create(hWndParent,
rcPos,szWindowName,dwStyle,dwExStyle,nID,lpCreateParam);
if(!hwnd)
return NULL;
wndStyles = GetWindowLong(GWL_STYLE);
wndExStyles = GetWindowLong(GWL_EXSTYLE);
GetWindowRect(&wndRect);
return hwnd;
}
BOOL FullScreen()
{
BOOL retval;
// Save the styles and position of the window
GetWindowRect(&wndRect);
wndStyles = GetWindowLong(GWL_STYLE);
wndExStyles = GetWindowLong(GWL_EXSTYLE);
ShowWindow(SW_HIDE);
SetWindowLong(GWL_STYLE,WS_POPUP);
retval = SetWindowPos(HWND_TOPMOST,0,0,GetSystemMetrics(SM_CXSCREEN),
GetSystemMetrics(SM_CYSCREEN),SWP_SHOWWINDOW);
ShowWindow(SW_SHOW);
return retval;
}
BOOL Windowed()
{
BOOL retval;
ShowWindow(SW_HIDE);
SetWindowLong(GWL_STYLE,wndStyles);
SetWindowLong(GWL_EXSTYLE,wndExStyles);
retval = SetWindowPos(HWND_NOTOPMOST,&wndRect,SWP_DRAWFRAME);
ShowWindow(SW_SHOW);
return retval;
}
// ... More left out
private:
LONG wndStyles;
LONG wndExStyles;
RECT wndRect;
};
It doesn't matter where in the class they're included so long as they're there. Basically
FullScreen() saves the styles and position of the window and
Windowed() restores them. If you need to reposition or resize the window use
SetWindowPos() or
ResizeClient().
The complete
CDxWindowImpl template class contains two message handlers not listed in this article. One stops screensavers and the other calls virtual functions when the application becomes active or inactive. The complete
CDxWindowImpl template class can be found in the accompanying source.
Conclusion
Well, that wraps up this installment. I expect we'll be able to finish up what's left in the next article, but it may stretch on to a fourth. You'll find in the source that accompanies this article two additional functions. They allow
CDxWindowImpl to provide window styles suitable for both windowed and full screen DirectX applications. You can even switch between the two at runtime.
In the next article we'll finally get to message loops and idle processing, and also implement our own version of
CAppModule that'll initialize and create the Direct3D object. In the meantime I can be reached by emailing [email="rmack005@hotmail.com"]rmack005@hotmail.com[/email] or by posting a comment in this articles discussion forum.
Acknowledgements
I'd like to thank Ben "Kylotan" Sizer, Jim Nasche, and Ranthalion for the feedback they provided. Their comments were a great help. And again, I'd like to thank the staff at Gamedev.net for providing both a home and an audience for this article. Thanks again everyone.
Notes
[sup]1[/sup] Technically speaking, there are actually two version of a few of the WTL classes and functions, an ANSI version and a Unicode version. Here I remove the 'A' and 'W' suffixes for the sake of clarity since SomeClassOrFunction is SomeClassOrFunctionW if _UNICODE is defined or SomeClassOrFunctionA if it is not.
[sup]2[/sup] For the curious: line 2933 for AtlModuleRegisterWndClassInfoA and line 2995 for AtlModuleRegisterWndClassInfoW.
[sup]3[/sup] I may change my mind about super classing and cursors and write an article covering them. Their use would more likely be seen in game tools like map editors and such than in an actual game. If I get to them, they'll be in a supplemental article which would likely cover using the WTL to create game development tools.
[sup]4[/sup] AtlGetStockBrush() checks to make sure the value passed is a valid brush type and casts the value returned by GetStockObject() to HBRUSH. You'll find it, along with a few other functions like it, in atlmisc.h.
References
[1] Michael Park, "ATL 3.0 Window Classes: An Introduction", MSDN.
http://msdn.microsoft.com/library/default.asp?url=/library/en-us/dnvc60/html/atlwindow.asp