Developing a GUI Using C++ and DirectX Part 3

Published May 17, 2000 by Mason McCuskey, posted by Myopic Rhino
Do you see issues with this article? Let us know.
Advertisement
Welcome back. This is Part III of the "Implementing a GUI in C++ And DirectX" Article. (Follow these links to part one and part two of the series). Continuing with the overall theme (showing how I implemented a GUI for my upcoming game, Quaternion), this article will walk through the creation of a few common controls you'll need for your GUI. We'll take a close look at several different control types, including pushbuttons, listboxes, textboxes, etc.

This section of the article 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.

One very important thing, before we get started - make your gui_window destructor virtual. I didn't mention it in Part II, because at that point we weren't subclassing any windows from gui_window, but I'm mentioning it now - making your destructors virtual for gui_window and all of its derivatives is a very smart thing to do, as it will ensure that no memory is leaked because of derived destructors not being called. Be wary of the traps of C++.

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


[size="5"]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...


    [size="5"]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.


    [size="5"]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. I've found this to be especially valuable if you're implementing "easy-move" windows - windows that you can move by grabbing anywhere, not just in the title bar, ala WinAMP.

    Now let's take a look at how to implement buttons.


    [size="5"]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.


    [size="5"]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 drawing code 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.

    Onto progress bars...


    [size="5"]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 initially 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 progress bars in terms of your scroll bars. I made my progress bar separate from my scroll bars simply because I wanted the two to have drastically different appearances and slightly different behaviors - your needs may be different.


    [size="5"]Sliders and Scrollbars

    Drawing a slider or a scrollbar is similar to drawing a progress bar, in that you need to express the slider's current position as a percentage of its client rectangle, giving you the position at which to draw the "pointer" (or, for a scrollbar, the elevator). You'll have to make some slight modifications for horizontal vs. vertical controls - I got around these by implementing a base class, gui_slider, which contained all of the common code, and all of the member variables, and then implementing two specific derivatives, gui_slider_horz and gui_slider_vert, which handled the differences in drawing and clicking logic.

    As for processing mouse clicks, I opted for the easy way when I created my sliders. If a mouse click occurs in the client area of a scrollbar, I automatically scroll directly to that position. In my sliders, you can't click in the "shaft" and move the position by a page at a time - you jump directly to where you clicked. This was a decision I made primarily because it was easy, but also because I dislike the default Windows behavior of paging.

    As for the scrollbar / slider logic, you've got the same basic setup as a progress bar - min, max, and current positions. Unlike a progress bar, however, the user can change the current position by clicking in the control.

    Now, scrollbars. I decided, for my GUI, that scrollbars are just sliders with two buttons tacked onto either side. These two buttons (the up/down or left/right arrows) move the elevator one position. This method eliminated a lot of code duplication between the pushbutton class and the scrollbars, and I would highly recommend that you take a look at doing something similar.

    Now that we've got scrollbars, we can tackle the most complex control of them all... the listbox.


    [size="5"]The Listbox Control

    Resign yourself to this now - the listbox control is where you're going to be spending the most time.

    // represents a column in our listbox
    class gui_listbox_column
    {
    public:
    gui_listbox_column() { }
    virtual ~gui_listbox_column() { }

    virtual void draw(uti_rectangle &where);

    void setname(const char *name) { m_name = name; }
    uti_string getname(void) { return(m_name); }

    int getwidth(void) { return(m_width); }
    void setwidth(int w) { m_width = w; }

    private:
    uti_string m_name;
    int m_width;
    };

    // an item in our listbox
    class gui_listbox_item
    {
    public:
    gui_listbox_item() { m_isselected = 0; m_indent = 0; }
    virtual ~gui_listbox_item() { }

    virtual draw(int colnum, uti_rectangle &where);

    void clearallcolumns(void); // boring
    void setindent(int i) { m_indent = i; }
    int getindent(void) { return(m_indent); }

    void settext(int colnum, const char *text); // boring
    uti_string gettext(int colnum = 0); // boring

    void setitemdata(unsigned long itemdata) { m_itemdata = itemdata; }
    unsigned long getitemdata(void) { return(m_itemdata); }

    void setselected(int s = 1) { m_isselected = s; }
    int getselected(void) { return(m_isselected); }

    private:
    int m_isselected;
    int m_indent; // # of pixels to indent this item
    unsigned long m_itemdata;
    uti_pointerarray m_coltext;
    };

    // the listbox itself
    class gui_fancylistbox : public gui_window
    {
    public:
    gui_fancylistbox() { m_multiselect = 0; }
    virtual ~gui_fancylistbox() { clear(); }

    int getselected(int iter = 0);

    virtual int wm_command(gui_window *win, int cmd, int param);
    virtual int wm_paint(coord x, coord y);
    virtual int wm_lbuttondown(coord x, coord y);

    gui_scrollbar_horz &gethscroll(void) { return(m_hscroll); }
    gui_scrollbar_vert &getvscroll(void) { return(m_vscroll); }

    virtual int wm_sizechanged(void); // the window's size has changed somehow

    gui_listbox_item *getitemat(int index); // boring
    gui_listbox_item *additem(const char *text); // boring
    int delitem(int index); // boring
    int delallitems(void); // boring
    gui_listbox_column *getcolumn(int index); // boring
    int addcolumn(const char *name, int width); // boring
    gui_listbox_column *getcolumnat(int index); // boring
    int delcolumn(int index); // boring
    int delallcolumns(void); // boring

    int clear(void); // delete columns & items

    int getnumitems(void);
    int getnumcols(void);

    void deselectall(void);
    void selectitem(int item);
    void selecttoggleitem(int item);

    void deselitem(int item);

    private:
    int m_numdispobjsy;
    int m_vertgutterwidth; // # of pixels between items vertically

    gui_scrollbar_horz m_hscroll;
    gui_scrollbar_vert m_vscroll;

    bool m_multiselect; // is this multi-selectable?
    uti_pointerarray m_items; // array of gui_listbox_items
    uti_pointerarray m_columns; // array of gui_listbox_columns
    };
    The listbox is by far the most complex control you'll make... but that's only because it's the most versatile. A good listbox control, capable of multiple columns, indenting, and multi-selection will prove practically indispensable in your game's GUI. Stop and think for a moment about all the places that listboxes are used in the average game, and you'll quickly see my point.

    I tackled my listbox control by splitting it up into two separate controls: a multi-column "report-style" list control, and an icon list control, which creates a view similar to what you'd see when selecting "large icons" in an explorer window.

    The icon list control was fairly easy to do. It kept track of a list of static icons (again, code reuse), all the same size. I divided the listbox width by the width of the icons, which gave me the number of columns available. (If it turned out that my listbox was smaller than the largest icon, I assume I have only one column, and let the rendering system take care of clipping the icons so that they don't overrun my client area). Once I had the number of columns, I calculated how many rows I'd need by dividing the total number of icons by the number of columns. This told me how to setup my included scrollbar.

    Note that these values will have to be recalculated whenever the control is resized. For this reason, I set up a wm_sizechanged() message that calcall() would call whenever the client area of a window was modified.

    The report-style list control was a little more complex. I first created two helper classes, gui_listbox_column and gui_listbox_item, which contained all of the information about a given item and column in the list.

    gui_listbox_column is the simpler of the two classes. The main listbox class keeps, as a member variable, a dynamic array of gui_listbox_columns, which represent the columns in the listbox right now. gui_listbox_column contains all of the information needed for a column in our list box, including the name of the column, the alignment of the column, whether it's shown or hidden, its size, etc.

    The main listbox class also keeps a dynamic array of gui_listbox_items. The gui_listbox_item class contains everything related to a particular row (or item) in our report-style listbox. By far the most important data member of this class is the array of strings, representing the data for each column. I also decided to let each item store an additional 32-bits of data with it, via the m_itemdata member. This technique is similar to how Windows allows you to store 32-bits of data by calling SetItemData() and GetItemData() for your listbox items. This feature is important because it allows clients of the listbox to store a pointer with each item - usually a pointer to the specific class associated with the item, so that it's readily available later.

    As for drawing the columns and items... I decided that I'd like to have absolute control over how each individual item/column in the listbox was drawn. Towards this end, I decided to have the listbox draw its items and columns by repeatedly calling two virtual functions, gui_listbox_item::draw() and gui_listbox_column::draw(). Each function took a rectangle understood to be the location on the screen where the column or item was supposed to be drawn. The default implementations of these draw() functions just spit out the text associated with that particular column and subitem in that rectangle; however, I could now easily derive and override draw() for items or columns that required a unique appearance. This technique has seemed to work for me so far, but I haven't concentrated enough on it to claim that it's the best and right way to do it.

    Drawing the items required a little more work than the columns, however. Items had to be drawn with or without a highlight, depending on whether they were selected or not. Not a big deal, but important to remember.

    Then there's the issue of scrollbars. My report-view listbox contained two members, m_horzscrollbar and m_vertscrollbar, both GUI scrollbars. Whenever the size of the listbox was changed (wm_sizechanged()), it took a peek at the width and height of the data it had, and either displayed or hid the scrollbars as appropriate.


    [size="5"]Conclusion

    It's been a whirlwind tour, but hopefully you have a general idea of what lies ahead of you in your quest to create controls for your GUI. The only thing I want to reiterate is "style is fun." Don't be afraid to take a few liberties as you create your GUI - implement the stuff you've always wished for, and the stuff that makes the most sense in your game. This is especially important if the game your making relies heavily on the functionality of your GUI - like, say, you're making a RTS game.

    But also remember that in creating your controls, your doing the same balancing act as with the rest of your game - you're weighing features against development time. Give your players as easy and intuitive GUI as possible, but also, don't spend all your time making 50 different controls. You want to strike a balance between functionality, the good thing, and complexity, the bad thing.

    That's it for controls. In the next and final part of this article, we'll take a quick peek at resource editors, serializing windows, and creating dialog boxes. Until then... have fun!
Cancel Save
0 Likes 0 Comments

Comments

Nobody has left a comment. You can be the first!
You must log in to join the conversation.
Don't have a GameDev.net account? Sign up!
Advertisement