CSE 483 Windows Programming

Barbara Nostrand, Ph.D.

Electrical Engineering and Computer Science


Laboratory 4 - Implementing a Simple GUI

Adapted from an Article by Mason McCuskey of Spin Studioes

Introduction

At first glance,  it may seem like we're 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 games.  Games tend to want a more precise control over the GUI than Windows can provide (for example,  you 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 series 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.  They're meant to be read in order,  so you've picked the right article to start with. 

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) 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++ works great for implementing window and control types.  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 data,  render all our windows,  render the mouse cursor,  then push the whole thing to the screen. 

The Mouse

	// Developing a GUI Using C++ and DirectX
	// Source Listing 1
	// Mouse Class Description 

	class CMouse { 
	public: 
	  CMouse(); // boring 
	  ~CMouse(); // boring 
	  int Init(LPDIRECTINPUT di); // well talk about this later 
	  int Refresh(void); // well talk about this later 
	  int GetButton(int index) 
	  { 
	    if (index < 0 || index > NUMMOUSEBUTTONS) return(0); 
	    return(m_button[index]); 
	  } 
	  void clear(void); // sets all vars to zero 

	  // makes sure p is a valid on-screen point 
	  void ConstrainPosToScreenSize(CPoint &p); 
	  CPoint GetAbsPosition(void) { return(m_absposition); } 
	  CPoint GetRelPosition(void) { return(m_relposition); } 
	  enum { NUMMOUSEBUTTONS = 3 }; // three button mouse 
	private: 
	  LPDIRECTINPUTDEVICE m_mousedev; 
	  char m_button[NUMMOUSEBUTTONS]; // state of buttons 
	  CPoint m_absposition; // actual screen position 
	  CPoint m_relposition; // relative position 
	}; 

Pretty straightforward class definition.  We've got three data pieces,  m_button,  m_absposition and m_relposition,  abstracted by two functions,  GetButton,  GetAbsPosition(),  and GetRelPosition().  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. 

Absolute vs. Relative Position, and DirectInput

Why am I using DirectInput? It's a matter of taste, actually. There are two ways to get mouse data in Windows – from DirectInput (the way I'm about to show), and via a Win32 API function called GetCursorPos(). The primary difference is that DirectInput will give you "relative" mouse information – that is, the cursor's current position relative to its last position – whereas GetCursorPos will give you the absolute screen coordinates. Absolute positioning is great for GUIs; relative positioning is good when the mouse is used without a cursor, i.e., to look around in a FPS game. You can however, calculate relative from absolute, and vice versa.

I used DirectInput. This decision was made for many reasons, all of which are outside the scope of this article (three words: multiple-mice systems). GetCursorPos() may be a better solution for you – if that's the case, it should be easy to flesh out the mouse class. DirectInput is more tricky (and more interesting), so the rest of this article will be in DirectInput. 

Initializing DirectInput

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.  Make sure you understand this distinction.  Here's the code to initialize the master DirectInput interface pointer: 

  // Developing a GUI Using C++ and DirectX 
  // Source Listing 2 
  // General DirectInput Initialization code 

  LPDIRECTINPUT di = NULL; 

  hr = DirectInputCreate(hinst, DIRECTINPUT_VERSION, &di, NULL); 
  if (FAILED(hr)) {
    // error 
    handle_error (); 
    } 
  // 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*)&m_mousedev, 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 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.)

Polling DirectInput for Mouse Status

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

// Developing a GUI Using C++ and DirectX 
// Source Listing 3 
// Polling DirectInput for Mouse Status 

void CMouse::Refresh(void) { 
  char done = 0; 
  int q; 
  HRESULT hr; 
  CPoint p; 
  DIMOUSESTATE dims; 
  
  // clear our struct  eventually, directinput will fill this in 
  memset(&dims, 0, sizeof(DIMOUSESTATE)); 
  if (!m_mousedev) return; // we dont have a pointer! Bail! 
  
  while (!done) { 
    // query DirectInput for newest mouse data 
    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)) { 
          // error handling goes here 
          clear(); 
          done=1; 
          } 
        } 
        else { 
          // its some other error  handle it 
          clear(); 
          done = 1; 
          } 
        } 
        else // read mouse successfully! { 
          done = 1; 
          } 
        } // end while loop  we've read DI correctly 
        
        // squirrel away newest rel position data 
        m_relposition.x = dims.lX; 
        m_relposition.y = dims.lY; 
        m_relposition.z = dims.lZ; 
        // now calc abs position from new relative data 
        m_absposition.z += dims.lZ; 
        m_absposition.x += dims.lX; 
        m_absposition.y += dims.lY; 
        
        // keep the mouse pointer on-screen... 
        ConstrainPosToScreenSize(m_absposition); 
        // get button data 
        for (q=0; q < NUMMOUSEBUTTONS; q++) { 
          m_button[q] = (dims.rgbButtons[q] & 0x80)); 
          } 
        } 

That code's doing a lot of things.  First,  it queries DirectInput for the new absolute mouse position (there's a while loop in there that will automatically retry the query if we've lost the interface).  Next,  it squirrels the absolute position data away in m_absposition,  then it "applies" the relative position to come up with the new absolute position.  The ConstrainPosToScreenSize() makes sure the point is within the bounds of the screen.  Finally,  it loops through and refreshes all the buttons. 

Drawing The Mouse

There's two main philosophies behind drawing the mouse cursor.  If you know that your entire screen will be refreshed with new pixel data every frame,  you can simply blt the mouse cursor at its new absolute position,  and be done with it.  A better,  solution,  however,  is to grab a copy of the pixel data under the mouse cursor before you blt it,  then,  when the mouse moves,  you erase your old blt by blting your saved pixel data back.  I prefer the second method. 

I'm not going to go into the gritty details of blitting surfaces and all that;  you should know how to do that. 

Threads and Tails

If you don't mind multithreading,  there's a better way to deal with the mouse than what's described here.  The method here is a one thread method,  which polls the mouse every frame.  Good for high frame rates,  but not so good for low frame rates – the mouse cursor will appear "sluggish." The best way to deal with the mouse is to start a separate "mouse-rendering" thread,  which continually monitors WM_MOUSEMOVE messages,  and takes care of updating and bltting the mouse cursor each time the mouse is moved.  The advantage to multithreading the mouse pointer in this manner is that your mouse will still be fluid regardless of how slow your game's frame rate is.  Having a separate thread for your mouse will make the game feel responsive,  regardless of frame rate. 

Also,  it should be obvious to you by now how to create mouse trails.  Keep the last several (five or ten) mouse cursor positions in an array.  Whenever the mouse moves,  discard the oldest coordinate,  move all the other coordinates down one slot,  and put the newest coordinate in the top slot.  Then,  if you want to get extra fancy,  use alpha-blitting to render the old coordinates with more transparency than the newer ones. 

Designing Windows

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. 

Implementing a GUI System

  1. First code 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.  stub out the window drawing procedure by simply drawing rectangles where the windows should be,  then draw 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 you have basic window management functions,  spend some time thinking about your coordinate systems.  Now,  code up some coordinate management functions.

  3. Next,  I tackle the window drawing code.  You can derive a "fancy window" class,  and show it how to draw itself using a set of sprites.  Sprites are typically simple 2D images with transparent backgrounds so that they can be overlaid on the screen.  In our example we have:  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. 

  4. Once the drawing code for the generic window was complete,  Start implementing the controls.  Coding controls is straightforward,  but again,  requires very thorough testing.  Start with the simple controls:  statics,  icons,  etc.,  and work your way up from there,  as explained earlier. 

  5. Finally, after all of your controls are complete,  code up a simple Resource Editor,  a program that allows someone to graphically place controls and layout dialog boxes.  (You should probably try to find a pre-built Resource Editor,  but this is a very instrucdtive one man month exercise.  So,  you may choose to construcdt your own sometime.  It is also possible to use text files to position stuff,  however this is rather akward. ) It's much easier to create dialog boxes graphically,  and it was a good exercise:  during development you may uncover bugs in your controls' code,  which may prove very difficult to catch in the actual game.  Regardless,  yous should plan on using MSVC++ reource (.RC) files or more modern Microsoft compatible formats. 

