Oct
22

Windows: Part 2 Jon Keon

C++

Last time I was just starting to build a Window wrapper class to use in C++ projects.

As I started digging deeper though and implementing more features I was greeted with this:

Yep, good old Windows Blue Screen of Death. Fortunately I only managed to make it happen four times although once it was enough to completely corrupt my SystemWindow.cpp file. SVN showed it as having no changes but when I opened it it was just a bunch of garbage null characters.

  • Important Lesson Of The Day:
    Always make sure you back up your work using some sort of Source Control. You never know when your own faulty code will erase it for you.
    I use Assembla because it is awesome.

Harrowing excitement aside, I was able to figure out why these crashes were happening and more importantly fix my code to prevent them, build a nice encapsulated Window wrapper class, and meet all of my objectives from the previous post.

 

Let’s recap those objectives quickly:

  • Class that encapsulates Microsoft Windows functionality.
  • Allow for Message Based or Real Time interactivity with the Window.
  • Toggle between Full Screen and Windowed Mode
  • Allow for dynamic re-sizing and re- positioning of the window.
  • Allow for Multiple Windows and Proper message dispatching to the right window instance.
  • Map dynamic functionality in response to a Windows Message.
For readability I’ll start by posting the Header file so you can see the overall structure of the class and then I’ll post snippets of the source to explain. I’ll post the full source file at the end of the post though for a simple copy/paste.

SystemWindow.h

//////////////////////////////////////////////////////////////////////
// Window //
// //
// Description: //
// Author: jkeon //
//////////////////////////////////////////////////////////////////////

#ifndef _SYSTEMWINDOW_H_
#define _SYSTEMWINDOW_H_

//FUTURE: In the future we may make this multi-platform
#ifdef _WIN32
#define WIN32_LEAN_AND_MEAN
#endif

//////////////////////////////////////////////////////////////////////
// INCLUDES //////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////

//FUTURE: In the future we may make this multi-platform
#ifdef _WIN32
#include <Windows.h>
#endif

#include <unordered_map>
#include <string/TString.h>
#include <util/DebugUtil.h>
#include <util/Callback.h>

//////////////////////////////////////////////////////////////////////
// NAMESPACE /////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////

namespace Athena {
//////////////////////////////////////////////////////////////////////
// GLOBALS ///////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////

//////////////////////////////////////////////////////////////////////
// CLASS DECLARATION /////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////

class SystemWindow {

//TYPEDEFS
typedef util::Callback<void (WPARAM wparam, LPARAM lparam)> MessageCallback;
typedef std::unordered_multimap<UINT, MessageCallback> MessageMap;

//PUBLIC FUNCTIONS
public:
SystemWindow();
virtual ~SystemWindow();

bool Init(HINSTANCE* hInstance, tstring windowClassName, tstring windowTitleBarName, bool fullScreen, bool realTime);
void Show(int nCmdShow);
void Destroy();
MSG HandleWindowsMessage();

void SetWindowedProperties(int x, int y, int width, int height);
void SetFullScreenProperties(DWORD horizontalResolution, DWORD verticalResolution, DWORD bitsPerPixel, DWORD refreshRate);

void ToggleFullScreen(bool fullScreen);

void AddMessageCallback(UINT msg, MessageCallback messageHandler);
void RemoveMessageCallback(UINT msg, MessageCallback messageHandler);
void RemoveAllMessageCallbacks();

HWND* GetHWND();

bool IsFullScreen();
bool IsMinimized();

LRESULT CALLBACK LocalWndProc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam);

//PUBLIC VARIABLES
public:

//PROTECTED FUNCTIONS
protected:

//PROTECTED VARIABLES
protected:

//PRIVATE FUNCTIONS
private:
SystemWindow(const SystemWindow& other);
SystemWindow& operator = (const SystemWindow& other);

void DefaultCloseMessageCallback(WPARAM wparam, LPARAM lparam);
void DefaultDestroyMessageCallback(WPARAM wparam, LPARAM lparam);
void DefaultSizeMessageCallback(WPARAM wparam, LPARAM lparam);
void DefaultMoveMessageCallback(WPARAM wparam, LPARAM lparam);

//PRIVATE VARIABLES
private:
HINSTANCE* p_hInstance;
WNDCLASSEX m_definition;
HWND m_hwnd;

int m_windowed_x;
int m_windowed_y;
int m_windowed_width;
int m_windowed_height;

int m_fullscreen_width;
int m_fullscreen_height;

tstring m_windowClassName;
tstring m_windowTitleBarName;
bool m_realTime;
bool m_fullScreen;
bool m_hasMenu;
bool m_minimized;
bool m_manageFullScreen;
LONG_PTR m_gwl_style;
LONG_PTR m_gwl_exstyle;
DEVMODE m_originalSettings;
DEVMODE m_fullScreenSettings;

MessageMap m_messageMap;

static const LONG_PTR GWL_STYLE_WINDOWED = WS_OVERLAPPEDWINDOW | WS_CLIPCHILDREN | WS_CLIPSIBLINGS | WS_VISIBLE;
static const LONG_PTR GWL_STYLE_FULLSCREEN = WS_POPUP | WS_SYSMENU | WS_CLIPCHILDREN | WS_CLIPSIBLINGS | WS_VISIBLE;

static const LONG_PTR GWL_EXSTYLE_WINDOWED = WS_EX_OVERLAPPEDWINDOW;
static const LONG_PTR GWL_EXSTYLE_FULLSCREEN = WS_EX_TOPMOST;
};

//////////////////////////////////////////////////////////////////////
// STATICS ///////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////

static LRESULT CALLBACK GlobalWndProc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam);

}
#endif

 

Constructor:

