We'll be using C++ heavily here. If you're rusty on pure virtual functions, dynamic_cast'ing, etc., grab a C++ book and brush up before continuing.
Without further banter, let's get started.
Before we dive into code, it's important to make a blueprint of what we're aiming for.
In the finished GUI of our game, we'll use a tree to keep track of every window displayed on the screen. The window tree is a simple n-node tree. At the root of the tree is the Windows Desktop (or, if you're in X, the "root window" - now you know why they call it that). The children of the Desktop window are (usually) main windows; their children are dialog boxes, and the dialog boxes' children are the individual dialog controls (buttons, textboxes, etc). An important distinction - the appearance of a window is NOT determined by its place in the tree. For example, many games place buttons directly on their desktop windows, as well as in dialogs.
And yes, buttons are windows too. This is a very important frame of mind. A button is just a window with a funny appearance. In fact, all of the GUI controls are simply windows with different appearances. This is where the power of C++ comes in. If we create a generic window class, and give it a few virtual functions, we can then easily create our different controls by overriding the base window class's behavior. This use of polymorphism is extremely elegant; so elegant, in fact, that many C++ books use it as an example. (I'll talk more about this in Part III.)
That's our basic design, now, let's work through an implementation strategy...
[size="5"]The Plan
I took the following steps when I implemented my GUI:
- First I coded some basic window management code. This chunk of code is responsible for the window tree, adding / deleting windows, showing / hiding them, moving them to the top of the Z-Order, etc. I stubbed out the window drawing procedure by simply drawing rectangles where my windows should be, then drawing a number in the top-left corner of them to indicate their z-order.
Understand up front that your life will become tremendously easier if you buy or make a good, solid, template class for arrays of pointers. The STL (Standard Template Library) that ships with most versions of C++ has several good template-able pointer array classes, but if you want to make your own, do it formally - test it thoroughly and completely before you start implementing your window manager. The last things you need right now are subtle memory leaks or null pointer references caused by a shoddy array class. - Once I had basic window management functions, I spent some time thinking about my coordinate systems. Coded up some coordinate management functions.
- Next, I tackled the window drawing code. I derived a "fancy window" class, and showed it how to draw itself using a set of nine sprites - four sprites for the corners, four sprites for the edges, and one sprite for the background.
Using nine window sprites, it's possible to create windows that sport a unique, artistic appearance, and yet are still dynamically re-sizeable (ala StarDock's WindowBlinds). The downside to this is that you'll need a fairly smart drawing library, one that can handle tiling sprites, stretching them, and centering them, as well as a very complex window creation program (something the artists can use to construct their windows), to really make this method work well. And, of course, you'll pay in window drawing speed, too. - Once the drawing code for the generic window was complete, I started implementing the controls. Coding controls is straightforward, but again, requires very thorough testing. I started with the simple controls: statics, icons, etc., and worked my way up from there, as explained earlier.
- Finally, after all of my controls were complete, I coded up a simple Resource Editor, a program that allows someone to graphically place controls and layout dialog boxes. The resource editor took me a good month to do, but I highly suggest doing it (instead of just using text files to position stuff) - it's much easier to create dialog boxes graphically, and it was a good exercise: during development I uncovered several bugs in my controls' code, things that would have proven very difficult to catch in the actual game.
I toyed, for a very long time, with the idea of creating a program that would convert an MSVC++ resource (.RC) file into a custom resource file useable by my GUI. In the end, I decided such a program would be more trouble than what it would be worth. The whole reason I was writing a GUI was to get away from the confines of Windows, and to truly do that, I needed my own editor, tied to my own resource file format and my own way of doing things. I decided to implement a WYSIWYG Resource Editor in MFC from the ground up. My needs, my decision; your needs may be different. If anyone out there tries to write a converter, I'd love to hear about it.
So... let's start with step one: basic window management functions.
[size="5"]The Implementation
Here we go. Here's a good start for our base-class window definition:
class gui_window
{
public:
gui_window(); // boring
~gui_window(); // boring
virtual void init(void); // boring
gui_window *getparent(void) { return(m_pParent); }
/////////////
// section I: window management controls
/////////////
int addwindow(gui_window *w);
int removewindow(gui_window *w);
void show(void) { m_bIsShown = true; }
void hide(void) { m_bIsShown = false; }
bool isshown(void) { return(m_bIsShown); }
void bringtotop(void);
bool isactive(void);
/////////////
// Section II: coordinates
/////////////
void setpos(coord x1, coord y1); // boring
void setsize(coord width, coord height); // boring
void screentoclient(coord &x, coord &y);
int virtxtopixels(coord virtx); // convert GUI units to actual pixels
int virtytopixels(coord virty); // ditto
virtual gui_window *findchildatcoord(coord x, coord y, int flags = 0);
/////////////
// Section III: Drawing Code
/////////////
// renders this window + all children recursively
int renderall(coord x, coord y, int drawme = 1);
gui_wincolor &getcurrentcolorset(void)
{ return(isactive() ? m_activecolors : m_inactivecolors); }
/////////////
// Messaging stuff to be discussed in later Parts
/////////////
int calcall(void);
virtual int wm_paint(coord x, coord y);
virtual int wm_rendermouse(coord x, coord y);
virtual int wm_lbuttondown(coord x, coord y);
virtual int wm_lbuttonup(coord x, coord y);
virtual int wm_ldrag(coord x, coord y);
virtual int wm_lclick(coord x, coord y);
virtual int wm_keydown(int key);
virtual int wm_command(gui_window *win, int cmd, int param) { return(0); };
virtual int wm_cansize(coord x, coord y);
virtual int wm_size(coord x, coord y, int cansize);
virtual int wm_sizechanged(void) { return(0); }
virtual int wm_update(int msdelta) { return(0); }
protected:
virtual void copy(gui_window &r); // deep copies one window to another
gui_window *m_pParent;
uti_pointerarray m_subwins;
uti_rectangle m_position;
// active and inactive colorsets
gui_wincolor m_activecolor;
gui_wincolor m_inactivecolor;
// window caption
uti_string m_caption;
};
The whole GUI system will be contained within one global static variable - the root window. To be on the safe side, I encapsulated this variable within a global GetDesktop() function.
Now that we've got the header, let's start filling in some functions, starting with the Window Management code...
[size="5"]Window Management
/****************************************************************************
addwindow: adds a window to this window's subwin array
****************************************************************************/
int gui_window::addwindow(gui_window *w)
{
if (!w) return(-1);
// only add it if it isn't already in our window list.
if (m_subwins.find(w) == -1) m_subwins.add(w);
w->setparent(this);
return(0);
}
/****************************************************************************
removewindow: removes a window from this window's subwin array
****************************************************************************/
int gui_window::removewindow(gui_window *w)
{
w->setparent(NULL);
return(m_subwins.findandremove(w));
}
/****************************************************************************
bringtotop: bring this window to the top of the z-order. the top of the
z-order is the HIGHEST index in the subwin array.
****************************************************************************/
void gui_window::bringtotop(void)
{
if (m_parent) {
// we gotta save the old parent so we know who to add back to
gui_window *p = m_parent;
p->removewindow(this);
p->addwindow(this);
}
}
/****************************************************************************
isactive: returns true if this window is the active one (the one with input
focus).
****************************************************************************/
bool gui_window::isactive(void)
{
if (!m_parent) return(1);
if (!m_parent->isactive()) return(0);
return(this == m_parent->m_subwins.getat(m_parent->m_subwins.getsize()-1));
}
The only thing interesting in the add / remove window functions is the question, "who is responsible for the window pointer?" This is always a good question to ask yourself in C++. Addwindow and removewindow both take pointers to a window class. This means that to create a new window, your code news it, then passes the pointer to the parent (desktop) window through addwindow(). So who's responsible for deleting the pointer you newed?
My answer was "the GUI doesn't own the window pointers; the game itself is responsible for adding them." This is consistent with the C++ rule of thumb that says "those who new things also delete them."
The alternative to the method I chose was to say "the parent window is responsible for the pointers of all his child windows." That would mean that to prevent memory leaks, each window must, in it's (virtual) destructor (remember, there's derived classes), loop through its m_subwindows array and delete all of the windows contained within it.
If you decide to implement a GUI-owns-pointer system, be aware of an important trade-off - all of your windows must be dynamically allocated (newed). A quick way to crash a system like that is to pass in the address of a variable on the stack, i.e. say something like "addwindow(&mywindow)", where mywindow is declared as a local variable on the stack. Things will work until mywindow goes out of scope, or until the destructor for the parent window is called, whereupon it'll try to delete that address and all hell will break loose. The lesson is "be extra careful with pointers."
That's the main reason behind why I decided that my GUI would not own the window pointer. If you're passing a lot of complex window classes into and out of your GUI (say, for example, you're populating a tabbed property sheet), you might prefer a system where the GUI doesn't keep track of the pointers, and where remove simply means "the pointer is now in my control; remove it from your array, but don't delete it." This would also allow you to (carefully) use addresses of local variables on the stack, provided you made sure that you removewindow()'ed them before they went out of scope.
Moving on... Showing and hiding windows is accomplished through a boolean variable. Showwindow() and hidewindow() simply set or clear this variable; the window drawing and message processing functions check this "is window shown" flag before they do anything. Pretty easy stuff.
Z-ordering was also fairly easy. For those unfamiliar with the term, z-ordering refers to the "stacking" of windows on top of each other. At first thought, you may decide to implement z-ordering similar to how DirectDraw does it for overlays - you might decide to give each window an integer that describes its absolute z-order position, their place on the z axis - say, maybe, 0 is the top of the screen, and negative -1000 is furthest back. I thought a bit about implementing this type of z-ordering, but decided against it - absolute z-order positions don't concern me; I care more about relative z-order positions. That is, I don't really need to know "how far back" one window is from another; I simply need to know whether a given window is behind another, or in front of it.
So, I decided to implement z-order like this: The window with the highest index in the array, m_subwins, would be the window "on top." The window at [size-1] would be directly under it, followed by [size-2], etc. The window at position [0] would be on the very bottom. In this way, processing z-ordering became very easy. Also, killing two birds with one stone, I deemed that the topmost window would always be the active window, or more technically, the window with input focus. Although this restricted my GUI from making "always on top" windows (for example: Windows NT's task manager is always on top of all other windows, regardless of who has the input focus), I felt it was worth it to keep the code as simple as possible.
Also, I paid a small price for using array indices as z-orders was the array shuffle that occurs when I tell a given window to move to the top of the z-order. Say I tell window #2 to move to the top of a 50 window list; I've got to shift 48 windows down a slot to accommodate window #2's new position at the end. The good news is that moving a window to top of the z-order isn't really a time-critical function, and even if it were, there's dozens of good, quick ways to juggle array items like this - linked lists spring to mind.
Check out the cheap trick I used in the bringtotop() function. Since I know that the window doesn't own the pointers, I can just clobber the window and then immediate re-add him, effectively repositioning him at the top of the array. I did this solely because my pointer class, uti_pointerarray, already had code that would delete an element and slide all higher elements backwards one slot.
So that's window management. Now, onto the joy of coordinate systems...
[size="5"]Coordinate Systems
/****************************************************************************
virtual coordinate system to graphics card resolution converters
****************************************************************************/
const double GUI_SCALEX = 10000.0;
const double GUI_SCALEY = 10000.0;
int gui_window::virtxtopixels(int virtx)
{
int width = (m_parent) ? m_parent->getpos().getwidth() : getscreendims().getwidth();
return((int)((double)virtx*(double)width/GUI_SCALEX));
}
int gui_window::virtytopixels(int virty)
{
int height = (m_parent) ? m_parent->getpos().getheight() : getscreendims().getheight();
return((int)((double)virty*(double)height/GUI_SCALEY));
}
/****************************************************************************
findchildatcoord: returns the top-most child window at coord (x,y);
recursive.
****************************************************************************/
gui_window *gui_window::findchildatcoord(coord x, coord y, int flags)
{
for (int q = m_subwins.getsize()-1; q >= 0; q--)
{
gui_window *ww = (gui_window *)m_subwins.getat(q);
if (ww)
{
gui_window *found =
ww->findchildatcoord(x-m_position.getx1(),
y-m_position.gety1(), flags);
if (found) return(found);
}
}
// check to see if this window itself is at the coord - this breaks the recursion
if (!getinvisible() && m_position.ispointin(x,y))
return(this);
return(NULL);
}
What this really meant was that I needed to implement a virtual coordinate system, just like Microsoft Windows. I based my virtual coordinate system around an arbitrary number - I effectively said, "Henceforth, I will assume that every window is 10,000x10,000 units, regardless of the actual size of that window," and then let my GUI do the work of scaling the coordinates. For the desktop window, the coordinates are scaled to the physical resolution of the monitor.
I accomplished this through four functions: virtxtopixels(), virtytopixels(), pixelstovirtx(), and pixelstovirty(). (Note: only two are listed in the code; I figured you got the idea). These functions are responsible for converting between the virtual 10,000x10,000 unit coordinates and either the actual dimensions of the parent window, or the physical coordinates of the monitor. Obviously, the rendering functions of the windows use these functions heavily.
The screentoclient() function is responsible for taking an absolute screen position and converting it into relative virtual coordinates. Relative coordinates have their origin at the upper-left of a window; it's the same idea as world space and object space, in 3D. Relative coordinates are indispensable for dialog boxes.
All coordinates in the GUI system are relative to something. The only exception to this is the desktop window, whose coordinates are absolute. This relative way of doing things ensures that child windows move when their parents do, and that the structure of dialog boxes is consistent as the user drags them to different locations. Also, because our entire virtual coordinate system is relative, when a use stretches or shrinks a dialog box, all of the controls within that dialog will stretch and shrink also, automatically trying their best to completely fill up their new dimensions. This is an amazing trait, for those of us who have ever tried to do the same thing in Win32.
Finally, the findchildatcoord() function takes a (virtual) coordinate and determines which child window (if any) is under that coordinate - useful, for example, when a mouse button is clicked, and we need to know which window to send the button click event to. The function works by looping through the subwindow array backwards (remember, the topmost window is at the back of the array), doing some rectangle geometry to see if the point is in that window's rectangle. The flags parameter provides some extra conditions for determining if a "hit" occurred; for example, when we start implementing controls, we'll realize that it's often useful to prevent label and icon controls from registering a "hit," instead giving the windows beneath them a chance at the test - if a label is placed on top of a button, the user can hit the button, even if technically, they're clicking on the label. The flags parameter controls those special cases.
Now that we've got some coordinates, we can finally begin to draw our window...
[size="5"]Window Drawing
Recursion is a double-edged sword. It makes the window drawing code very easy to follow, but it also ends up touching pixels twice, which can be a significant performance hit (say, for example, you have a stack of fifty windows, all the same size and at the same screen position - the code will run through the drawing loop fifty times, and touch the same set of pixels fifty times). This is a notorious problem. There are certainly hidden-surface elimination algorithms one could apply to this situation - in fact, this is an area I need to spend some time with on my own code - Quaternion's GUI is most active during the non-game screens (title, closing, etc.), places where it's perfectly OK for the GUI to be a hog, because there isn't anything else going on.
But, I am tinkering with it; I'm currently trying to employ the DirectDrawClipper object in my drawing routines. So far, the initial code looks pretty promising. Here's the way it will work: The desktop window "clears" the clipper object. Each window then draws is subwindows backwards, top one first, bottom one last. After each window is drawn, it adds its screen rectangle to the Clipper, effectively "excluding" that area from the windows below it (yes, this assumes all windows are 100% opaque). This helps to ensure that at the very least, each pixel will be touched only once; granted, the code is still churning through all of the calculations and calls required for GUI rendering, (and the clipper's probably got its hands full, too), but at least the code isn't actually drawing redundant pixels. Whether the clipper object operates fast enough to make this worthwhile remains to be seen.
I'm tossing around several other ideas, too - perhaps using the built-in z-buffer on the 3D graphics card, or implementing some sort of dirty rectangle setup. If you've got any ideas, let me know; or, try them yourself and let me know what you found.
Most of the bulk of the window drawing code I cut out, because it's very specific to my situation (it calls my custom sprite classes). Suffice it to say that once you know the exact screen dimensions of where you're going to draw a window, the actual drawing code is straightforward (and fun) to implement. Fundamentally, my drawing code takes a set of nine sprites - four for the corners, four for the edges, one for the background - and uses those sprites to draw the window.
The color sets deserve a small explanation. I decided that each window would have two unique color sets; one set for when that window is active, one set for when it's not. Before the drawing code gets started, it makes a call to getappropriatecolorset(), which returns the correct color set for the window's activation status. Having separate colors for active and inactive windows is a basic principle of GUI design; it was also fairly easy to implement.
Now our windows draw, so it's time to start looking at messaging....
[size="5"]Window Messages
This section is the core of GUI implementation. Window messages are the events that get sent to a window when the user performs certain actions - clicking the mouse, moving it, hitting a key, etc. Some messages (like wm_keydown) are sent to the active window, some (wm_mousemove) are sent to the window the mouse is over, and some (wm_update) are always sent to the desktop, regardless.
Microsoft Windows has a message queue. My GUI does not - when calcall() figures out that it needs to send a message to a window, it stops right there and "sends" it - it calls the appropriate wm_xxxx() virtual function for that window. I've found that this method is just fine for simple GUIs. Unless you have a really good reason, don't bother with implementing a full-blown message queue, storing things into it, and having separate threads pick up the messages and dispatch them. For most game GUIs, it isn't worth it.
Also, notice that the wm_xxxx()'s are all virtual functions. This is where C++'s polymorphism is working for us. Need to change how certain types of windows (or controls - say, buttons) deal with a "left mouse button has just been pushed down" event? Simple, derive a class from the base window and override its wm_lbuttondown() method. The system will automatically call the derived class's method where appropriate; behold the power of C++.
As much as I'd like to, I can't go into very much detail about calcall(), the function that polls all the input devices and sends out the messages. It does many things, and implements many behaviors that are specific to my GUI. For example, you might want your GUI to behave like X-Windows, where the window the mouse is over is always the active window. Or, you might want to make the active window system modal (meaning nothing else can happen until the user gets rid of it), like several Mac-based programs do. You might want the ability to move windows by clicking anywhere in them, instead of just in their title bar, like WinAmp. The implementation of calcall() will vary wildly depending on which behaviors you decide to incorporate into your GUI.
I'll give you a hint, though - the calcall() function is not stateless, in fact, your calcall() function will probably end up being a rather complex state machine. The perfect example of this is dragging and dropping things. In order to properly calculate the difference between a normal "mouse button released" event, and a similar but completely different "whatever the user was dragging has just been dropped" event, calcall() must maintain a state. If you're rusty on finite state machines, save yourself a lot of headaches and brush up on them before you tackle calcall()'s implementation.
The wm_xxx() functions included in the window header file were the ones I felt represented the minimum set of messages a GUI would need to calculate and dispatch. Your needs may differ, and there's no reason why you have to stick to the Microsoft Windows set of messages; if a custom message would be perfect for you, now's the time to implement it.
[size="5"]Window Messages
In the first part of this article I PDL'd out a function called CApplication::RenderGUI(), the master function behind calculating and drawing our GUI:
void CApplication::RenderGUI(void)
{
// get position and button status of mouse cursor
// calculate mouse cursor's effects on windows / send messages
// render all windows
// render mouse
// flip to screen
}
void CApplication::RenderGUI(void)
{
// get position and button status of mouse cursor
m_mouse.Refresh();
// calculate mouse cursor's effects on windows / send messages
GetDesktop()->calcall();
// render all windows
GetDesktop()->renderall();
// render mouse
m_mouse.Render();
// flip to screen
GetBackBuffer()->Flip();
}
In the next article, Part III, we're going to be doing dialog controls. Buttons, textboxes, progress bars... the works. Until then...