Jump to content

  • Log In with Google      Sign In   
  • Create Account


Like
0Likes
Dislike

Developing a GUI Using C++ and DirectX Part 2

By Mason McCuskey of Spin Studios | Published May 17 2000 01:30 PM in DirectX and XNA

window windows int gui virtual code drawing coord functions
If you find this article contains errors or problems rendering it unreadable (missing images or files, mangled code, improper text formatting, etc) please contact the editor so corrections can be made. Thank you for helping us improve this resource

Welcome to Part II of the "Developing a GUI Using C++ And DirectX" Series. You can find Part I of the article here. Continuing with the overall theme (showing how I implemented a GUI for my upcoming game, Quaternion), this article will explain the many mysteries of windows. We'll look at how a window tree works, develop a plan to implement our GUI, and dive into the specifics of creating a window class, including drawing, messaging, coordinate systems, and all sorts of other madness.

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…


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.
Where to now? The rest of this article will explore the first two steps. Part III of this series will go into mind-numbing detail about coding controls. Part IV will talk a bit about implementing a resource editor and serializing windows.

So... let's start with step one: basic window management functions.


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;
};

As you peruse the functions we'll be talking about, keep in mind that recursion is everywhere. For example, our game will be drawing the entire GUI system by making a call to the renderall() method of the root window, which will in turn call the renderall() methods of its subwindows, which will call renderall() for their subwindows, and so on. Most of the functions follow this recursive pattern.

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…


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));
}

This set of functions deals with what I call window management; adding windows, deleting them, showing/hiding them, and changing their z-order. All of these are really just array operations; this is where your array class gets a workout.

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…


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);
}

One of the top priorities for my GUI was resolution independence, and what I call "stretchy dialog boxes." Basically, I wanted my windows and dialog boxes to scale themselves larger or smaller, depending on the screen resolution of the system they were running on. On systems with higher resolutions, I wanted the windows, controls, etc. to expand; on 640x480, I wanted things to shrink. Also, I wanted them to be able to fill their parent windows, regardless of the parent window's size.

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…


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….


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.


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
}

Now, finally, we're at a position where we can begin filling in some of that PDL. Check it out:

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();
}

Hopefully, seeing this code now will show you how things are starting to come together.

In the next article, Part III, we're going to be doing dialog controls. Buttons, textboxes, progress bars… the works. Until then…



About the Author(s)


Mason McCuskey is the programmer and fearless leader of Spin Studios, a six-person development team working on Quaternion, an action-puzzle game he hopes to eventually flying off the store shelves. He can be reached at [email="mason@spin-studios.com"]mason@spin-studios.com[/email].




Comments

Note: Please offer only positive, constructive comments - we are looking to promote a positive atmosphere where collaboration is valued above all else.




PARTNERS