SystemWindow::SystemWindow() {
p_hInstance = NULL;

m_fullscreen_width = GetSystemMetrics(SM_CXSCREEN);
m_fullscreen_height = GetSystemMetrics(SM_CYSCREEN);

m_windowed_x = 100;
m_windowed_y = 100;
m_windowed_width = 960;
m_windowed_height = 540;

m_minimized = false;
m_fullScreen = false;
m_realTime = false;
m_hasMenu = false;
m_gwl_style = GWL_STYLE_WINDOWED;
m_gwl_exstyle = GWL_EXSTYLE_WINDOWED;
}

The Constructor does nothing but initialize variables. By default, I assume the window is a window (and not a fullscreen app) and I give it a position of 100, 100 and width/height of 960/540 which is exactly half of 1080p resolution and not a standard display format. This just makes sure that my future code for switching between fullscreen and windowed mode works correctly.

The fullscreen width and height are defined by the resolution you have your monitor set to. You can override this later but we need something there in case you want to start in fullscreen mode.

The rest is pretty self explanatory. m_realTime is just a flag for whether I use PeekMessage or GetMessage which you will see later and the style variables are for setting the Window Style and Extended Style through the windows API.

 

Init:

bool SystemWindow::Init(HINSTANCE* hInstance, tstring windowClassName, tstring windowTitleBarName, bool fullScreen, bool realTime) {

//Store Instance Variables
m_windowClassName = windowClassName;
m_windowTitleBarName = windowTitleBarName;
p_hInstance = hInstance;
m_fullScreen = fullScreen;
m_realTime = realTime;
m_gwl_style = (m_fullScreen) ? GWL_STYLE_FULLSCREEN : GWL_STYLE_WINDOWED;

//Default Message Mapping
AddMessageCallback(WM_SIZE, BIND_MEM_CB(&SystemWindow::DefaultSizeMessageCallback, this));
AddMessageCallback(WM_MOVE, BIND_MEM_CB(&SystemWindow::DefaultMoveMessageCallback, this));
AddMessageCallback(WM_CLOSE, BIND_MEM_CB(&SystemWindow::DefaultCloseMessageCallback, this));
AddMessageCallback(WM_DESTROY, BIND_MEM_CB(&SystemWindow::DefaultDestroyMessageCallback, this));

//TODO: Pull in some of this from Config file
ZeroMemory(&m_definition, sizeof(m_definition));
m_definition.cbSize = sizeof(WNDCLASSEX);
m_definition.cbClsExtra = 0;
m_definition.cbWndExtra = 0;
m_definition.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);
m_definition.hCursor = LoadCursor(NULL, IDC_ARROW);
m_definition.hIcon = LoadIcon(NULL, IDI_WINLOGO);
m_definition.hIconSm = m_definition.hIcon;
m_definition.hInstance = *p_hInstance;
m_definition.lpfnWndProc = GlobalWndProc;
m_definition.lpszClassName = m_windowClassName.c_str();
m_definition.lpszMenuName = NULL;
m_definition.style = CS_HREDRAW | CS_VREDRAW | CS_OWNDC;

//Register the Class
if (!RegisterClassEx(&m_definition)) {
//If a failure, let us know about it
etrace("Register Class Failed on %s", m_windowClassName.c_str());
return false;
}

//Set the initial window size and position
RECT rect;
rect.left = (m_fullScreen) ? 0 : m_windowed_x;
rect.top = (m_fullScreen) ? 0 : m_windowed_y;
rect.right = (m_fullScreen) ? m_fullscreen_width : m_windowed_x + m_windowed_width;
rect.bottom = (m_fullScreen) ? m_fullscreen_height : m_windowed_y + m_windowed_height;
AdjustWindowRectEx(&rect, m_gwl_style, m_hasMenu, m_gwl_exstyle);

//Create the window and store the handle. Passing "this" pointer as the lparam so we can make the mappings in our GlobalWndProc
m_hwnd = CreateWindowEx(m_gwl_exstyle, m_windowClassName.c_str(), m_windowTitleBarName.c_str(), m_gwl_style, rect.left, rect.top, rect.right - rect.left, rect.bottom - rect.top, NULL, NULL, *p_hInstance, this);
if (!m_hwnd) {
//If a failure, let us know about it
etrace("Create Window Failed on %s", m_windowClassName.c_str());
return false;
}

//Get and Store the current System Display Settings
HDC hdc = GetDC(m_hwnd);

//These are the current settings of the users computer
m_originalSettings.dmSize = sizeof(DEVMODE);
m_originalSettings.dmPelsWidth = GetDeviceCaps(hdc, HORZRES);
m_originalSettings.dmPelsHeight = GetDeviceCaps(hdc, VERTRES);
m_originalSettings.dmBitsPerPel = GetDeviceCaps(hdc, BITSPIXEL);
m_originalSettings.dmDisplayFrequency = GetDeviceCaps(hdc, VREFRESH);
m_originalSettings.dmFields = DM_PELSWIDTH | DM_PELSHEIGHT | DM_BITSPERPEL | DM_DISPLAYFREQUENCY;

//Fullscreen settings SHOULD mimic the current settings but we do want to be able to override them in the future potentially.
m_fullScreenSettings.dmSize = sizeof(DEVMODE);
m_fullScreenSettings.dmPelsWidth = m_fullscreen_width = GetDeviceCaps(hdc, HORZRES);
m_fullScreenSettings.dmPelsHeight = m_fullscreen_height = GetDeviceCaps(hdc, VERTRES);
m_fullScreenSettings.dmBitsPerPel = GetDeviceCaps(hdc, BITSPIXEL);
m_fullScreenSettings.dmDisplayFrequency = GetDeviceCaps(hdc, VREFRESH);
m_fullScreenSettings.dmFields = DM_PELSWIDTH | DM_PELSHEIGHT | DM_BITSPERPEL | DM_DISPLAYFREQUENCY;

