Выбрать главу

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: