现在的位置: 首页 > 综合 > 正文

Developing a GUI in C++ and DirectX

2012年09月03日 ⁄ 综合 ⁄ 共 42732字 ⁄ 字号 评论关闭
 

Developing a GUI in C++ and DirectX

Mason McCuskey

Spin Studios

www.spin-studios.com

 

 

 

Introduction

 

At first glance, it may seem like I’m reinventing the wheel; Windows already comes with a very complex, very functional GUI. Unfortunately, while the Windows GUI is great for office apps, quite frequently, it’s not suited for many games. Games tend to want a more precise control over the GUI than Windows can provide (for example, games may want to use alpha-blending to implement partially transparent windows – easy if you’ve written your own GUI, but next to impossible using the Windows GUI). 

 

This article will walk you though how to create a GUI using C++ and DirectX. The series is divided into several parts, each dealing with a specific aspect of GUI programming:

 

Part I:        The Basics, and the Mouse

Part II:       Windows

Part III:      Controls

Part IV:      Resource Editors and Other Madness

 

NOTE: This document was originally four separate articles on www.gamedev.net.   I’ve concatenated all four into one for the XGDC, but they remain otherwise unchanged. - Mason

 

Part I: The Basics, and the Mouse

 

Before I get started, I want to throw out a disclaimer: I’m going to outline the approach I used when I created the GUI system for my upcoming title, Quaternion. Treat this text as one solution to a very intricate problem, nothing more. I’m not saying that this way of making a GUI is the fastest or easiest way; I’m simply outlining a solution that worked for me. Also, this text is not complete. Like one of those Bob-Vila TV episodes, it skips over the easy stuff and concentrates on the interesting. There is no attached source file; there are code snippets in the text, and that’s it.   In that code, I’ve stripped out a lot of layers of indirection that aren’t relevant to what I’m trying to show (i.e. the wrappers that you’d probably have for your DirectX functions, non-relevant initialization and cleanup code, etc). Also, beware of bugs – I’ve done lots of bug checking, but I’m only human. If you find a bug, please let me know about it by emailing mason@spin-studios.com.

 

I’m making several assumptions about your knowledge. I’m assuming you know the basics of how event-driven programming works (message queues, etc), and I’m assuming you have a strong grasp of PDL (the commenting language – if you don’t know what this is, read Code Complete, by Steve McConnell), and C++. I used C++ to implement my GUI system, because I’m a card-carrying member of the C++ fan club, and because the OOP of C++ work great for implementing window and control types.   Shameless plug for the C++ language: Note the power of OOP in this solution, and ask yourself if you could do the same thing as easily in C.

 

Let’s start by defining our scope. It’s important to realize up front that we’re not remaking Windows 95, we’re just trying to get a simple GUI up for a game, so we don’t have to implement every single control and GUI construct. We only need a few parts for this simple GUI: a mouse pointer, a generic window, and some dialog controls to place within that window.   We’re also going to need a resource editor, a program that will allow us to design dialogs by graphically dropping controls at various places.

 

Start with the basics – The Rendering Loop

 

I’m going to start at the top, by defining a function that will calculate and draw one frame of our GUI system. Let’s call this function RenderGUI(). In PDL, RenderGUI does something like this:

 

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

}

 

Pretty straightforward for now. Basically, we grab the new position and status of the mouse cursor, calculate any changes that are caused by the new position, render all our windows, render the mouse cursor, then push the whole thing to the screen.

 

The Mouse

 

Now that we’ve got a main function, we’re going to create a mouse class. This mouse class will initialize the rodent, and will be responsible for querying its position and storing the results. Here’s the definition:

 

class CMouse {

 

public:

 CMouse(); // boring

 ~CMouse(); // boring

 

 int Init(LPDIRECTINPUT di); // we’ll talk about this later

 int Refresh(void); // we’ll talk about this later

 

 int GetButton(int index)

 {

    if (index < 0 || index > NUMMOUSEBUTTONS) return(0);

    return(m_button[index]);

 }

 

 CPoint GetPosition(void) { return(m_position); }

 

 enum { NUMMOUSEBUTTONS = 3 }; // three button mouse

 

private:

 LPDIRECTINPUTDEVICE m_mousedev;

 char m_button[NUMMOUSEBUTTONS]; // state of buttons

 CPoint m_position; // actual screen position

};

 