//For every GetDC we should ReleaseDC
ReleaseDC(m_hwnd, hdc);

//If we're starting in Fullscreen Mode, ensure the Display Settings have switched as well
if (m_fullScreen) {
ChangeDisplaySettings(&m_fullScreenSettings, CDS_FULLSCREEN);
}

return true;
}

The Init method is where all the initial setup and actual creation of the window happens. At first I’m simply storing the variables passed in and modifying some flags based on whether we’re starting in fullscreen mode or not.

The next section requires a bit of an aside and I’ll talk more about it when dealing with Message Callbacks. Essentially I’m just saying that when we get a message of a certain type from the Windows Message Pump, then call this specific function.

Ex.

AddMessageCallback(WM_SIZE, BIND_MEM_CB(&SystemWindow::DefaultSizeMessageCallback, this));

Whenever I get a WM_SIZE message, I’ll call the DefaultSizeMessageCallback function on this instance of SystemWindow.

Next I fill out the Window classex definition. This is pretty standard but note that the lpfnWndProc is pointing to the GlobalWndProc function which is a static function and that the style has CS_OWNDC for performance.

Then the class definition is registered and the initial rectangle of the window is sized and adjusted based on the flags.

Now the window can actually be created, again ensuring that the flags are properly passed in to allow for starting in fullscreen mode or windowed mode. Take note that the last parameter is a this pointer which I will be using in the GlobalWndProc function later.

Since the window is created, I can get the device context and get some of the settings for your monitor. I get these twice, once for the original settings that the user’s computer was using before they started the application and then a duplicate version for the fullscreen settings.

In most cases, when you go full screen, you’re simply going to make your window take up the entire resolution of the monitor. However, you may allow the user to configure the resolution to be a lower or higher resolution than what their monitor is currently set at and so we need a way to store the desired settings to change to. The settings are pretty easy to understand, it’s just the Horizontal/Vertical resolution, the number of bits per pixel (usually 32) and the display frequency or monitor refresh rate (usually 60hz). The dmFields simply specifies that we want to use those four properties.

Finally if I’m starting in fullscreen mode, I’ll want to force a change in the display settings of your monitor. This will work the same as when you go into control panel and change the resolution. If there is a change, your monitor should flicker. In most cases, there won’t be a flicker since the resolution, bits per pixel and frequency haven’t changed. The CDS_FULLSCREEN flag makes it so it hides the Start menu.

ToggleFullScreen:

void SystemWindow::ToggleFullScreen(bool fullScreen) {
//Only if we're actually changing
if (m_fullScreen != fullScreen) {

m_fullScreen = fullScreen;

//Adjust the location and size of the window
RECT rect;
rect.left = (m_fullScreen) ? 0 : m_windowed_x;
rect.top = (m_fullScreen) ? 0 : m_windowed_y;
rect.right = (m_fullScreen) ? m_fullscreen_width : m_windowed_x + m_windowed_width;
rect.bottom = (m_fullScreen) ? m_fullscreen_height : m_windowed_y + m_windowed_height;

//Set all the styles and booleans based on Fullscreen or not
m_gwl_exstyle = (m_fullScreen) ? GWL_EXSTYLE_FULLSCREEN : GWL_EXSTYLE_WINDOWED;
m_gwl_style = (m_fullScreen) ? GWL_STYLE_FULLSCREEN : GWL_STYLE_WINDOWED;
SetWindowLongPtr(m_hwnd, GWL_EXSTYLE, m_gwl_exstyle);
SetWindowLongPtr(m_hwnd, GWL_STYLE, m_gwl_style);

AdjustWindowRectEx(&rect, m_gwl_style, m_hasMenu, m_gwl_exstyle);

SetWindowPos(m_hwnd, (m_fullScreen) ? HWND_TOPMOST : HWND_NOTOPMOST, rect.left, rect.top, rect.right - rect.left, rect.bottom - rect.top, SWP_SHOWWINDOW);

//Change the Display Settings
ChangeDisplaySettings((m_fullScreen) ? &m_fullScreenSettings : &m_originalSettings, (m_fullScreen) ? CDS_FULLSCREEN : CDS_RESET);
}
}

Toggling between fullscreen and back was actually somewhat difficult. This is one of the areas that caused the Blue Screens of Death but it was due to my own fault of not properly setting the fullscreen width and height so they were set to 0 which causes a fatal crash.

However the actual function logic is pretty simple. So long as we’re actually changing from fullscreen to windowed mode or vice versa, we update a rect with the stored windowed dimensions or the stored fullscreen dimensions.

The flags are set for the window styles but to actually change them on the window itself we need to use the SetWindowLongPtr function.

AdjustWindowRectEx just modifies the window size to account for things like borders and menus and whatever styles you had set. I had mistakenly been using AdjustWindowRect with an Ex style window and everytime I would switch between fullscreen or windowed the size would slowly shrink by about 10 pixels in the width and height. Just make sure if you’re using the Ex style window, you use all the Ex style functions!

Next we set the window’s position using that Rect and toggle a few flags based on fullscreen or not.

Finally we change the monitor display settings to account for fullscreen or not. If we’re going from Fullscreen to windowed there will always be the flicker because we’re using CDS_RESET. That flag simply means that no matter what, force the change.

Now to make the fullscreen/windowed toggle actually work, we need to be able to intercept and update our sizes properly. You’ll see in the full source code below there are two functions for setting the windowed properties and the fullscreen properties. This allows your application to explicitly specify the size and location in both modes.

