Using the Windows Template Library Part 2
Get the source code for this article here. IntroductionWelcome 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. RequirementsI 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. A New ProjectBefore 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]
#ifndef __ATLWIN_H__ #error CDxWindowImpl.h requires atlwin.h to be included first #endif template <class T, class TBase = CWindow > class CDxWindowImpl : public CWindowImpl<T,TBase > { 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]
//#define _UNICODE #include <atldef.h> #include <windows.h> #include <atlbase.h> #include <atlapp.h> CAppModule _Module; #include <atlwin.h> #include <atlmisc.h> #include "CDxWindowImpl.h" class CDxAppWindow : public CDxWindowImpl<CDxAppWindow> { public: BEGIN_MSG_MAP(CDxAppWindow) CHAIN_MSG_MAP(CDxWindowImpl<CDxAppWindow>) 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 TraitsIn 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 <DWORD t_dwStyle = 0, DWORD t_dwExStyle = 0> 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<WS_CHILD | WS_VISIBLE | WS_CLIPCHILDREN | WS_CLIPSIBLINGS, 0> CControlWinTraits; typedef CWinTraits<WS_OVERLAPPEDWINDOW | WS_CLIPCHILDREN | WS_CLIPSIBLINGS, WS_EX_APPWINDOW | WS_EX_WINDOWEDGE> CFrameWinTraits; typedef CWinTraits<WS_OVERLAPPEDWINDOW | WS_CHILD | WS_VISIBLE | WS_CLIPCHILDREN | WS_CLIPSIBLINGS, WS_EX_MDICHILD> 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<WS_OVERLAPPEDWINDOW,0> CDxAppWinTraits; template <class T, class TBase = CWindow, class TWinTraits = CDxAppWinTraits > class CDxWindowImpl : public CWindowImpl<T,TBase, TWinTraits > { 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 RegistrationWindow 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()1 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 elsewhere2. 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. CWndClassInfoThe CWndClassInfo structure is used by the WTL libraries to store information needed to register a window class. Below are the members of CWndClassInfo1. [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 series3. DECLARE_WND_CLASS and DECLARE_WND_CLASS_EXThe DECLARE_WND_CLASS 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()4. [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() FunctionOne 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<WS_OVERLAPPEDWINDOW,0> CDxAppWinTraits; template <class T, class TBase = CWindow, class TWinTraits = CDxAppWinTraits> class CDxWindowImpl : public CWindowImpl<T,TBase,TWinTraits> { 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<CDxAppWindow,CWindow,CDxAppWinTraits>::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<CDxAppWindow> { public: static LPCTSTR GetWndClassName() { return _T("CDxAppWindow"); } BEGIN_MSG_MAP(CDxAppWindow) CHAIN_MSG_MAP(CDxWindowImpl<CDxAppWindow>) END_MSG_MAP() }; //... Full Screen and BackAnother 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<WS_OVERLAPPEDWINDOW,0> CDxAppWinTraits; template <class T, class TBase = CWindow, class TWinTraits = CDxAppWinTraits> class CDxWindowImpl : public CWindowImpl<T,TBase,TWinTraits> { 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<T,TBase,TWinTraits>::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. ConclusionWell, 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 rmack005@hotmail.com or by posting a comment in this articles discussion forum. AcknowledgementsI'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. Notes1 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. 2 For the curious: line 2933 for AtlModuleRegisterWndClassInfoA and line 2995 for AtlModuleRegisterWndClassInfoW. 3 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. 4 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 Discuss this article in the forums
See Also: © 1999-2011 Gamedev.net. All rights reserved. Terms of Use Privacy Policy
|