The Implementation

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

  // Developing a GUI Using C++ and DirectX 
  // Source Listing 1 
  // Window Class 
  
  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

  // Developing a GUI Using C++ and DirectX 
  // Source Listing 2 
  // 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 is 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.  You may wish to implement this type of z-ordering.  However,  first consider the merits of relative ordering.  With relative ordering,  you don't really need to know "how far back" one window is from another;  you simply need to know whether a given window is behind another,  or in front of it. 

You can implement a relative 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 becomes very easy.  Also,  killing two birds with one stone,  you can designate the topmost window as always being the active window,  or more technically,  the window with input focus.  Although this restricts your 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),  this approach can help keep your code as simple as possible. 

Also,  using array indices as z-orders requires an array shuffle when you tell a given window to move to the top of the z-order.  Say you tell window #2 to move to the top of a 50 window list;  you'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. 

Check out the cheap trick in the bringtotop() function.  Since the window doesn't own the pointers,  you can just clobber the window and then immediate re-add him,  effectively repositioning him at the top of the array.  This works, because the pointer class,  uti_pointerarray,  already has code that deletes an element and slides all higher elements backwards one slot. 

Coordinate Systems

  // Developing a GUI Using C++ and DirectX 
  // Source Listing 3 
  // 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 our GUI is resolution independence,  and what Mason McCuskey calls "stretchy dialog boxes." Basically,  if you want your windows and dialog boxes to scale themselves larger or smaller,  depending on the screen resolution of the system they are running on.  On systems with higher resolutions,  we want the windows,  controls,  etc.  to expand; on 640x480,  we want things to shrink.  This keeps their screen size relatively constant.  Also,  we want our graphics objects to be able to fill their parent windows,  regardless of the parent window's size. 

This means that we need to implement a virtual coordinate system,  just like Microsoft Windows.  We can base our virtual coordinate system around an arbitrary number.  We can say that,  "Henceforth,  we will assume that every window is 10,000x10,000 units,  regardless of the actual size of that window," and then let our GUI do the work of scaling the coordinates.  For the desktop window,  the coordinates are scaled to the physical resolution of the display. 

You can accomplish this through four functions:  virtxtopixels()virtytopixels()pixelstovirtx(),  and pixelstovirty().  (Note:  only two are listed in the code; ).  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 where you need to spend some time with on your 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. 

You may also want to employ the DirectDrawClipper object in your 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. 

You may want to use the built-in z-buffer on the 3D graphics card,  or implement some sort of dirty rectangle setup.  

Most of the bulk of the window drawing code is omitted from the example,  because it's very specific to a particular application (it calls 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,  the sample 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.  In the example,  each window defaults to 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 is 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.  The sample 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.  This method should work 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.  However,  many of these facilities are now available in libraries so you can use them without the headache of creating them for yourself. 

Also, notice that the wm_xxxx()'s are all virtual functions.  This is where C++'s polymorphism is working for us.  Suppose you 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++. 

Unfortunately,  we 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 the sample 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. 

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 are a fairly minimumal 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:

  // Developing a GUI Using C++ and DirectX
  // Source Listing 4
  // Render Pseudocode

  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: 

  // Developing a GUI Using C++ and DirectX 
  // Source Listing 5 
  // Wrapping it up.. 
  
  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 lab,  we're going to be doing dialog controls.  Buttons,  textboxes,  progress bars… the works.  Until then… 


Last modified: 2008 FEB 06
bnostran@syr.edu