When in windowed mode though, the user could drag the window around or resize it by dragging on the edges of the window. This is where those Default callbacks come in. We have two specifically for this responding to the WM_SIZE and WM_MOVE windows messages. They simply update the internal properties of the x, y, width and height of the window so that when you toggle between fullscreen and windowed mode everything behaves as expected.

 

Messages:

Messages are a big part of Windows programming and probably could warrant their own post.

Essentially this is the basic flow.

  1. Windows (The Operating System) will queue up messages to give to your application.
  2. You need to explicitly check these messages and do something with them. See SystemWindow::HandleWindowsMessage.
    1. This requires either PeekMessage or GetMessage.
    2. Once you’ve gotten the message you need to TranslateMessage and then DispatchMessage.
  3. When a message is dispatched, it will hit your lpfnWndProc static function that you defined in your Window ClassEx definition. See SystemWindow:::GlobalWndProc
Now if you’re making a very simple app with only ever one window, you can follow the Microsoft documentation and have a while loop that does the GetMessage or PeekMessage and then have a giant case statement in your version of a GlobalWndProc for handling the various messages.
But what if you want to have more than one window? And you want to customize the functionality per message? Now you need to have some more advanced functionality to deal with that.
Let’s start with HandleWindowsMessage.

HandleWindowsMessage:

MSG SystemWindow::HandleWindowsMessage() {
MSG msg;
ZeroMemory(&msg, sizeof(MSG));
if (m_realTime) {
PeekMessage(&msg, m_hwnd, 0, 0, PM_REMOVE);
}
else {
GetMessage(&msg, m_hwnd, 0, 0);
}

if (msg.message != WM_NULL) {
TranslateMessage(&msg);
DispatchMessage(&msg);
}

return msg;
}
You'll notice that I don't have a Message Loop in the window class. This is because that loop would restrict execution of the program to this instance of the Window class. Again what if i had multiple windows? We'd just be stuck inside the first window's message loop until that window was destroyed. Instead I opted to let the user define their own program loop and explicitly call to check what messages are waiting for a certain window.

Here's an example:
bool running = true;
MSG windowMessage;

while(running) {
windowMessage = m_window.HandleWindowsMessage();
if (windowMessage.message != WM_QUIT) {
//TODO: Execute app code
}
else {
running = false;
break;
}
}

The loop runs at the application level and each iteration we check to see if there is a windows message waiting. So long as the message isn’t WM_QUIT, we can execute our application code. Otherwise we will want to exit the loop and start cleaning up. This approach allows us to extend this loop for multiple windows.

In the HandleWindowsMessage function itself, we use PeekMessage or GetMessage based on whether we want to run in real time or not. It really depends on what your application will do but know that PeekMessage returns immediately while GetMessage is a blocking call and your program will wait there forever until a message comes into play. If you’re making a 3D game or something that requires constant updates to visuals etc, you don’t want to get stuck waiting for a message.

So long as there is a message, we translate and then dispatch it to our GlobalWndProc.

 

GlobalWndProc:

//Every Windows Message will hit this function.
LRESULT CALLBACK GlobalWndProc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam) {

//Hold our target window instance
SystemWindow *targetWindow = NULL;

//If it's the WM_NCCREATE message (which should be the first message we get...)
if(msg == WM_NCCREATE) {
//Pull the target window out of the lpCreateParams which is the this pointer we pass into CreateWindowEx
targetWindow = reinterpret_cast<SystemWindow*>((LONG)((LPCREATESTRUCT)lparam)->lpCreateParams);
//Set the pointer to this instance in the GWLP_USERDATA so we can pull it out reliably in the future
SetWindowLongPtr(hwnd, GWLP_USERDATA, (LONG_PTR)targetWindow);
}
else {
//Pull the window instance out of the GWLP_USERDATA
targetWindow = reinterpret_cast<SystemWindow*>(GetWindowLongPtr(hwnd, GWLP_USERDATA));
}

//If we still don't have a window we can't respond to any events so kick it to the default.
if(targetWindow == NULL) {
return DefWindowProc(hwnd, msg, wparam, lparam);
}

//Otherwise we're all good and we can pipe it to the instances version of the WndProc
return targetWindow->LocalWndProc(hwnd, msg, wparam, lparam);
}

Remember when we defined our Windows ClassEx definition in the Init function we set the property lpfnWndProc to GlobalWndProc? This is because Windows needs to know where to pipe messages so you can handle them. For whatever reason it needs to be a static function too. This presents a bit of a problem because we might want to have multiple windows and have each of those windows respond to messages differently. Just one WndProc function won’t cut it.

So what we do instead is a bit of trickery courtesy of the following links:

http://members.gamedev.net/sicrane/articles/WindowClass.html

 

http://www.gamedev.net/topic/387255-wndclassexlpfnwndproc–member-function/

http://stackoverflow.com/questions/7759874/weird-bug-with-wndproc-and-dispatchmessage-member-functions-dont-exist

http://www.gamedev.net/page/resources/_/technical/game-programming/creating-a-win32-window-wrapper-class-r1810

When we created our window with the CreateWindowEx function in our Init method, we passed a this pointer as the last parameter. Well it just so happens that the first message you receive when creating a window is the WM_NCCREATE message. This message comes along with a pointer to the handle to the window (hwnd) and some information in the lparam.

In the case of WM_NCCREATE, the lparam is whatever you passed in as the last parameter in the CreateWindowEx function. So as you can see in the code, we pull the instance of the window out of the lparam and by using SetWindowLongPtr, we store the pointer to the instance of the window in the special GWLP_USERDATA space on the handle to the window.