Pretty straightforward class definition. We’ve got two data pieces, m_button and m_position, abstracted by two functions, GetButton and GetPosition(). Then we’ve got Init and Refresh functions, which initialize the mouse and Refresh its button and position information. The m_mousedev is an interface to our mouse device; we get this interface during Init(), and use it in Refresh to communicate with DirectInput.

 

So… before we go any further with CMouse, let’s look at the code to initialize DirectInput. Note that this code doesn’t belong in our CMouse::Init() routine; the DirectInput pointer is used by the entire game, not just the mouse, so the code that inits DirectInput should go in your main init function – the same time you init DirectDraw, DirectSound, etc. A DirectInput interface pointer is different than a DirectInput device pointer; you use DirectInput pointers to get DirectInputDevice pointers.

 

Here’s the code to initialize the master DirectInput interface pointer:

 

LPDIRECTINPUT di = NULL;

hr = DirectInputCreate(hinst, DIRECTINPUT_VERSION, &di, NULL);

if (FAILED(hr)) {

 // error processing

}

 

That will put a valid DirectInput interface pointer into di. (Don’t forget to Release() it when your game ends!)

 

Now that we’ve got a DirectInput interface, let’s begin fleshing out our CMouse by implementing CMouse::Init(). 

 

bool CMouse::Init(LPDIRECTINPUT di)

{

 // Obtain an interface to the system mouse device.

 hr = di->CreateDevice(GUID_SysMouse, (LPDIRECTINPUTDEVICE*)&di_mouse, NULL);

 if (FAILED(hr)) { /* handle errors! */ }

 

 // Set the data format to "mouse format".

 hr = m_mousedev->SetDataFormat(&c_dfDIMouse);

 if (FAILED(hr)) { /* handle errors! */ }

 

 // Set the cooperativity level

 hr = m_mousedev->SetCooperativeLevel(hwnd, DISCL_NONEXCLUSIVE | DISCL_FOREGROUND);

 if (FAILED(hr)) { /* handle errors! */ }

}

 

That code does three important things. First, it gets a valid DirectInput mouse device interface, and puts it in di_mouse. Next, it sets the data format and the cooperative level for the device, basically letting windows know that we want to query the device as if it were a mouse, and that we don’t want to take exclusive ownership of it. (Exclusive ownership means that we’re the only app that can use the mouse – by specifying DISCL_NONEXCLUSIVE, we’ve told Windows that we’re going to be sharing the mouse with other applications.)

 

Now let’s flesh out CMouse::Refresh(), the function responsible for updating the CMouse’s internal button state and position variables. Here’s the code.

 

void CMouse::Refresh(void)

{

 C done = 0;

 int q;

 HRESULT hr;

 POINT p;

 DIMOUSESTATE dims;

 

 if (!m_di) return;

 

 // clear our struct – eventually, directinput will fill this in

 memset(&dims, 0, sizeof(DIMOUSESTATE));

 

 if (!m_mousedev) return; // we don’t have a pointer! Bail!

 

 while (!done)

 {

    hr = m_mousedev->GetDeviceState(sizeof(DIMOUSESTATE), &dims);

    if (FAILED(hr))

    {

      if (hr == DIERR_INPUTLOST || hr == DIERR_NOTACQUIRED)

      {

        // device lost... reacquire

        hr = m_mousedev->Acquire();

        if (FAILED(hr))

        {

          // houston, we have a problem... clear & bail

          clear();

          done=1;

        }

      }

      else

      {

          // it’s some other error - clear and bail!

          m_mousedev.clear();

          done = 1;

      }

    }

    else // read mouse successfully!

    {

      done = 1;

    }

 } //while !done

 

 m_position.z += dims.lZ;

 

 if (m_vga->isfullscreen())

 {

    // we're in fullscreen, so this is easy... just copy the coords

    m_position.x += dims.lX;

    m_position.y += dims.lY; 

 }

 else

 {

    // we're in window mode, so this is not-so-easy...

    // grab the relative mouse position

    GetCursorPos(&p);

    ScreenToClient((HWND)m_vga->gethwnd(), &p);

 

    if (p.x < 0 || p.y < 0)

    {

      // the cursor is out of our window! "hide" it!

      m_mousedev.setposition(KJM_AXES_X, m_vga->getscreendims().getwidth());

      m_mousedev.setposition(KJM_AXES_Y, m_vga->getscreendims().getheight());

    }

    else

    {

      m_mousedev.setposition(KJM_AXES_X, p.x);

      m_mousedev.setposition(KJM_AXES_Y, p.y);

    }

 }

 m_mousedev.constrainpos(KJM_AXES_X, 0, m_vga->getscreendims().getwidth());

 m_mousedev.constrainpos(KJM_AXES_Y, 0, m_vga->getscreendims().getheight());

 

 for (q=0; q < KJM_NUMMOUSEBUTTONS; q++)

 {

    m_mousedev.setbutton(q, (dims.rgbButtons[q] & 0x80));

 }

}


