Developing A GUI Using C++ and DirectX Part 4

Published September 15, 1999 by Mason McCuskey, posted by Myopic Rhino
Do you see issues with this article? Let us know.
Advertisement
[size="5"]Part IV: Resource Editors and Other Madness

Welcome to the fourth and last part of the "Developing a GUI in C++ And DirectX" Article. Continuing with the overall theme (showing how I implemented a GUI for my upcoming game, Quaternion), this article will address a whole bunch of miscellaneous issues and ideas to polish off your game GUI.

[size="3"] Saving Windows

Window serialization (or, saving and loading windows) may or may not be crucial for your project. If your game GUI is minimal, you might be able to get by with just hard-coding the windows into your game. But if your GUI's relatively complex, or will change often over the course of development, you're going to want code that will save a window (and all its children) to a file, and then load it back up again. For starters, having window serialization code allows you to change your game's GUI without recompiling, and is a boon if you're working with more than one person.

My plan of attack was easy - start at the main dialog window, and recursively go through all of its child windows, saving each one to disk. If I were programming in C, the first thing I would have said to myself would have been "OK, so if I have to save these windows, I need a byte for each window that tells me what type of window it is, so I can load it back up correctly. 1 is a button, 2 is a listbox, 3 is an icon, etc."

This kind of problem is specifically what C++'s RTTI (Run Time Type Identification) addresses. RTTI provides two things, a type_info class and a typeid() function, which together allowed me to query an object for its class name - "gui_window", "gui_button", etc. Instead of fiddling with enums and IDs, I simply call typid() for each window I'm going to save, and "write down" the class name of the window.

I saw two minor disadvantages to using RTTI's object identification functions to help save windows. First of all, the RTTI IDs are strings, not integers, which means they'll take up more space on disk (store them Pascal style, that is, the first 4 bytes describe the length of the string, followed by the string data itself). Second, if you change the name of one of your window classes, you'll break any window files that you've previously saved.

For these reasons, you might opt out of using RTTI in this manner - after all, just because a technology is there doesn't mean you have to use it. However, I found RTTI to be a lifesaver in my code. For more information on RTTI and these two functions, search for them in your online help.

Also, if you decide to use RTTI with Visual C++, make sure you turn it on in your project settings, C/C++ tab, C++ language option.

[size="3"] Loading Windows

Loading windows is more difficult than saving them, primarily because you have to new each window, load it up, and then remember to delete it when it's no longer needed.

This function is recursive, and looks like this in PDL:

void gui_window:load(int filehandle)
{
// read window properties (colorsets, etc.)
// read total number of children for this window
// for each child...
// read window ID from disk
// new a gui_window derivative based on that ID
// tell the newly created window to load itself (recurse!)
// next child
}

In other words, you'd load windows from disk exactly as you would expect. First, you take care of the base window: read in its properties. Then, read in the total number of children of the base window. For each child, read an ID byte, new up a window based on that ID, and then tell that new window to load itself (recurse down into it). Once all of your children are loaded, you're done.

Of course, it's also very important that your file structure mirrors this same layout. Make sure your save code saves things in the same order that you're loading them.

[size="3"] Resource Editors

To really make your game GUI shine, you're going to need a resource editor. Certainly you don't need one as slick and functional as Developer Studio's, but you at least need a basic application that will let you add, edit, delete, and arrange things, and will save you the trouble of calculating out virtual coordinate positions for all of the controls in your dialogs.

Writing a full-featured, WYSIWYG resource editor is beyond the scope of this article, but I can give you a few miscellaneous tips for if you do decide to attempt such a beast:
  • Share your code. Specifically, make the resource editor share the same rendering code as your actual game. This way, you get WYSIWYG support, and you save yourself the trouble of having to implement two sets of GUI code. I guarantee you, it's easier to tweak your DirectX code so that it renders to a GDI surface instead of to a double-buffered system than it is to re-implement an entirely new drawing core. Remember also that it's quite likely your GUI system will change over time - you don't want to constantly have to change code in two different places.
  • Don't try to emulate DevStudio's look and feel. In other words, don't waste time trying to figure out how to mimic the various features of the DevStudio GUI (like, say, tabbed property sheets and preview windows). Don't feel bad if your Resource Editor is ugly in comparison; yes, it's true that the productivity of a team is directly proportional to how useful its tools are, but at the same time, it's very unlikely that anyone outside your team will be using your Resource Editor, and you're not using it to create a full-fledged GUI app; you're just making a few dialogs. You don't need context sensitive help. You don't need context menus, unless in your opinion they ease a particularly tedious operation. It's OK if your resource editor isn't polished, just so long as it gets the job done.
  • Emphasize data integrity over speed. The Resource Editor is a data wrangler, not a high-performance app, and there's nothing more annoying than when a dialog you've spent an hour designing goes down the tubes because of a program crash. When writing your GUI, preserving data should be your highest goal - implement autosaves, undo buffers, etc, and go easy on the optimization.