NOTE: The Handle to the window and the instance of the Window are NOT the same thing. The Handle is a Windows API thing that allows limited access to the actual Window you see on screen. The pointer we got out of the lparam is a pointer to our instance of the SystemWindow class that we created.

By storing the pointer to our instance of our SystemWindow class on the Handle the next time we get a message we can simply pull it out again. If we still don’t have a valid window, we’ll just let the default occur which happens by calling DefWindowProc.

Otherwise we have a pointer to our instance so we can pipe the message down into our window’s LocalWndProc function.

This allows us to have custom functionality per window should we so choose.

LocalWndProc:

LRESULT CALLBACK SystemWindow::LocalWndProc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam) {
//Check to make sure the map isn't empty
if (!m_messageMap.empty()) {

//Finds the first and last instance that matches the key
std::pair<MessageMap::const_iterator, MessageMap::const_iterator> range = m_messageMap.equal_range(msg);

//Exit condition
MessageMap::const_iterator end = m_messageMap.end();

//There were no matches found
if (range.first == end && range.second == end) {
//We should default
return DefWindowProc(hwnd, msg, wparam, lparam);
}
else {
MessageMap::const_iterator start = range.first;
end = range.second;

MessageCallback messageCallback;

//Loop through all the callbacks
for (; start != end; ++start) {
//Get the Callback and Call it
messageCallback = start->second;
messageCallback(wparam, lparam);
}
return 0;
}
}
else {
return DefWindowProc(hwnd, msg, wparam, lparam);
}
}

Now since we want our Window instances to be able to have custom functionality per message we’ll introduce the concept of a MessageMap. A MessageMap is just a typedef for an unordered_multimap with a UINT for a key and a MessageCallback for the value.

This allows us to register MessageCallbacks to respond to certain Windows Messages which come in as UINT’s.

In the LocalWndProc, we check if the message map is empty which if it is, we don’t need to do anything and can just default the message handling to the Windows DefWindowProc.

If it’s not empty, we want to get all the items that have that specific Message as their key. (Since it’s a multimap, there can be more than one). If we don’t find any, we’ll let Windows handle it again. If we do find some, we simply iterate through the list and trigger each MessageCallback and pass the wparam and lparam in case those functions want to use them.

Callbacks:

Now is a good time for an aside on callbacks. Unfortunately in C++ there isn’t a nice way to just pass functions around and store them for later. You can use a void pointer and do some trickery with casting but its generally regarded as messy and potentially unsafe. There are solutions where you wrap the functions in objects that you can use later but they have some issues as well when dealing with ease of use and/or performance.

Fortunately I was able to find a great little callback library by Elbert Mai on The Code Project. It’s easy to use, has good performance and is published under a very permissive licence.

The only thing it doesn’t have is the ability to check for equality between Callback objects, but as I’ve posted in the comments of Elbert’s article I think I found a way to do it. At least it works as expected for me.

The AddMessageCallback and RemoveMessageCallback make use of adding and removing callbacks respectively from the message map and handle the checks for equality so that we remove the correct callback and never add a duplicate message. Because these are public, you can have any part of your application pipe into the message notifications from Windows without having to customize the SystemWindow class or extend off of it.

This is a great library and I’m very thankful to Mr. Mai for posting it.

Cleaning Up:

The last part of dealing with the SystemWindow is properly cleaning it up.

There is a default message callback for handling the WM_CLOSE message. This message occurs when you press Alt-F4 or click on the X or right click on the task bar and select close. In here it’s a good opportunity for you to prompt the user an “Are you sure?” dialog and handle any saving of state to a file etc.

void SystemWindow::DefaultCloseMessageCallback(WPARAM wparam, LPARAM lparam) {
		dtrace("CLOSING");
		//TODO: Allow for hook to save state
		//Destroy the Window
		DestroyWindow(m_hwnd);
		//Very important to set this to NULL so the WM_QUIT is picked up
		m_hwnd = NULL;
	}

In my case, I’m not ready to save anything so I just call DestroyWindow which is a Windows API function to trigger the WM_DESTROY message. I also make sure I set my m_hwnd to NULL. This is because the final WM_QUIT message isn’t associated with any window handle in particular. If I didn’t set the m_hwnd to NULL, PeekMessage and GetMessage would never get the WM_QUIT because they would be looking only for messages on that window handle. Since WM_QUIT only comes in on NULL and now m_hwnd is NULL, we can get that message, pass it back up to our application loop through HandleWindowsMessage and our program can exit properly.

That took quite some time to figure out…

Finally, the default message callback for WM_DESTROY just posts the quit message via PostQuitMessage. This simply inserts a WM_QUIT into the queue so we can get it next time around.

void SystemWindow::DefaultDestroyMessageCallback(WPARAM wparam, LPARAM lparam) {
dtrace("DESTROYING");
PostQuitMessage(0);
}

Lastly, you’ll notice that there is an explicit Destroy function in SystemWindow.

void SystemWindow::Destroy() {
//Unregister the Class
UnregisterClass(m_windowClassName.c_str(), *p_hInstance);

//Unregister the Message Mappings
RemoveAllMessageCallbacks();

//NULL out pointers
p_hInstance = NULL;
}

Since the SystemWindow is explicitly instantiated via Init, it is expected to be explicitly destroyed via Destroy. You’d call this in your application once you’ve broken out of your application loop and are cleaning up after yourself.

The bits of destruction we did in the Close and Destroy MessageCallbacks are only for the Window itself, not the SystemWindow class. Still it’s pretty simple. We just unregister the class, remove all the message callback in the message map and then null out any pointers we had.

 

Recap:

