Introduction - GameDev.net

advertisement
Introduction
One of the most vital components to any game is a GUI (Graphical User
Interface). A GUI provides the user with information about the current game
state, as well as acting as a virtual input-receiver. Thus, the purposes of any GUI
are two-fold –
1. To graphically display information and
2. To translate user input into meaningful data.
There are plenty of resources available to learn how to create a GUI system, but
almost all of them (that the author has seen, at least) is layered with API-specific
details. This is partly due to the nature of GUIs – the first point requires rendering
code, and the second requires event-polling code.
C++ gives us a unique strategy to abstract a GUI system, such that it isn’t
dependant on API-specific code – template metaprogramming. We can define a
set of behaviors that are required to be implemented, but ignore their
implementations.
Templates are not the only way to do this. Similar (and even more daring deeds)
can be implemented by using polymorphism. Polymorphism is done at run-time,
allowing the user to dynamically switch between implementations. As templates
are compiled into the code at compile-time, such behavior cannot be easily
achieved in the same way.
Polymorphism, however, has the drawback of adding a minute overhead to each
function call. While it isn’t that drastic of a setback, it is something that can be
avoided if unnecessary.
Because our goal is to create an API-independent GUI system, such behavior is
overkill.
A Note About Style
From what I’ve seen, there are three ways, regarding code, to write articles. The
first is to omit code altogether from the article and (possibly) provide an attached
sample of the concepts discussed. While this is good for a technical discussion, I
want to bloat this with as much code as possible to stress code reuse. Because
code reuse is good.
The second consideration, once you’ve decided to incorporate code into the
article is how to incorporate it. There are two main styles I’ve seen thus far – the
first is to paste massive blocks of commented code and follow with a good
explanation paragraph. This, however, requires the code to be commented, and
for the most part, mine isn’t. Aside from those few comments like “lol”, or the
ever-amusing all caps “WTF LOL?”
The style I’ve chosen to use is essentially the same approach taken by the NeHe
tutorials – to write a bit of code and explain it, then write the next bit of code,
such that the paper flows logically.
Additionally, almost all of the code used in this paper will have been snipped from
a larger whole – actual class declarations will include more functionality (for
better or worse) and will look, on average, less pretty than shown here. It may
not be pieced together coherently; if you want to see the actual code (or
copy/paste) I’d recommend downloading the source and using that.
Finally, I prefix private members with an underscore, eg. _privateMethod.
Adapters
Before we can begin developing our GUI system, we need to first define a set of
behaviors that we will expect our API-specific code (hereon termed an “adapter”)
to expose to us.
Furthermore, we can divide our needs into two distinct classes – an adapter for
rendering (a “graphics adapter”), and an adapter for input (an “event adapter”).
Adapters should not, under any circumstances expose any API-specific
implementations. If they did, you might accidentally create an API dependency in
your application.
The Graphics Adapter
Graphical requirements for a basic GUI are pretty slim – essentially filling
rectangles on the screen and drawing text. Additional necessities are accessing
the screen dimensions (for culling purposes), as well as utilities to make colors,
textures, and such. An example implementation might look something like this –
class SDLGfxAdapter {
public:
// ! important !
typedef SDL_Surface* TextureType;
typedef unsigned int ColorType;
typedef unsigned int SizeType;
int screenW();
int screenH();
void fillRect( ColorType color, int x, int y, int w, int h );
void fillRect( TextureType tex, ColorType color );
void fillRect( TextureType tex, ColorType color, int x, int y, int w, int h );
TextureType createTexture( std::string filename );
TextureType createTexture( int w, int h );
void freeTexture( TextureType tex );
SizeType getWidth( TextureType tex );
SizeType getHeight( TextureType tex );
void setColorKey( TextureType tex, ColorType color );
ColorType makeColor( int r, int g, int b, int a = SDL_ALPHA_OPAQUE );
void drawText( std::string text, ColorType color, int x, int y );
};
A key element are the typedefs, which allow the adapter to define “custom”
classes, if you will, that it uses. Since the GUI will not have access to the
definitions of these typedefs, we have to provide a way to create and free them.
Every implementation of a graphics adapter should have the same form at this
point, regardless of API. If a graphics adapter is implemented which lacks a
function which the GUI expects, then the compiler is going to throw a compiletime error. This makes it important to make sure you don’t accidentally create a
dependency here by providing functionality that won’t be available for every API
you might want to implement.
The Event Adapter
While the graphics adapter’s functions are called by the GUI, the event adapter
calls the GUI’s functions. This behavior is achieved using callbacks. I personally
implemented my own callback class ages ago, and the GUI code was written
before I discovered boost::function. If you’re writing code now, stop, and go
download Boost (boost.org), because it is the sexiest thing to happen to C++
ever.
Regardless, let’s take a look at the exposed functionality of an event adapter –
class SDLEventAdapter {
public:
// Called ever pass or so. Polls the system for events
// and parses them into the GUI’s event structs, then
// calls the handler to notify the GUI of the event.
void poll();
// In these functions, a Handler is a callback to call
// whenever the system passes us an event. These functions
// allow the GUI to inform the adapter during init where
// to pass the events to.
void setKeyHandler( KeyHandler handler );
void setMouseHandler( MouseHandler handler );
void setMouseMoveHandler( MouseMoveHandler handler );
void setWindowHandler( WindowHandler handler );
};
We haven’t gotten into the structure of the event system yet, so I won’t show you
the handler typedefs yet (they’re pretty messy, especially since my code isn’t
using Boost).
Returning from the Boost Crusade, the event adapter serves a single purpose –
to receive events from the system, whether from a WindowProc or from Direct
Input, and pass them down to the GUI system.
Another nasty thing that the event adapter is expected to provide is a list of
keycodes, stored in macros, ie –
#define KEY_ENTER SDLK_ENTER; // or whatever
Which does get really messy, and I’m sure there’s a better way to do it. This way
does compile down nicely (since its done in the preprocessing step) and helps
hide implementation details. Still messy nonetheless.
Details about the event adapter and the functionality it needs to provide will be
covered later, when we get into the event system.
IComponent
Before we get into rendering our fancy buttons and labels, we have to define a
set of behaviors for the system.
Every button, widget and textbox in a GUI can be classified as one of two types –
a control (component) or a container. The difference is simple – a container is a
component with children. Essentially this allows us to create a tree of
components, which as a whole, constitute our GUI.
We are left then, with two main interfaces – IComponent and IContainer, which
inherits from IComponent.
class IComponent {
The absolute first thing we should do is to throw in a virtual destructor. This is
going to be base class for polymorphism, and without a virtual destructor weird
stuff will happen. So don’t take the chance to forget.
virtual ~IComponent() { };
should embody all functionality that every control will have. This
prevents us from having to re-write code multiple times, and as MustEatYemen
put it, is “[u]seful for leveraging rapid development and [increasing] your ROI to
create products that maximize the synergy in large workgroups.”
IComponent
The first thing every control has is a dimension, namely size and position. While
you can represent this as four stand-alone integers, I prefer to think of it as a
rectangle, which might look something like template< typename T >
struct Rect {
T x, y, w, h;
Rect()
: x(0), y(0), w(0), h(0) {}
Rect( T x, T y, T w, T h )
: x(x), y(y), w(w), h(h) {}
// and so on with all the operations
// you want to get done.
};
Now that we’ve defined a way to store our dimensions, we can put that in our
class, as well as some accessors/mutators –
typedef Rect< int > DimType;
DimType _dim;
void
void
void
void
void
setPos( int x, int y );
setSize( int w, int h );
setDim( DimType dim );
setDim( int x, int y, int w, int h );
move( int x, int y );
DimType& getDim();
int getX();
int getY();
int getW();
int getH();
Every control also has a parent container. We need a pointer to this container for
operations like removing this control from its parent. Lets add this in, as well as
some functions for getting/setting the value IContainer* _parent;
void setParent( IContainer* parent );
IContainer* getParent();
And, before we get into the big nasty stuff, lets also throw in some properties
which we’ll use later on. I’m just adding it in now to impede progress.
bool _focus;
bool hasFocus();
void setFocus( bool focus );
bool _zOrdered;
bool isZOrdered();
For more kudos, lets throw in a helper function to determine whether a point is
within the control or not (useful for checking if the control is clicked), which is
implemented with a simple bounding-box check -
bool containsPoint( int x, int y );
The next, and possibly most obvious standard behavior is a method to draw the
control, namely virtual void draw( int rel_x, int rel_y ) = 0;
This function will be called every pass, on essentially every visible control. The
parameters passed to it, rel_x and rel_y, represent the parent’s offset into
screen space –
Each container has its own respective coordinate system. In this screenshot, the
main window (titled “Window Title”) might have a position of (20,20). When the
button, at (30,30), has its draw method called it will be passed the parent’s
position relative to the screen, in this case, (20,20).
This should be done recursively, so if there was a button in “Sub Window” (at
position (80,60) within “Window Title”) it would be passed (20+80, 20+60), and so
on.
The reasoning behind this is because we need to be able to convert from local
coordinate systems to screen space, which can be done as follows within the
draw code (which comes much later) –
int x = getX() + rel_x; int w = getW();
int y = getY() + rel_y; int h = getH();
The propagation of these values will be handled by the IContainer class, since
there’s no reason to do it in here. We have no children to propagate it to.
The final behavior we need every GUI object to implement is a way to handle
events. Which is unarguably the most complex part of the whole system.
To simplify matters, we’ll be using the wonderful Observer design pattern. The
Observer pattern is typically used when many objects rely on one object for
information and state updates. While this sounds really complicated to me, it, in
reality, is pretty simple.
Event handling code will be placed in a listener class, which implements
functions declared in a base listener class (depending on the event type). An
instance of a listener class can then be registered to a control to receive events.
So, when a control receives an event, the only action it will take is to forward that
event to all of its listeners.
The control (in my implementation) does not claim ownership over the listeners.
(ie, it doesn’t attempt to free memory allocated for them when its destroyed). It,
along with every other class, uses no managed pointers – all memory
management is left up to the user. If you create, you destroy it; for me, this
means just allocating everything on the stack (because it unwinds
automagically).
Anyway, we need a method to store pointers-to-listeners. Since we don’t have
ownership over them, we can just use an STL container. (If we did have
ownership, we’d want to use a Boost pointer container).
Any control can receive three types of events – key events (triggered by the
keyboard), mouse events (from pressing mouse buttons) and mouse move
events (the mouse moving around).
Lets start by making life easier and typedef’ing our container types. This allows
us to easily change container types with a single line of code (presuming we
don’t rely on container-specific functions). Which is a Good ThingTM.
typedef std::vector< KeyListener* >
KeyListType;
typedef std::vector< MouseListener* >
MouseListType;
typedef std::vector< MouseMoveListener* > MouseMoveListType;
Now, you’re probably asking me right now why I’m not using std::list. The
answer, of course, is because a vector is more suited to what we want. We’re
not often going to be adding/removing listeners, and even if we are, we’re
probably not going to have all that many. This, combined with the fact that we’ll
be iterating over them all the time makes vector a superior choice.
So, uh, we actually need to declare the members themselves. Using those
typedefs.
KeyListType
_keylist;
MouseListType
_mouselist;
MouseMoveListType _movelist;
And some functions to modify these internals –
void addKeyListener( KeyListener* listener );
void delKeyListener( KeyListener* listener );
void addMouseListener( MouseListener* listener );
void delMouseListener( MouseListener* listener );
void addMouseMoveListener( MouseMoveListener* listener );
void delMouseMoveListener( MouseMoveListener* listener );
Hooray.
Now, you’d think we’d be done at this point, but sadly we’re not. We still haven’t
provided a way to tell the control it has received an event. And even then, not all
components should react the same way – an IComponent should just pass it on to
its listeners, and then be done, whereas an IContainer needs to pass the event
onto its children in addition to its listeners.
Hense, we need to make these functions virtual to allow us to override them
later on (the return value will be discussed later) –
// Functions which pass the event to the component.
// the component then sends the event to all the
// listeners.
virtual bool processKeyEvent( KeyEvent e );
virtual bool processMouseEvent( MouseEvent e );
virtual bool processMouseMoveEvent( MouseMoveEvent e );
And now we’re done declaring our IComponent class. It may seem like a lot,
because it is – it provides to core functionality for essentially every control.
All of this can be found in the file “IComponent.h” included in the source.
IContainer
As mentioned before, the only difference between IComponent and IContainer is
that IContainer has children. This actually yields a lot of code to write – since
every IComponent has a parent IContainer, every event has to be distributed by
IContainer classes to their respective children.
Lets get down to it and define a container type for the children.
typedef std::vector< IComponent* > ChildListType;
ChildListType _children;
Once again I’ve chosen to use a vector, for the same reasons – we’re not going
to be doing major insertions or removals, and we probably aren’t going to have
enough entries anyway to make a list faster than a vector.
We also need methods to add/remove IComponent* from the list –
void addComponent( IComponent* component );
void delComponent( IComponent* component );
And a couple methods of moving the components around, for things like zordering and passing mouse input to the top-most window, for example –
void moveToFront( IComponent* component );
void moveToBack( IComponent* component );
The draw method is still not implemented, but we need to add a helper function
to draw all the children –
void drawChildren( int rel_x, int rel_y );
This function should be called at the end of the container’s draw method, so that
all the children are drawn on top of the controller. The method itself should
propagate the offset to convert local coordinates to screen coordinates, as well
as do some rudimentary culling –
void IContainer::drawChildren( int rel_x, int rel_y ) {
int off_x = this->getX() + rel_x;
int off_y = this->getY() + rel_y;
for ( ChildListType::iterator i = this->_children.begin();
i != this->_children.end(); ++i )
if ( isVisible( *i ) )
( *i )->draw( off_x, off_y );
}
We’ve made use of another helper function, isVisible, which is a member of
IContainer, and is basically a bounding-box check to see if the child is visible in
local space –
bool IContainer::isVisible( IComponent* child ) {
return ( child->getX() + child->getW() > 0 &&
child->getY() + child->getH() > 0 &&
child->getX() < this->getW() &&
child->getY() < this->getH() );
}
That part should be pretty simple. Now we’re going to move onto an area which
is a little bit shadier – how the container passes events to the components.
Like mentioned before, we defined the event processing functions as virtual. Lets
override ‘em –
virtual bool processKeyEvent( KeyEvent e );
virtual bool processMouseEvent( MouseEvent e );
virtual bool processMouseMoveEvent( MouseMoveEvent e );
The value returned is a flag that determines how the event handling will continue
higher up on the chain. This will be discussed later, but at this level is essentially
just propagated up from the listeners (the bottom level of the event chain).
processKeyEvent is the most simple of the three. Remember back when we
added in IComponent::hasFocus()? Well, this is where we need it – a component
should only receive key events if it currently has focus (control over the keys).
So, essentially, our processKeyEvent just needs to iterate through the children
and pass the event to them. If it encounters an EV_EAT flag, then the event gets
“eaten” and the propagation stops.
bool IContainer::processKeyEvent( pawn::gui::KeyEvent e ) {
IComponent::processKeyEvent( e );
bool ret = EV_OK;
for ( ChildListType::iterator i = _children.begin();
i != _children.end(); ++i ) {
if ( ( *i )->hasFocus() )
if ( ( *i )->processKeyEvent( e ) )
ret |= EV_EAT;
}
return ret;
}
Another thing to note is that the list typedefs are used to get the iterator type, not
the actual underlying class. This allows us to maintain a level of abstraction,
which allows us to easily switch the container type should the need arise.
This process gets slightly more complicated with mouse events – when you click
a control more things happen.
First, we have to convert from screen coordinates to local coordinates. This
conversion is the exact opposite of the opposite conversion. Next, we do some
checking to see whether the click is actually within the control. If it is clicked, we
have to give it focus, and bring it to the front (if it is z-ordered).
Z-ordering itself is done by maintaining order in the list – if a control is brought to
the front, its actually at the back of the list (Painter’s Algorithm). Because of this,
the topmost controls are actually at the end of the list, so we have to iterate
backwards through the list to make sure the topmost control receives the event.
This reversed traversal of the list really couldn’t be avoided – either the drawing
code or the event code has to go backwards, since they’re ordered differently
due to the Painter’s Algorithm, since we can’t assume our graphics API supports
a z-buffer.
All this turns into a massive function which I am almost reluctant to paste.
bool IContainer::processMouseEvent( pawn::gui::MouseEvent e ) {
IComponent::processMouseEvent( e );
// the value to return. We'll mark flags during the function.
bool ret = EV_OK;
// translate the event to local space
e.x -= this->getX();
e.y -= this->getY();
// take focus away from everything. har har.
for ( ChildListType::iterator i = _children.begin();
i != _children.end(); ++i ) {
( *i )->setFocus( false );
}
for ( ChildListType::reverse_iterator i = _children.rbegin();
i != _children.rend(); i++ ) {
if ( ( *i )->containsPoint( e.x, e.y ) ) {
( *i )->setFocus( true );
if ( ( *i )->processMouseEvent( e ) ) {
ret |= EV_EAT;
break;
}
// check to see if we need to reorder it
// INVALIDATES ITERATOR.
if ( ( *i )->isZOrdered() )
moveToBack( *i );
// only one thing can recieve mouse input
// and also, the vector iterator isn't valid anymore.
break;
}
}
return ret;
}
This is where the EV_EAT flag comes into play. While we’re in the scope of this
function, if at any point the list of children components is altered our iterator is
suddenly and painfully invalidated. The big capital “INVALIDATES ITERATOR”
comment shows this – if the loop continued past the break statement, the app
would break (crash).
So, in what circumstances would the child list be altered during the course of an
event? Well, the big one for me is when you close a window. We’ll cover this
later, but when you click the “X” box in the corner, the button’s action event
handlers are called, which send a quit event to the window, which then removes
itself from its parent’s list. This is all done within the scope of the original button
click handling, and thus our iterator is invalildated. Which would cause our
program to crash.
Ideally, a function call that alters the state of the list would set a flag which would
alert the event processing code of the change. And I doubt it would take much
more effort to change it. And it might eventually get changed. But at this point,
that’s how it works. Admittedly it’s a weak point of the system :P
Now we can move on to the last part of IContainer – handling mouse move
events. Unlike a MouseEvent, a MouseMoveEvent has two coordinates – the
absolute position (abs_x, abs_y) of the mouse (where it currently is) and the
relative position (rel_x, rel_y) representing the amount the mouse has been
moved since the last event. Like handling clicks, we need to transform the
absolute position into local space before we do anything else.
The mouse move events come in three flavors – a typical mouse move event,
which reports that the mouse has indeed moved, and two notifier events which
alert controls that the mouse is moving in/out of their bounds.
The catch is that only one control can receive a mouse move event, while any
number can receive enter/leave notification. This makes sense in the case where
a bunch of controls are stacked directly on top of one another and the mouse
moves over the bounds – the top control and so on will receive a mouse
enter/leave event, while the final control will receive the mouse move event. The
logic works out for preventing enter/leave events for controls beneath the toplevel one – if they’re hidden by a control above them then the mouse hasn’t really
entered or left them.
I should mention there is a flaw in this system, with regards to overlapping
controls. If two controls are overlapped and the mouse moves over the
overlapped section, the bottom control will not receive an enter/leave event
because it will be blocked by the topmost control. If you need this behavior to be
in place and don’t mind redundant mouse move events, take out the break in this
function.
bool IContainer::processMouseMoveEvent(pawn::gui::MouseMoveEvent e) {
IComponent::processMouseMoveEvent( e );
bool ret = EV_OK;
// translate event into local space
e.abs_x -= this->getX();
e.abs_y -= this->getY();
for ( ChildListType::reverse_iterator i = _children.rbegin();
i != _children.rend(); ++i ) {
if ((*i)->containsPoint( e.abs_x, e.abs_y ) ) {
// check to see if the mouse just entered
if (!(*i)->containsPoint( e.abs_x - e.rel_x, e.abs_y - e.rel_y )){
// mouse entered event.
e.state = e.MOUSE_ENTER;
if ( ( *i )->processMouseMoveEvent( e ) )
ret |= EV_EAT;
} else {
// move move event
e.state = e.MOUSE_MOVE;
if ( ( *i )->processMouseMoveEvent( e ) )
ret |= EV_EAT;
// only one component can get a mouse-move event at a time
// take this out to “unblock” event chains.
break;
}
} else if ((*i)->containsPoint( e.abs_x - e.rel_x, e.abs_y - e.rel_y )){
// check to see if the mouse just left
// mouse left event
e.state = e.MOUSE_LEAVE;
if ( ( *i )->processMouseMoveEvent( e ) )
ret |= EV_EAT;
}
}
return ret;
}
That pretty much concludes the event processing methods of IContainer, which
were originally defined in IComponent. Now we can pass events down the tree by
just recursively calling process*Event(..). Which is good.
The Event Subsystem
Up until now I’ve been leaving off anything other than a component-specific
discussion of the event system. Let’s change that.
First, there are several types of events –
– results from a key being pressed or released. Contains information
about the state of the key, as well as which key is being pressed –
KEY_EVENT
class KeyEvent {
public:
enum KeyStates {
KEY_DOWN = 0,
KEY_UP
};
unsigned int state;
unsigned int key;
};
– essentially a mouse click. Has information about which button was
clicked, and where the click took place.
MOUSE_EVENT
class MouseEvent {
public:
enum MouseStates {
MOUSE_LEFT = 0,
MOUSE_RIGHT,
MOUSE_UP,
MOUSE_DOWN
};
unsigned int state;
unsigned int button;
unsigned int x, y;
};
– results from the mouse moving. Three types – a normal
move, an enter event and a leave event. Good for keeping track of where the
mouse it. Stores the movement as an absolute position of where the mouse is,
and a relative value – how much it moved.
MOUSE_MOVE_EVENT
class MouseMoveEvent {
public:
enum MouseMoveStates {
MOUSE_MOVE = 0,
MOUSE_ENTER,
MOUSE_LEAVE
};
enum ButtonStates {
BUTTON_LEFT = 0x01,
BUTTON_RIGHT = 0x02,
BUTTON_MIDDLE = 0x04
};
unsigned int state;
unsigned int button;
int abs_x, abs_y;
int rel_x, rel_y;
};
– events triggered which affect a window. We haven’t gotten too
much into the IWindow interface yet, so most of the discussion will take place
there.
WINDOW_EVENT
class WindowEvent {
public:
enum WindowStates {
WINDOW_RESIZE = 0,
WINDOW_MOVE,
WINDOW_MINIMIZE,
WINDOW_MAXIMIZE,
WINDOW_CREATE,
WINDOW_DESTROY
};
unsigned int state;
int param1, param2;
};
– a generic callback. Good for binding things like buttons, or
notifying code that something needs to be done.
ACTION_EVENT
class ActionEvent {
public:
IComponent* source;
IEvent( IComponent* source )
: source( source )
{ }
};
Note that in the source, “Events.h” there’s some crazy templates and typedefs
going on. Now, I remember when I wrote that there was a plan. Daft if I know
what it was though. Needless to say, the implementation of it doesn’t matter too
much; there’s no reason it needs to be more complicated than the above though.
Anyway, each event (excepting ActionEvent) defines a set of states which it can
be in. If you recall, this flag was used by the process*Event(..) code in
IContainer to distribute the events properly. It essentially represents different
“sub-types” of events without having to worry about a bajillion classes. We could
easily have made a single unified event class, but it would either have involved a
union of multiple classes, or been absurdedly large.
Another thing you might have noticed is that events are passed by value (a new
event is copied and pushed onto the stack each time its passed to a function).
This was done as a simplification – like most other parts of this system, nothing is
allocated on the heap. Again, most of the reason is that there isn’t much need to
allocate on the heap all the time. Since none of the components, excepting the
events, are allocated internally, the system leaves that up to the user. I
personally prefer the stack for most things, simply because it makes a memory
leak much less likely. To each their own.
You’d also want to pass event by value if you were writing a multi-threaded
system. This is to eliminate a race condition where the message/event is freed
before it is read by the other thread. The solution is a thread-safe smart pointer,
of course, but its almost as easy to use the stack. And almost as fast because…
Allocating small objects on the heap with the default new operator is slow. In most
implementations, new is basically just a wrapper around C’s allocation functions,
which were optimized for allocating massive POD structs. Which isn’t what we
have. Again, there is a solution – writing a custom small-object allocator (or just
using one), which is more effort than its worth. Well-written allocators are pretty
easy to use though, normally something along the lines of
class MySmallObject : public Loki::SmallObject { };
Which isn’t too bad at all.
Anyway, let’s end this sidetrack and discuss the listener base classes –
class KeyListener {
public:
typedef bool RetType;
public:
virtual RetType onKeyDown( KeyEvent e ) { return RetType( 0 ); };
virtual RetType onKeyUp( KeyEvent e ) { return RetType( 0 ); };
};
Here we have one of the five listener types, and its pretty simple. You should
note that for each state defined in KeyListener there is a corresponding event
function. The return values for the functions, at this point, are Booleans simply
because I didn’t need anything beyond that. By changing the typedef we can
easily make it any type we wanted, although we’d also have to adjust the return
values of the process*Event functions if we wanted that value to propagate
properly (in the state shown in this article, they return a bool which will be
implicitly cast from whatever you return).
The event’s state flag is used by ICompnent::process*Event to determine the
proper group of listeners to dispatch the event to.
The rest of the listeners are pretty intuitive – they all have the same structure with
functions corresponding to the state enums in the event structures themselves.
You can also check “Events.h” for the source.
IWindow
There is one main part left before we can start writing random controls to suit our
needs – the master (top-level) GUI class. That class is derived from another
“behavior” class, IWindow, so we’ll start there.
When writing controls, we want to separate the functionality from the rendering,
so that we can have many ways of drawing a single control but have it act
essentially the same. Code re-use is awesomeness.
IWindow represents a logical window – a dynamic on-screen container which
responds to mouse actions to drag it around, close it, etc. We’ve already laid out
the foundation for this behavior in our event system, but how do we convert
mouse events into window events? Two windows might be completely different,
having different handles and buttons.
The solution is to not attempt to convert mouse events to window events, but
rather, to let that be done by the derived class. Instead, we can stick our own
window event listener into the list and respond to whatever the derived class tells
us to do.
Speaking of listener lists – we’ll need a list of all the listeners subscribed to this
window.
typedef std::vector< WindowListener* > WindowListType;
WindowListType _windowlist;
Now, handling events themselves isn’t too hard, the exception being the destroy
event. To implement them, we’ll define a listener subclass within IWindow –
class _Listener : public WindowListener {
IWindow* _parent;
public:
_Listener( IWindow* parent )
: _parent( parent )
{}
virtual WindowListener::RetType onMove( WindowEvent e ) {
_parent->move( e.param1, e.param2 );
return EV_OK;
}
// etc., for all window events.
virtual WindowListener::RetType onDestroy( WindowEvent e ) {
if ( 0 != _parent->_parent ) {
_parent->_parent->delComponent( _parent );
}
return EV_EAT;
}
};
There’s a couple of things to note here – first, the listener has to access data
from the window its subscribed to, so we need to instantiate it with an instance of
IWindow. The distinct lack of a default constructor prevents us from forgetting this;
you’ll have to explicitly initialize the listener within IWindow’s constructors.
For basically all of the window events which should modify the state of the
window, do just that. Minimizing/maximizing can be noted by toggling a state
flag, while moving/resizing should use the interface supplied by IComponent.
There isn’t really any need for an onCreate handler, since that’s covered in the
constructor.
The onDestroy handler is where things get icky. Hopefully you’ll recall that long
discussion about EV_EAT and why it’s a pain to modify the children/listener lists
during an event handler (it invalidates an iterator and causes a crash).
In this case, we have to return EV_EAT to inform the event system that we’ve
tinkered with it, and to eat the event (ie, prevent other controls from receiving it
and breaking out of the iteration loops).
We’ll also want to have an instance of this listener –
_Listener _list;
Now, I mentioned we needed a list of window listeners. Again, this is
implemented in the same manner as all of the other listener lists – a typedef’ed
container type containing pointers-to-listeners, an instance of the container and
functions to modify the contents. And so –
typedef std::vector< WindowListener* > WindowListType;
WindowListType _windowlist;
void addWindowListener( WindowListener* listener );
void delWindowListener( WindowListener* listener );
bool processWindowEvent( WindowEvent e );
Next, we want to be able to store the current window state – whether the window
is being dragged, resized, is minimized, etc. The best way to do this is with bit
flags. First, we’ll define an enumeration of the possible states.
enum {
WINDOW_DRAGGING = 0x10,
WINDOW_MINIMIZED = 0x20,
WINDOW_MAZIMIZED = 0x40,
WINDOW_RESIZING = 0x80,
WINDOW_HIDDEN = 0x100
};
The data itself is stored in an unsigned int, which gives us four bytes of data to
work with.
unsigned int _state;
Working with bit flags is easy, once you understand how to retrieve/modify
individual flags. First, we’ll want a method to check to see if a current flag is set –
if ( ( _state & FLAG ) == FLAG ) { ... }
Setting a flag is even easier – just OR the flag in.
_state |= flag;
Or you can toggle it with an XOR
_state ^= flag;
Finally, to remove a flag, you AND its inverse –
_state &= ~flag;
Easy enough, no idea why we went off on that tangent. But now you know how to
use flags. The flags themselves aren’t actually used by the IWindow code,
however, they represent key states that every window should have. They should
be used during the derived class’s drawing and event handling functions to
properly interpret the current state of the window, while still allowing the state to
be accessed by code which is unaware of the type of window.
The last topic of discussion before we move onto deriving the main GUI class
from IWindow is the constructor. The constructor needs to do a couple of things –
initialize the members and insert the listener into the list.
IWindow()
: _state( 0 ),
_list( this )
{
_zOrdered = true;
addWindowListener( &_list );
}
_zOrdered is a flag residing in the IComponent interface. Its not included in the
IWindow flags because any component could need to be z-ordered. Z-ordered
was used in IContainer::processMouseEvent to determine whether or not to
bump the control to the top of the list when it received a mouse click.
And that’s all of IWindow. Hooray.
GUI
The aptly-named GUI class represents the base of the tree on which all other
components depend. It can be thought of as the application window, receiving all
the input and distributing it to all components in the application. It also serves as
a hub to render all components with a single draw call.
GUI is the first class which
GfxAdapter type, so we’re
IWindow–
requires both the EventAdapter type and the
going to make it a templated class deriving from
template < class GfxAdapter, class EventAdapter >
class GUI : public IWindow
We also need pointers to our adapters so that we can use them.
GfxAdapter*
_gfx;
EventAdapter* _events;
Almost all of the dirty work is done in the constructor – if you remember way back
to the discussion of the event adapters, we wrote some functions to take
callbacks to handler functions. This is where we need to use them – when we
initialize our GUI class we pass the event adapter instance callbacks to the
process event functions. Like mentioned before, this would be an excellent time
to use boost::function, but meh.
Other things that need to get done – we need to set the dimensions of this
“control” to the size of the actual application window. This is for things like culling,
so that anything offscreen doesn’t get drawn. Finally, we set this control’s parent
to NULL (0), signifying that it is the top-level control.
GUI( GfxAdapter* gfx, EventAdapter* events )
: _gfx( gfx ),
_events( events )
{
// set this parent to NULL
IComponent::setParent( 0 );
IComponent::setSize( _gfx->screenW(), _gfx->screenH() );
// set up all the event stuff (ie, pass it this object's
// event handlers through callbacks.
_events->setKeyHandler( new Callback1< GUI< GfxAdapter, EventAdapter >,
bool, KeyEvent >( this, &GUI< GfxAdapter,
EventAdapter >::processKeyEvent ) );
_events->setMouseHandler( new Callback1< GUI< GfxAdapter, EventAdapter >,
bool, MouseEvent >( this, & GUI< GfxAdapter,
EventAdapter >::processMouseEvent ) );
_events->setMouseMoveHandler(new Callback1< GUI<GfxAdapter, EventAdapter>,
bool, MouseMoveEvent >( this, &GUI< GfxAdapter,
EventAdapter >::processMouseMoveEvent ) );
_events->setWindowHandler( new Callback1< GUI< GfxAdapter, EventAdapter >,
bool, WindowEvent >( this, &GUI< GfxAdapter,
EventAdapter >::processWindowEvent ) );
}
It seems like a mess, but it gets the job done. The code for my callbacks are in
“Callback.h”, which is less messy than you’d think (lies). Again, if you’re writing
your own, I’d highly recommend boost::function.
The other two functions that our GUI class implements are simple – polling the
event handler for events (which it automagically sends out to the callback
functions we’ve provided it) and drawing the children.
void poll() {
return _events->poll(); }
void draw() {
drawChildren( 0, 0 ); }
We pass drawChildren the coordinate ( 0, 0 ) because this is the top-level
window, and shouldn’t have any offset. We’re essentially telling the system to
start drawing the controls in the upper left corner of the application window.
The only other thing to do is hide the functions to change the dimensions of the
control. While you could implement functions to actually change the size of the
window, the ones defined in IComponent just swap the values around (which are
used in the draw functions). Since we’ve defined another draw overload, we
should kill the old one. We prevent them from being called by marking them as
private –
private:
virtual void draw( int, int ) {};
void setDim( pawn::math::Rect< int > ) {};
void setDim( int, int, int, int ) {};
void setSize( int, int ) {};
void setPos( int, int ) {};
And that essentially concludes our main GUI class. It’s not that bad because
most of the functionality is build upon gradually throughout the inheritance
hierarchy. Yay inheritance!
Deriving Functional Controls - IButton
Up to now we’ve been defining a framework for creating controls – now its time to
actually create those controls.
Since the general theme of this framework is separating behavior from rendering
code, we should continue it by abstracting the behavior of all the controls. This
enables us to easily write different ways to draw a control without having to
rewrite too much code. An example of this would be a win32-looking button and a
simple textured quad button – they both use the same base code from IButton,
but differ in how they render the control. Once the base code is written (assuming
its done well) we no longer have to worry about how it works to write more
controls.
For almost every control (excepting perhaps a simple Label) we’ll want to
interpret input. This, of course, requires installing a listener into a (or multiple)
lists.
The behavior we’d expect is simple – a button can be in two states, either
pressed or not pressed. When you initially click the mouse the button goes into
the pressed state, and stays that way until the button is released or moves off the
control. When the mouse button is released while on the control, an ActionEvent
is created and sent out to all subscribed ActionListeners. Additionally, if the
button has key focus and a certain key is pressed (enter) then an event should
be triggered.
First, we’ll define the ActionListener list –
typedef std::vector< ActionListener* > ActionListType;
ActionListType _actionlist;
void addActionListener( ActionListener* listener );
void delActionListener( ActionListener* listener );
bool processActionEvent( ActionEvent e );
These behave just like all the other listener list functions described earlier.
To implement the behavior, we need to define a listener class which will provide
all of the functionality. We need it to listen for key events, mouse events and
mouse move events. Hense –
class _Listener : public KeyListener,
public MouseListener,
public MouseMoveListener
We’ll need a pointer to the parent IButton, and we can also define a helper
function to trigger an action event.
IButton* _parent;
_Listener( IButton* parent )
: _parent( parent ) {}
bool fireActionEvent() {
return _parent->processActionEvent( ActionEvent( _parent ) };
Next we can define methods to handle mouse input – on a down click we want to
flag the button as pressed; on an up click we want to unflag it and send and call
fireActionEvent.
bool IButton::_Listener::onLeftClick( MouseEvent e ) {
switch ( e.state ) {
case e.MOUSE_DOWN:
_parent->_pressed = true;
break;
case e.MOUSE_UP:
_parent->_pressed = false;
return fireActionEvent();
}
return EV_OK;
}
We also want to handle when the mouse enters and leaves the button area while
the button is depressed. When the mouse leaves, we want the button to no
longer look depressed. It should depress when the mouse enters with the button
held down. Hense –
virtual bool onMouseEnter( MouseMoveEvent e ) {
_parent->_pressed = e.button & e.BUTTON_LEFT;
return EV_OK; }
virtual bool onMouseLeave( MouseMoveEvent e ) {
_parent->_pressed = false;
return EV_OK; }
Key input is a little more complicated, since they should only click the button if it
has key focus. Thankfully, we’ve already implemented a primitive focus system
back in IComponent::processMouseEvent. This greatly simplifies our life, except
that we still have to define which keys actually generate an event. I decided upon
space and enter.
I took the cheap way out and used an array of keys, with some hacky
preprocessor code –
static const int numClickKeys = 2;
static const int IButton::_Listener::clickKeys[] = {
KEY_SPACE,
KEY_ENTER
};
Now, I know you’re wondering where the heck those key constants came from,
and the simple answer is – they’re expected to be defined by the event adapter.
There is no other easier way to do it, and it all gets compiled away in the end.
Finally, we can write our function to trigger action events on a keypress –
bool IButton::_Listener::onKeyUp( KeyEvent e ) {
for ( int i = 0; i < numClickKeys; ++i ) {
if ( e.key == clickKeys[ i ] )
return fireActionEvent();
}
return EV_OK;
}
Again, I really dislike the usage of the array, part because arrays are obsolete
with STL and boost waving around, and part because it represents hard-coded
data. Which is bad, because we’d want to be able to (hypothetically) change
those keys on the fly. Adding this functionality is left as an exercise for the
reader (don’t you just love when people say that)?
The last nasty things to do are hide the functions to subscribe listeners for mouse
and key input, since we’re already grabbing all the data and converting it into
action events, and to write a constructor.
private:
void
void
void
void
addKeyListener( KeyListener* );
delKeyListener( KeyListener* );
addMouseListener( MouseListener* );
delMouseListener( MouseListener* );
The constructor is really important because it has to explicitly initialize the listener
and subscribe it to receive events. Which caused much frustration for me at one
point, so grr.
IButton() : _buttonlist( this ),
_pressed( false ) {
IComponent::addKeyListener( &_buttonlist );
IComponent::addMouseListener( &_buttonlist );
IComponent::addMouseMoveListener( &_buttonlist );
}
And that essentially covers the behavioral functionality of our IButton class. Now
lets derive something from it so that we can actually… instantiate it (remember,
the draw method is still undefined).
TexturedButton
is a class which is basically a texture rendered to the screen
which you can click. We’ll get into some more advanced stylization techniques a
little later, but this should be pretty firm territory at this point.
TexturedButton
First, and most importantly – it has to be a templated class. Our template
parameter is the type of the graphics adapter that we’ll be using to render. We
also need a pointer to an adapter so we can make the appropriate render calls.
template < class GfxAdapter >
class TexturedButton : public IButton
GfxAdapter* _gfx;
TexturedButton( GfxAdapter* gfx )
: _gfx( gfx )
{ }
Since it’s a textured button, we need a texture for it, as well as methods to modify
it. Remember we’re getting the texture type from the graphics adapter itself (the
TextureType typedef) so we don’t need to know what it is, just that it represents a
texture in memory.
typedef typename GfxAdapter::TextureType TextureType;
TextureType _tex;
TextureType setTexture( TextureType tex ) {
TextureType oldtex = _tex;
_tex = tex;
IComponent::setSize( _gfx->getWidth( tex ),
_gfx->getHeight( tex ) );
return oldtex;
}
TextureType getTexture() {
return _tex;
}
The last task is to write the draw method –
virtual void draw( int rel_x, int rel_y ) {
int x = getX() + rel_x;
int y = getY() + rel_y;
_gfx->blitTexture( _tex, x, y );
}
And we’ve got a fully functional button class. The code required to create a single
class when the interface is developed is very minimal – the idea behind the
system is pure code reuse. Develop basecode once, then never have to worry
about it (much).
Styles
Now, assume we want to create MFC-looking controls. Each control has a variety
of attributes – colors, border sizes, all kinds of crazy things. The trick is how do
you store and manage all of this information?
Keeping a set of attributes (a style) in each instance would be ridiculous. At the
same time, we want to be able to share the same style between multiple classes,
which suggests we’re going to want to use a policy class, probably with static
members.
Taking it a step further, perhaps we want to have copies of the same type of
control, but each with a different style. We’d want a way to define different
instances of the same class, while still making the members static.
Templates are key, once again.
Our class definition for this default style is pretty simple –
template < class GfxAdapter, int instance = 0 >
struct DefaultStyle
Using the instance parameter, we can refer to a specific style (at compile-time)
with an integral identifier, ie –
DefaultStyle< GfxAdapter >
hStyle0;
DefaultStyle< GfxAdapter, 1 > hStyle1;
Now that we’ve got a class, we can start defining some members. Lets do some
typedef’ing to make life easier (since we have to use adapter-defined classes) –
typedef typename GfxAdapter::ColorType ColorType;
typedef typename GfxAdapter::SizeType SizeType;
And we’re set to define a bunch of attributes. Each attribute needs to be static,
since we want changes to the style class to be reflected on every control which
inherits from it. Additionally, we want to have accessors and mutators for each
attribute.
I’ll spare the monotony, but some sample attributes might be –
static ColorType _colorForeground;
static SizeType _sizeTitleBar;
And so forth. That’s basically all a style class is – a policy class which allows us
to easily change the “look and feel” of any classes built using it.
Button
Our next step it to take our happy style class and make a win32-looking button
with it. Lets start by declaring a Button class.
template < class GfxAdapter, class Style = DefaultStyle< GfxAdapter > >
class Button : public IButton, public Style;
Again, we want to have a pointer to a graphics adapter at all times –
GfxAdapter* _gfx;
explicit Button( GfxAdapter* gfx )
: _gfx( gfx ) { }
We also want to give our happy button a caption. Which isn’t too bad –
std::string _caption;
void setCaption( std::string caption ) {
_caption = caption; }
The last part to implement, again, is the drawing function. The drawing function is
where the attributes in the style class. It takes some tinkering to make the
controls look right, but its pretty easy to make decent-looking graphics just by
filling rectangles.
Anyway, on with the code dump –
void draw( int rel_x, int rel_y ) {
int x = getX() + rel_x; int w = getW();
int y = getY() + rel_y; int h = getH();
_gfx->fillRect( _colorShadow, x, y, w, h );
int midoffset = _pressed ? _sizeBorder : 0;
_gfx->fillRect( _colorHighlight, x + midoffset, y + midoffset,
w - _sizeBorder, h - _sizeBorder );
_gfx->fillRect( _colorForeground, x + _sizeBorder,
y + _sizeBorder, w - 2*_sizeBorder, h - 2*_sizeBorder );
_gfx->drawText( _caption, _colorText, x + 3, y );
}
This yields a nice, happy button control for us to play with!
Now let’s move onto how to get that pretty window to put our buttons in! (We
could have just put the buttons into the main window, but that would be boring
and un-pretty. Just like the nasty code behind that little window. Arrgh!)
Window
The Window class is formed just like the Button class previously discussed – it
utilizes a template for both a graphics adapter parameter and a style parameter
and implements behavior by subscribing an internal listener to itself. The listener
is the bulk of the class, and modifies the window by sending window events to it.
Since we’ve already laid out the functionality for the handling of these events, it
makes our life a little easier.
_,.-'``'-.,_,.='`` End ``'-.,_,.-'``'-.,_
Download