Part II – Windows

 

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. 

 

The Design

 

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:

 

1)       First I coded some basic window management code. This chunk of code is responsible for the window tree, adding / deleting windows (i.e., new’ing and deleting window pointers), 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.

 

2)       Once I had basic window management functions, I spent some time thinking about my coordinate systems. Coded up some ClientToScreen() functions, and some other misc. stuff.

3)       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 (see diagram) <<DIAGRAM>>. 

 

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. 

 

4)       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.

 

5)       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.

 

The Implementation

 

Here we go. Here’s a good start for our base-class window definition:

 

class gui_window

{

public:

 gui_window(); // boring

 virtual ~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;

};

 

First of all, notice the virtual destructor on the window class. This may not seem like it’s needed just yet, but we’ll eventually be deriving controls from this class, so it’s important that it have a virtual destructor.

 

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.

 

Also, notice that the class definition is rife with virtual keywords. 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++.

 

Now that we’ve got the header, let’s start filling in some functions, starting with the Window Management code…

 

Window Management Code

 

/****************************************************************************

 

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

 

What this really meant was that I needed to implement a virtual coordinate system. 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 Code

 

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.

 

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.

 

Wrapping It Up

 

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

 

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

 

Part III: Implementing Controls

 

This section doesn’t have as much code as the others – this is mainly because we programmers are fairly picky when it comes to the appearance of our GUI. We like to code things up so that our buttons, our textboxes, and our GUI appear unique, and fit our own aesthetic tastes.   Consequently, everyone’s control code will be slightly (or maybe drastically different), and it wouldn’t make sense to include my particular drawing code. Besides, writing code to draw all the GUI elements is fun, in fact, in my opinion, it’s the most fun you can have implementing a GUI. Go wild.

 

That being said, let’s start by determining which GUI controls we need.

 

GUI Controls We’ll Need

 

I didn’t want to spend a lot of time implementing controls for my game GUI; I wanted to stick with the smallest set of controls that I could. So, I came up with a list of controls that I consider the minimum set for game GUIs…

 

·         Static Text, Icon, and Group Boxes – vital. These controls label and group the other controls in a dialog box. The static control is crucial; the frame control we could probably live without, but it’s fairly simple, and in some cases can go a long way towards making a dialog box easy to navigate, so I’m including it. Icon controls should be simple, but should be able to animate, providing cool background animations in our dialogs and menus (ala Theif: The Dark Project).

 

·         Buttons and Checkboxes – vital. Weird button types (flat buttons, pushbutton-style radio buttons) we can do without, but most games can’t live without a basic button and checkbox.

 

·         List control – important. I’ve found list controls, especially multi-column list controls, indispensable when creating game GUIs. They’re used everywhere. You’re going to want a very intelligent, heavyweight list control, as good or better than the Windows List Control. For me, the list control was the most difficult control to implement.

 

·         Sliders and scrollbars – Important. Famous for controlling sound and music volume. The bad news is that we’ll probably need horizontal and vertical flavors of these guys; the good news is that they’re so similar you can implement them very easily.

 

·         Textboxes – Vital. You have to be able to enter your mega-3l33t, super-kewl player handle somewhere, right? 

 

·         Progress Bars – Essential for displaying hit points, “I’m almost done loading!”, etc.

 

Noticeably absent from this list are the spin button controls (which aren’t crucial, and irritate me to no end anyway), radio buttons (we can get by with a single selection listbox instead), and the drop-down combo box (again, we can just use a list box), and tree control. By making the listbox control smart enough to indent certain items, we can incorporate the functionality of the tree control

 

Tab controls aren’t included simply because my game doesn’t have enough of a GUI to warrant them, though your needs may differ.

 

Even with all the omissions, the “minimum” list might seem daunting at first, but we can simplify it quite a bit…

 

Breaking It Down: Complex Controls As Combinations of Simple Ones

 

The list becomes much more manageable when we realize that the more complex controls are just clever combinations of other, more simple controls. For example, a scrollbar is basically just two buttons and a slider control. A checkbox is a static control and two buttons (one “off” button, and one “on” button).  A plain old button could be implemented using three static icon controls (just show/hide the appropriate ones to get the button to “press”), so that you can reuse your drawing code. If you were really strapped for time, you could even implement a progress bar as a slider that’s moved by the computer, though I prefer having a separate control for this.

 

There are, however, disadvantages to this – namely, your GUI controls are going to take up more system resources than they really need. Think about it – each control is a window. Let’s say you’ve gone with the reuse philosophy, and have created a button control that’s really just three different statics. That’s three windows per button. Now, you build a scrollbar control, using two button controls. That’s six windows per scrollbar control. Build a List control using horizontal and vertical scrollbars, and you’re sitting at twelve windows per list. It adds up quickly.

 

So it’s really just another example of the classic tradeoff between “how fast can I develop it” and “how little resources can I get it to use?” If you need a very high performance, no-waste GUI, implement each control from the ground up. If you would instead a quick implementation, and don’t mind the performance hit, you might choose to implement your controls so that the only control that would actually draw to the screen would be the static, and all other controls would be made up of combinations of statics.

 

When building my GUI, I tried to create a good balance between these two extremes.

 

Now, let’s dive into the actual implementation of each control, starting with everyone’s favorite, the static label.

 

The Static Controls

 

There are three kinds of static controls we’ll be looking at: static text controls, static icon controls, and frame controls.   All three of these controls are very easy, because they take no messages – all they do is draw themselves at certain positions.

 

Static text controls are by far the easiest control you’ll ever implement – just draw your window’s caption at the upper-left of your window, and you’re done. If you’re especially through, you might want to add code to justify your text a certain way – for example, to center your text in your client rect, you might employ the classic centering algorithm – take the width of your window, subtract the width of the text you’re going to draw, and divide by two, telling you how many pixels “in” (that is, how many pixels right from the left window edge) to start drawing.

 

Static icon controls are a little tougher. Actually, the term “static icon control” is a bit of a misnomer, given that we want our icon controls to be able to animate. Even so, implementation of these icon controls isn’t tough, provided you’ve got a solid sprite library to handle all the details of implementing animation: checking the millisecond delta between this frame and the one that’s on the screen now, using this delta to determine how many frames your sprites should advance by, etc. 

 

Icon controls only become painful to implement if you’re not redrawing your entire GUI system every frame. In this case, you’ve somehow got to deal with clipping the icon control, so that even though it’s being drawn every frame, it doesn’t accidentally overwrite pixels belonging to a window that’s sitting on top of it (but wasn’t changed, so therefore wasn’t drawn). I didn’t implement this – my GUI gets redrawn every frame – but if you’re faced with this problem, you might want to try setting up a clip list for each icon, using it to draw the icon, and re-evaluating it when any window is moved, closed, or opened. This may or may not be a viable solution – I just dreamt it up while writing this – but it seems to be at least a good jumping off point.

 

Frame controls are also pretty straightforward. I implemented my frame control by drawing a border around m_position, then drawing the window caption at about position (5,5), in client coordinates (that is, about five pixels right and five pixels down from the upper-left of the frame control), but you may decide you want something a little fancier. 

 

The one complex thing you might want to do for your static controls is to change the behavior of the findwindow function slightly so that it “skips” all windows that are static controls. That way, if a static text control is sitting on top of a pushbutton, the user will be able to push the button “through” the static control.

Speaking of, let’s now take a look at how to implement that button.

 

Pushbutton Controls

 

Pushbuttons are only slightly more difficult than static controls. Your pushbutton control needs to keep track of whether it’s “pressed” (pushed down) or “unpressed.” It does this by implementing two virtual functions, wm_mousedown() and wm_mouseup(), which your main calcall() function needs to call when appropriate. 

 

Basically, in wm_mousedown(), you set a boolean variable, which I call the “depressed flag,” to true, and in wm_mouseup(), you set it back to false. Then, in your drawing code, if the depressed flag is set, you draw the button “pressed,” otherwise, you draw it “unpressed.”

 

Next, add an additional condition –say, “only draw the button depressed if the depressed flag is set, AND, the mouse cursor is within my client coordinates, otherwise, set the depressed flag back to false.” This will give you buttons that “pop out” if you move your mouse cursor off of them, and is very important for accurately determining when a button is clicked.

 

In normal GUIs, when a button is clicked, it fires off an event to its parent window, which then does whatever the button represents – i.e., clicking the close button will close the window, clicking the save button will save the file, whatever. My GUI considers a button clicked if and only if, inside wm_mouseup(), the depressed flag is set. The only way the depressed flag can still be set inside mouseup() is if the user both pressed and released the mouse button while the pointer was inside the button. This allows users to “bail out” at the last minute by holding the button down and dragging the mouse pointer somewhere outside of the button to cancel the button click, just like in any other GUI system.

 

That’s pushbuttons. Now, let’s take a peek at text boxes.

 

Carets and The Textbox Control

 

I chose to implement a very simple textbox control. It just captures keys, and does no scrolling - but you might want something more complex, say, a control that accurately handles the home, end, insert, and delete keys, or maybe even one with support for cutting, copying, and pasting, via the windows clipboard.

 

But before we can have a textbox, we need a caret. In case you’re not familiar with the terminology, a caret is another word for a cursor – that’s right, that little blinking vertical bar. Carets tell the user where the text they type is going to go.

 

For the purposes of my GUI, I’ve made things simple – I’ve ordained that the active window is the window that has the caret, period. This is how most GUIs behave, and seems to be the best solution. Also, my GUI, like Windows, considers the “caption” of the textbox to be the text that’s actually in the box.

 

So how do you implement the caret? Well, I decided that since it’s a given that the caret always going to be drawn inside the active window, and that the caret will only appear if the active window is a textbox, it makes sense to consider the caret part of the textbox and implement it inside the textbox’s draw function. This makes it really easy to implement – simply use an integer to represent an index into the character array of the window caption, and your textbox has all the information it needs to properly draw the caret.

 

Which means, basically, that if you’re a textbox, all you have to do to render yourself is draw a border around your client area, draw your window caption inside this border, and then, if you’re the active window, draw your caret at the correct position. In my GUI, the maximum length of a string inside a textbox is governed by the size of the textbox window, meaning that I don’t have to deal with scrolling the text within the box. You, however, might want to some way for the user to scroll through the contents of a textbox, allowing them to enter a very long string in a very small box.

 

By far the most difficult thing about a textbox is the keyboard processing that comes with it. Once we have a key, it’s easy to create a wm_keypressed() virtual function, and call it, and it’s easy to implement the textbox handler for wm_keypressed() so that it processes the key, and either tacks it onto the end of the window caption, or processes special keys (backspace, etc. – this is where your heavyweight string class pays for itself), and moves the caret.

 

The hard part is getting the key in the first place. Windows provides no less than three completely different ways to query the keyboard – the WM_KEYDOWN event, the GetKeyboardState() and GetAsyncKeyState() functions, and of course, DirectInput. I used the DirectInput method, simply because I already had done most of the heavy-lifting associated with DirectInput back when I implemented the mouse cursor, and because getting the keyboard state through DirectInput seemed to me the cleanest and most elegant solution.

 

To use DirectInput’s keyboard functionality, the first thing you need to do is set up a keyboard device. This is incredibly similar to how we set up the DirectInput mouse device way back in Part I of this article. Basically, the only difference here is that instead of telling DirectInput to treat our new device as a mouse, we’re telling it to treat it as a keyboard (duh). If you’ve gone through DirectInput for the mouse, doing the same stuff again for the keyboard should be easy.

 

Once we’ve got our keyboard device we can query it.

 

To actual determine if a key was “hit” requires slightly more work. Basically, to determine which keys are pressed, you need two snapshots of all 101 key states – you need a snapshot from the last frame, and a snapshot from this frame. The keys that are down in this frame but not down last frame are the keys that have been “pressed,” and they’re the keys you should send out wm_keypressed() “messages” for.

 

Now, onto progress bars…

 

Progress Bars

 

Progess bars are just about as easy as static controls to implement, since they only take a few messages. 

 

Basically, you need to do two things with a progress bar – you need to tell it a min/max range, and you need to tell it to “step” some number of units. For example, say I wanted to put up a “Loading…” progress bar, because I had to load 100 different game resources. I would create a progress bar with a range of 0 to 100. I would initally set the progress bar to zero, then, whenever I loaded a game resource, I would “step” the progress bar by one unit. Whenever the progress bar was stepped, it would redraw itself, showing graphically how far along it was by displaying a bar whose length was proportionate to its client area.

 

Progress bars are very similar to scroll bars; in fact, it might make sense to implement your progess bars in terms of your scroll

抱歉!评论已关闭.