That’s it! It’s a long post with a fair amount of code but most of it is really understanding how Windows work under the hood. The actual logic is pretty simple once you’ve wrapped your head around it. Let’s look at the objectives:

  • Class that encapsulates Microsoft Windows functionality. – Done by the very nature of SystemWindow.
  • Allow for Message Based or Real Time interactivity with the Window. – Done by setting the m_realTime flag and using PeekMessage or GetMessage.
  • Toggle between Full Screen and Windowed Mode – Done by using ToggleFullScreen.
  • Allow for dynamic re-sizing and re- positioning of the window. – Done by using the SetWindowProperties and SetWindowFullscreenProperties.
  • Allow for Multiple Windows and Proper message dispatching to the right window instance. – Done by our GlobalWndProc/LocalWndProc mappings.
  • Map dynamic functionality in response to a Windows Message. – Done by the MessageMap and MessageCallbacks.
Looks like we hit all the objectives. Now we can start using the window to host content!
I don’t believe this class is fully complete by any means. I’m sure there will be other bits of functionality to add as I use the SystemWindow more and more but for now it handles all the major points I need.

SystemWindow.cpp

As promised, the full SystemWindow.cpp source.

//////////////////////////////////////////////////////////////////////
// INCLUDES //////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////

#include "SystemWindow.h"

//////////////////////////////////////////////////////////////////////
// NAMESPACE /////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////

namespace Athena {

//////////////////////////////////////////////////////////////////////
// STATICS ///////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////

//Every Windows Message will hit this function.
LRESULT CALLBACK GlobalWndProc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam) {

//Hold our target window instance
SystemWindow *targetWindow = NULL;

//If it's the WM_NCCREATE message (which should be the first message we get...)
if(msg == WM_NCCREATE) {
//Pull the target window out of the lpCreateParams which is the this pointer we pass into CreateWindowEx
targetWindow = reinterpret_cast<SystemWindow*>((LONG)((LPCREATESTRUCT)lparam)->lpCreateParams);
//Set the pointer to this instance in the GWLP_USERDATA so we can pull it out reliably in the future
SetWindowLongPtr(hwnd, GWLP_USERDATA, (LONG_PTR)targetWindow);
}
else {
//Pull the window instance out of the GWLP_USERDATA
targetWindow = reinterpret_cast<SystemWindow*>(GetWindowLongPtr(hwnd, GWLP_USERDATA));
}

//If we still don't have a window we can't respond to any events so kick it to the default.
if(targetWindow == NULL) {
return DefWindowProc(hwnd, msg, wparam, lparam);
}

//Otherwise we're all good and we can pipe it to the instances version of the WndProc
return targetWindow->LocalWndProc(hwnd, msg, wparam, lparam);
}

//////////////////////////////////////////////////////////////////////
// CONSTRUCTORS //////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////

SystemWindow::SystemWindow() {
p_hInstance = NULL;

m_fullscreen_width = GetSystemMetrics(SM_CXSCREEN);
m_fullscreen_height = GetSystemMetrics(SM_CYSCREEN);

m_windowed_x = 100;
m_windowed_y = 100;
m_windowed_width = 960;
m_windowed_height = 540;

m_minimized = false;
m_fullScreen = false;
m_realTime = false;
m_hasMenu = false;
m_gwl_style = GWL_STYLE_WINDOWED;
m_gwl_exstyle = GWL_EXSTYLE_WINDOWED;
}

//////////////////////////////////////////////////////////////////////
// DESTRUCTOR ////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////

SystemWindow::~SystemWindow() {

}

//////////////////////////////////////////////////////////////////////
// INITIALIZE ////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////

bool SystemWindow::Init(HINSTANCE* hInstance, tstring windowClassName, tstring windowTitleBarName, bool fullScreen, bool realTime) {

//Store Instance Variables
m_windowClassName = windowClassName;
m_windowTitleBarName = windowTitleBarName;
p_hInstance = hInstance;
m_fullScreen = fullScreen;
m_realTime = realTime;
m_gwl_style = (m_fullScreen) ? GWL_STYLE_FULLSCREEN : GWL_STYLE_WINDOWED;

//Default Message Mapping
AddMessageCallback(WM_SIZE, BIND_MEM_CB(&SystemWindow::DefaultSizeMessageCallback, this));
AddMessageCallback(WM_MOVE, BIND_MEM_CB(&SystemWindow::DefaultMoveMessageCallback, this));
AddMessageCallback(WM_CLOSE, BIND_MEM_CB(&SystemWindow::DefaultCloseMessageCallback, this));
AddMessageCallback(WM_DESTROY, BIND_MEM_CB(&SystemWindow::DefaultDestroyMessageCallback, this));

//TODO: Pull in some of this from Config file
ZeroMemory(&m_definition, sizeof(m_definition));
m_definition.cbSize = sizeof(WNDCLASSEX);
m_definition.cbClsExtra = 0;
m_definition.cbWndExtra = 0;
m_definition.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);
m_definition.hCursor = LoadCursor(NULL, IDC_ARROW);
m_definition.hIcon = LoadIcon(NULL, IDI_WINLOGO);
m_definition.hIconSm = m_definition.hIcon;
m_definition.hInstance = *p_hInstance;
m_definition.lpfnWndProc = GlobalWndProc;
m_definition.lpszClassName = m_windowClassName.c_str();
m_definition.lpszMenuName = NULL;
m_definition.style = CS_HREDRAW | CS_VREDRAW | CS_OWNDC;

//Register the Class
if (!RegisterClassEx(&m_definition)) {
//If a failure, let us know about it
etrace("Register Class Failed on %s", m_windowClassName.c_str());
return false;
}