[size="3"] Subclassing

Those of you who are familiar with the way Win32 does its windowing probably already know what the term "subclass" means. For those of you who don't - when you "subclass" a window, you effectively "derive" a new window type, and then wedge your new window type into places where the old window used to be.

Let me explain that a little better. Say we need a Super Listbox. We've got a normal listbox class, but it just doesn't cut it for some reason; our game demands the Super Listbox. So we derive a new "super listbox" class from our regular listbox class. So far so good.

But how do we place the Super Listbox in our game's dialog? Since the Super Listbox is specific to our application, it wouldn't make sense for us to add functionality to our resource editor to support it. But, at the same time, how do we tell our GUI system that for this one particular instance (our game), we'd like all listboxes to really be Super Listboxes? That's what subclassing is all about - it's not an exact, technical definition, but it will suffice for now.

The approach I'm about to illustrate is what I call "subclassing at load time." To understand it, let's start with the basic loading code I described in the last section. We've got a load function, which recursively news, loads, and then adds windows. Specifically, we've got something that looks like this, in PDL:

// read total number of children for this window
// for each child...
// read window ID from disk
// new a gui_window derivative based on that ID
// ...
// next child

To implement subclassing, I've told my window loading routine to "give the application a chance to create a window of this type," like so:

// read total number of children for this window
// for each child...
// read window ID from disk
// give application a chance to create a window of this type
// if the application didn't create a window,
// then new a gui_window derivative based on the ID
// else, use the application's created window
// ...
// next child

Specifically, I give the application this chance by way of a function pointer. If the application needs to subclass a window, it fills in the function pointer with the address of its own function. When the windows are loading, they call this application function, passing in the ID of the window they want to create. If the application wants to subclass a window from this ID, it news up the appropriate object and returns the new pointer back to the window. If the app doesn't want to do anything special for this ID, it returns NULL, and the window function senses this and news up the appropriate default object. This method allows the app to "pre-filter" the incoming window ID bytes, and to override the default behavior for certain window types. Perfect!

Using a method like this gave me a huge amount of freedom when it came to creating custom controls. I went back and added code to my resource editor that would let me change the IDs that were saved for each window. Then, when I needed a custom control in the app, I just used my resource editor to change the ID byte that was saved for that window. Saved on disk would be the ID, along with the dimensions and all the other base-class properties for the custom control!

Real quickly - there's another way to do this same thing, and that is to mirror the approach the STL has used when it needs to create things. The STL uses special "allocator" classes, which are sort of like "factories," in the sense that clients tell them what they want created, and they create it. You could use this same approach for creating windows.

It'd work something like this. Create a class and call it gui_window_allocator. Implement one virtual function, say CreateWindowOfType(...), which would take in a given window ID and spit back out a brand new pointer to that window. Now you've got a very simple allocator class, which your window loading code will use by default to new up windows as they are needed.

Now, when your application wants to override the "new" behavior for the windows, you simply derive a new, application-specific gui_window_allocator class, and tell your window loading code to use this allocator instead of the default one. This method is similar to providing a function pointer, only with a bit more C++ thrown into the mix.

[size="3"] Speeding Up GUI Rendering

Here's another miscellaneous tidbit for you that will help you if you're trying to speed up your GUI drawing.

The key concept here, just like with optimizing any other drawing routine, is simply "don't draw what you don't need to." By default, the GUI spends a lot of its time drawing stuff that hasn't changed. You can, however, optimize this a little, by telling the GUI to only draw windows that are "dirty." The windows set their dirty flag whenever their appearance needs to change, and they clear the dirty flag once they're drawn.

There's one catch - since our GUI controls might be transparent, when a control is marked as dirty, its parent window must also be marked as dirty. That way, when it draws itself, the background is still intact, since the parent window has also just redrawn.

[size="3"] Conclusion: See me at the XGDC!

Whew. It's been four long articles, but we've covered a lot.

I'm going to be speaking about GUIs at the upcoming Armageddon XGDC (eXtreme Game Developer's Conference), on October 2 and 3. I'd like my talk to be as useful as possible, so now that you've read all the articles, if there's anything you'd like me to expand on, or anything that I've missed, please email me and let me know about it.

Have fun developing your game!

[hr] Mason McCuskey is the leader of Spin Studios, a game development team working to break into the industry by creating a great game, Quaternion, and getting it published. He looks forward to your suggestions and comments, and can be reached at [email="mason@spin-studios.com"]mason@spin-studios.com[/email].
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