//Set the initial window size and position
RECT rect;
rect.left = (m_fullScreen) ? 0 : m_windowed_x;
rect.top = (m_fullScreen) ? 0 : m_windowed_y;
rect.right = (m_fullScreen) ? m_fullscreen_width : m_windowed_x + m_windowed_width;
rect.bottom = (m_fullScreen) ? m_fullscreen_height : m_windowed_y + m_windowed_height;
AdjustWindowRectEx(&rect, m_gwl_style, m_hasMenu, m_gwl_exstyle);

//Create the window and store the handle. Passing "this" pointer as the lparam so we can make the mappings in our GlobalWndProc
m_hwnd = CreateWindowEx(m_gwl_exstyle, m_windowClassName.c_str(), m_windowTitleBarName.c_str(), m_gwl_style, rect.left, rect.top, rect.right - rect.left, rect.bottom - rect.top, NULL, NULL, *p_hInstance, this);
if (!m_hwnd) {
//If a failure, let us know about it
etrace("Create Window Failed on %s", m_windowClassName.c_str());
return false;
}

//Get and Store the current System Display Settings
HDC hdc = GetDC(m_hwnd);

//These are the current settings of the users computer
m_originalSettings.dmSize = sizeof(DEVMODE);
m_originalSettings.dmPelsWidth = GetDeviceCaps(hdc, HORZRES);
m_originalSettings.dmPelsHeight = GetDeviceCaps(hdc, VERTRES);
m_originalSettings.dmBitsPerPel = GetDeviceCaps(hdc, BITSPIXEL);
m_originalSettings.dmDisplayFrequency = GetDeviceCaps(hdc, VREFRESH);
m_originalSettings.dmFields = DM_PELSWIDTH | DM_PELSHEIGHT | DM_BITSPERPEL | DM_DISPLAYFREQUENCY;

//Fullscreen settings SHOULD mimic the current settings but we do want to be able to override them in the future potentially.
m_fullScreenSettings.dmSize = sizeof(DEVMODE);
m_fullScreenSettings.dmPelsWidth = m_fullscreen_width = GetDeviceCaps(hdc, HORZRES);
m_fullScreenSettings.dmPelsHeight = m_fullscreen_height = GetDeviceCaps(hdc, VERTRES);
m_fullScreenSettings.dmBitsPerPel = GetDeviceCaps(hdc, BITSPIXEL);
m_fullScreenSettings.dmDisplayFrequency = GetDeviceCaps(hdc, VREFRESH);
m_fullScreenSettings.dmFields = DM_PELSWIDTH | DM_PELSHEIGHT | DM_BITSPERPEL | DM_DISPLAYFREQUENCY;

//For every GetDC we should ReleaseDC
ReleaseDC(m_hwnd, hdc);

//If we're starting in Fullscreen Mode, ensure the Display Settings have switched as well
if (m_fullScreen) {
ChangeDisplaySettings(&m_fullScreenSettings, CDS_FULLSCREEN);
}

return true;
}

//////////////////////////////////////////////////////////////////////
// DESTROY ///////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////

void SystemWindow::Destroy() {
//Unregister the Class
UnregisterClass(m_windowClassName.c_str(), *p_hInstance);

//Unregister the Message Mappings
RemoveAllMessageCallbacks();

//NULL out pointers
p_hInstance = NULL;
}

//////////////////////////////////////////////////////////////////////
// BODY //////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////

void SystemWindow::Show(int nCmdShow) {
//So long as we have the handle to the window, show it
if (m_hwnd != NULL) {
ShowWindow(m_hwnd, nCmdShow);
}
}

void SystemWindow::SetWindowedProperties(int x, int y, int width, int height) {
m_windowed_x = x;
m_windowed_y = y;
m_windowed_width = width;
m_windowed_height = height;
}

void SystemWindow::SetFullScreenProperties(DWORD horizontalResolution, DWORD verticalResolution, DWORD bitsPerPixel, DWORD refreshRate) {
//Allow for fullscreen settings to be overridden
m_fullScreenSettings.dmPelsWidth = m_fullscreen_width = horizontalResolution;
m_fullScreenSettings.dmPelsHeight = m_fullscreen_height = verticalResolution;
m_fullScreenSettings.dmBitsPerPel = bitsPerPixel;
m_fullScreenSettings.dmDisplayFrequency = refreshRate;

//If we're already in Fullscreen mode, reset to the new properties. Otherwise we'll wait till the next time ToggleFullScreen is called
if (m_fullScreen) {
ChangeDisplaySettings(&m_fullScreenSettings, CDS_FULLSCREEN);
}
}

void SystemWindow::ToggleFullScreen(bool fullScreen) {
//Only if we're actually changing
if (m_fullScreen != fullScreen) {

m_fullScreen = fullScreen;

//Adjust the location and size of the window
RECT rect;
rect.left = (m_fullScreen) ? 0 : m_windowed_x;
rect.top = (m_fullScreen) ? 0 : m_windowed_y;
rect.right = (m_fullScreen) ? m_fullscreen_width : m_windowed_x + m_windowed_width;
rect.bottom = (m_fullScreen) ? m_fullscreen_height : m_windowed_y + m_windowed_height;

//Set all the styles and booleans based on Fullscreen or not
m_gwl_exstyle = (m_fullScreen) ? GWL_EXSTYLE_FULLSCREEN : GWL_EXSTYLE_WINDOWED;
m_gwl_style = (m_fullScreen) ? GWL_STYLE_FULLSCREEN : GWL_STYLE_WINDOWED;
SetWindowLongPtr(m_hwnd, GWL_EXSTYLE, m_gwl_exstyle);
SetWindowLongPtr(m_hwnd, GWL_STYLE, m_gwl_style);

AdjustWindowRectEx(&rect, m_gwl_style, m_hasMenu, m_gwl_exstyle);

SetWindowPos(m_hwnd, (m_fullScreen) ? HWND_TOPMOST : HWND_NOTOPMOST, rect.left, rect.top, rect.right - rect.left, rect.bottom - rect.top, SWP_SHOWWINDOW);

//Change the Display Settings
ChangeDisplaySettings((m_fullScreen) ? &m_fullScreenSettings : &m_originalSettings, (m_fullScreen) ? CDS_FULLSCREEN : CDS_RESET);
}
}

//////////////////////////////////////////////////////////////////////
// MESSAGE MAPPING ///////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////

void SystemWindow::AddMessageCallback(UINT msg, MessageCallback messageHandler) {

//Finds the first and last instance that matches the key
std::pair<MessageMap::const_iterator, MessageMap::const_iterator> range = m_messageMap.equal_range(msg);

//Exit condition
MessageMap::const_iterator end = m_messageMap.end();

bool found = false;

//If there are matches
if (range.first != end && range.second != end) {

MessageMap::const_iterator start = range.first;
end = range.second;
//Look for an existing messageHandler already mapped to the same msg
MessageCallback existing;

//Loop through all the callbacks
for (; start != end; ++start) {
//Get the Callback and see if we've already got it
existing = start->second;
if (existing == messageHandler) {
found = true;
return;
}
}
}

if (!found) {
//Insert the new messageHandler at key msg
m_messageMap.insert(MessageMap::value_type(msg, messageHandler));
}

}

void SystemWindow::RemoveMessageCallback(UINT msg, MessageCallback messageHandler) {

//Finds the first and last instance that matches the key
std::pair<MessageMap::const_iterator, MessageMap::const_iterator> range = m_messageMap.equal_range(msg);

//Exit condition
MessageMap::const_iterator end = m_messageMap.end();

//If there are matches
if (range.first != end && range.second != end) {
MessageMap::const_iterator start = range.first;
end = range.second;
//Look for an existing messageHandler already mapped to the same msg
MessageCallback existing;

//Loop through all the callbacks
for (; start != end; ++start) {
//Get the Callback and see if we've already got it
existing = start->second;
if (existing == messageHandler) {
existing = util::NullCallback();
m_messageMap.erase(start);
return;
}
}
}
}

void SystemWindow::RemoveAllMessageCallbacks() {
//Delete all entries in the list
m_messageMap.clear();
}

//////////////////////////////////////////////////////////////////////
// MESSAGE HANDLING //////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////

MSG SystemWindow::HandleWindowsMessage() {
MSG msg;
ZeroMemory(&msg, sizeof(MSG));
if (m_realTime) {
PeekMessage(&msg, m_hwnd, 0, 0, PM_REMOVE);
}
else {
GetMessage(&msg, m_hwnd, 0, 0);
}

if (msg.message != WM_NULL) {
TranslateMessage(&msg);
DispatchMessage(&msg);
}

return msg;
}

void SystemWindow::DefaultSizeMessageCallback(WPARAM wparam, LPARAM lparam) {
dtrace("SIZING");

m_minimized = (wparam == SIZE_MINIMIZED);
(m_fullScreen) ? (m_fullScreenSettings.dmPelsWidth = m_fullscreen_width = LOWORD(lparam)) : (m_windowed_width = LOWORD(lparam));
(m_fullScreen) ? (m_fullScreenSettings.dmPelsHeight = m_fullscreen_height = HIWORD(lparam)) : (m_windowed_height = HIWORD(lparam));
}

void SystemWindow::DefaultMoveMessageCallback(WPARAM wparam, LPARAM lparam) {
dtrace("MOVING");
if (m_fullScreen == false) {
m_windowed_x = LOWORD(lparam);
m_windowed_y = HIWORD(lparam);
}
}

void SystemWindow::DefaultCloseMessageCallback(WPARAM wparam, LPARAM lparam) {
dtrace("CLOSING");
//TODO: Allow for hook to save state
//Destroy the Window
DestroyWindow(m_hwnd);
//Very important to set this to NULL so the WM_QUIT is picked up
m_hwnd = NULL;
}

void SystemWindow::DefaultDestroyMessageCallback(WPARAM wparam, LPARAM lparam) {
dtrace("DESTROYING");
PostQuitMessage(0);
}

//////////////////////////////////////////////////////////////////////
// LOCAL WND PROC ////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////

LRESULT CALLBACK SystemWindow::LocalWndProc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam) {
//Check to make sure the map isn't empty
if (!m_messageMap.empty()) {

//Finds the first and last instance that matches the key
std::pair<MessageMap::const_iterator, MessageMap::const_iterator> range = m_messageMap.equal_range(msg);

//Exit condition
MessageMap::const_iterator end = m_messageMap.end();

//There were no matches found
if (range.first == end && range.second == end) {
//We should default
return DefWindowProc(hwnd, msg, wparam, lparam);
}
else {
MessageMap::const_iterator start = range.first;
end = range.second;

MessageCallback messageCallback;

//Loop through all the callbacks
for (; start != end; ++start) {
//Get the Callback and Call it
messageCallback = start->second;
messageCallback(wparam, lparam);
}
return 0;
}
}
else {
return DefWindowProc(hwnd, msg, wparam, lparam);
}
}

//////////////////////////////////////////////////////////////////////
// GETTERS/SETTERS ///////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////

HWND* SystemWindow::GetHWND() {
return &m_hwnd;
}

bool SystemWindow::IsFullScreen() {
return m_fullScreen;
}

bool SystemWindow::IsMinimized() {
return m_minimized;
}

}

 

 

 

This entry was posted on Saturday, October 22nd, 2011 at 3:56 pm and is filed under C++. You can follow any responses to this entry through the RSS 2.0 feed. You can leave a response, or trackback from your own site.