• Advertisement
Sign in to follow this  

Object oriented GUI concept

This topic is 4303 days old which is more than the 365 day threshold we allow for new replies. Please post a new topic.

If you intended to correct an error in the post then please contact us.

Recommended Posts

Hi, I'm working on a GUI for my game and I'm trying to use nice OOP. So far I've got a base class called CGUIObject, which provides an interface with functions like render() or handle_event(). I'm planing to derive classes like CWindow, CButton etc. from CGUIObject. In my CGame class, I'm going to store the CGUIObjects using a std::map<string, CGUIObject*>. The whole thing is a little more complex than that (e.g. a window can have its own GUIObjects), but this should suffice. My question is: What would be a good way to define the action that a button executes? You could do something like this (pseudo c++):
class CGame
{
public:
   CGame() { objects["Pause"] = new CPauseButton(this); }
   ~CGame() {}

   bool execute() { //for each guiobject render() & handle_event() }

   void pause() { /*...*/ }

private:
   std::map<string, CGUIObject*> objects;
}

class CGUIObject
{
public:
   CGame* game;

   virtual void render()=0;
   virtual void handle_event()=0;
}

class CButton : public CGUIObject
{
public:
   int x, y;
   string caption;

   //and everything else a button needs
}

class CPauseButton : public CButton
{
public:
   CPauseButton(CGame* _game) {game = _game}
   ~CPauseButton() {}

   virtual void render(){ /*...*/ }
   virtual void handle_event()
   {
      //if the button was clicked
      game->pause();
   }
}


Or, you could make CGame game global. Then there would be no need to pass the pointer around. Another idea was this (again, pseudo c++):
class CGame
{
public:
   CGame() { objects["Pause"] = new CButton(this, &CGame::pause); }
   ~CGame() {}

   bool execute() { //for each guiobject render() & handle_event() }

   void pause() { /*...*/ }

private:
   std::map<string, CGUIObject*> objects;
}

class CGUIObject
{
public:
   CGame* game;

   virtual void render()=0;
   virtual void handle_event()=0;
}

class CButton : public CGUIObject
{
public:
   CButton(CGame* _game, void (CGame::*_action)()){ game=_game; action = _action; }
   ~CButton(){}

   int x, y;
   string caption;
   void (CGame::*action)();

   virtual void render() { /*...*/ };
   virtual void handle_event()
   {
      //if the button was clicked
      (*game.*action)();
   }
}


Which version do you like better? Any other suggesions? I'm sure there are many guys around here who have more experience with this than I do. I appreciate your comments!

Share this post


Link to post
Share on other sites
Advertisement
I definitely like the callback function approach better. That way you don't need to derive a class from Button everytime you need a button to do something different.

My only gripe would be with the interdependency issues going on - the game is dependent on the GUI and the GUI is dependent on the game. If I were you, I'd probably try to avoid having the GUI base classes know about the game class. It doesn't really matter in any practical sense, it's just an OO design issue to watch out for.

Share this post


Link to post
Share on other sites
I would not give the basic GUIObject class a reference to the Game instance since I assume that most GUIObject instances don't use it. Instead, I prefer an instance set-up (i.e. normally by c'tor) with the appropriate references.

If the Game instance is unique and very frequently accessed you may take into account to make it a singleton.

Okay, maybe you have a small GUI in fact running in your game, but it is not common to derive a particular class for each action to perform. E.g. in an application there would appear dozens to hundrets of button classes differing only by their action. More often callbacks, or to be more OOP like Listeners/Observers are used. You may also take a look at the Mediator pattern (see GoF).

Share this post


Link to post
Share on other sites
Thanks for your feedback! I'll probably go with the second version then. I'm not sure how to get rid of the dependencies between CButton and CGame. If I want the CButton to influence CGame, I'm bound to have interdependencies between them, right?
I'm also interested in the instance set-up with appropriate references, do you have an example of how that would look like?

Share this post


Link to post
Share on other sites
Quote:
Original post by kloffy
I'm also interested in the instance set-up with appropriate references, do you have an example of how that would look like?

That's nothing mysterious.

Using your example of a the pausing button, it may look like this:

// c'tor gets all collaborators needed, and stores them in member fields
CPauseButton::CPauseButton(Game& gameInstance)
: _gameInstance(gameInstance) { }

// typical instantiation
new CPauseButton(gameInstance);


Simply supply all dependent objects at initialization time. At initialization you have to know the _exact_ type, so it is not hard to supply specific parameter.

However, the above example isn't good since it uses a special purpose button for a single acton, and that's not good as already seen ;)

So, here is a Listener/Mediator example:

class Button
: public GUIObject {

public: // listener

class Listener {

public:

virtual void noticeAction(Button* sender) =0;

};

public: // c'tors

Button(Listener* listener)
: _listener(listener) { }

public: // action

void onPress() {
if(_listener) {
_listener->noticeAction(this);
}
}

};

class Game
: public Button::Listener {

public: // c'tors

Game() {
objects["Pause"] = new Button(this);
objects["Resume"] = new Button(this);
}

public: // listeners

void noticeAction(Button* sender) {
if(sender==objects["Pause"]) {
this->pause();
} else if(sender==objects["Resume"]) {
this->resume();
} else if(...) {
// doing something appropriate
} else {
// error: unknown sender; e.g. throw exception
}
}

};


Here the Game class's noticeAction routine is implemented as mediator, what means that the routine first checks which of all known possible senders is the one that actually has invoked the routine (a kind of dispatching). Then the routine performs the appropriate action.

Similarly a, say, slider widget could be implemented, like here:

class Slider
: public GUIObject {

public: // listener

class Listener {

public:

virtual void noticeChange(Slider* sender) =0;

};

public: // c'tors

Slider(Listener* listener)
: _listener(listener) { }

public: // action

void onDrag() {
if(_listener) {
_listener->noticeChange(this);
}
}

float getCurrentFraction() const {
// returning fraction between 0 and 1 as slider's knob position
}

};


To make life easier (and avoid multiple inheritance) one could move the Listener interface into GUIObject, so that one single routine is used for every and all action. The state could simply be extracted from the overhanded sender. (Okay, one has to do some casting then.)


Btw: If you're intersted in such stuff, I recommend you to get at least one book about software design patterns. E.g. the book of the "Gang of Four" (GoF) is it worth.

[Edited by - haegarr on April 5, 2006 12:40:34 PM]

Share this post


Link to post
Share on other sites
Quote:
Original post by kloffy
I'm not sure how to get rid of the dependencies between CButton and CGame. If I want the CButton to influence CGame, I'm bound to have interdependencies between them, right?


You don't want the CButton to influence the CGame. You want the game to subscribe to a button click and respond as appropriate. CButton has no need to know anything about CGame and all CGame needs to know is how to subscribe to a CButton click.

Share this post


Link to post
Share on other sites
I would create a general action interface like this:


class Action
{
public:
virtual bool Execute()=0;
};



Then I would make the button class take a pointer to a specific action class (Some class derived from the Action interface above). The button class might look liek this:



class Button
{
public:
//...

AssignAction(Action *new_action)
{
action=new_action;
}

void OnClick()
{
action->Execute();
}
//...
protected:
Action *action;
}



A specific implementation of the action interface may then look like this:



class PauseAction: public Action
{
public:
PauseAction(CGame *_game)
{
game=_game;
}
void Execute()
{
game->pause();
}
protected:
CGame *game;
}



This way you get all the advantages of the callback version but you don't limit yourself to a specific format of the callback function. Also this makes the gui independent of the game.

This is called the "Strategy" pattern when talking about design patterns. You can read more about it in a book called Design Patterns: elements of reusable object-oriented software.

Share this post


Link to post
Share on other sites
Ugh.


//
// This sort of binding would usually go into the base renderable
// class. Also likely into a class for containment, a map for event
// lookup, and mouse coordinates as parameters. Here with only one
// event type for clarity.
//
class Button{
public boost::function<void()> OnLeftClick;
//
// Stuff...
//
};

// Assume game is a CGame either global or created here.

Button *PauseButton=new Button();
PauseButton->OnLeftClick=boost::function<void()>(boost::bind(&CGame::pause,game));



The code that takes mouse clicks would then look through the buttons/objects to find the correct one to activate and does so.

No implicit requirement that the game be passed to the button. No restricting the function to be part of a single inheritance hierarchy. No giant listener if/else pile. No coupling between button and game, just between them both and the last line which ties them together.

Share this post


Link to post
Share on other sites
I'd even extend Telastyn's approach by adding a seperate Listener class



class Listener{
int event_type; // event type passed down by the main gui class

boost::function<void()> _callback;

void call(event); // event!=event_type? don't do anything, else _callback
};





class Button {
std::list<Listener *> l;
//
// Stuff...
//

void addListener(Listener *l);
Button();// initialize default Listener here ... like:
// Visual changes when mouse is over the Button
// or something like that
void call(eventstructure); // iterates through list and issues calls
};

// Assume game is a CGame either global or created here.

Button *PauseButton=new Button();
Listener *l=new Listener();
l->event_type= e.g. OnClick/MouseOver or whatever;
l->_callback=boost::function<void()>(boost::bind(&CGame::pause,game));
PauseButton->addListener(l);



now you only need to determine the currently active Button and pass all pending events/event sctructure to Button::call;

Share this post


Link to post
Share on other sites
UGH!

I would NOT extend it that way. Having a list, calling every single callback in every signle object just to trigger the one that is supposed to be? Random ints as event types?

Not good ideas. And what's this fixation with listeners? Your UI is going to need to determine the topmost object(s) in order to direct input to the correct object. How's it going to do that with arbitrary listener lists? How are the Actions going to determine if they're actually the top if they just recieve an event?

No, this is the sort of thing that I've done to extend it.

First, you determine the 4 basic input types based on parameters. Keyboard [key, modifier], Character [char], Mouse [event, x, y, modifier], and Mousewheel [delta, x, y, modifier]. Key, Modifier, and MouseEvent are all enumerations [Modifier is a flag for shift/alt/ctrl].

Next, you create an interface to query input bindings, accept new bindings, and control some utility flags. The utility flags I use are:

PassUp - Should a mouse event on this UI object consider it invisible.
CharInput - Flag to consider keyboard input as text. [so you don't have to bind every single key to get some text, just the char functor]
kbOpaque - Flag to catch [and discard] all keyboard input if it has not been dealt with yet.

Now the UI has an interface to use to determine where to direct input.

Finally, you create some concrete versions of that interface. One that stores no functors, and responds to queries accordingly. A few that handle only certain input, and one that handles all. For the actual storage, it's pretty simple. std::map does simple mapping for modifier -> functor-mapping. The functor mapping is again just a std::map for key or mouseevent -> functor.

Every UIobject is given an interface for input queries [defaults empty]. The actual input handling code then can easily determine which object has the key bound that it's looking for and trigger the action accordingly.

Share this post


Link to post
Share on other sites
Hi Kloffy!

I was just about to ask an almost identical question on this forum when I saw your post. Very interesting....

I'm not going to get involved with the great OOP best practise debate but I would suggest looking at boost signals for your callback method.
Boost.Signals

Good Luck.

Simon

Share this post


Link to post
Share on other sites
Just when I thought I knew which way I'm gonna do it, there comes another bunch of suggestions... :)
I'll have another look at boost. Last time I used it for serialization, I wasn't too happy with it because it was rather slow. But that's probably just the serialization part of it.

Share this post


Link to post
Share on other sites
Quote:
Original post by Telastyn
... calling every single callback in every signle object just to trigger the one that is supposed to be?...


erm,sorry,but you got me wrong there (last line of my post)

the UI would only call the Listeners of the Button(or whatever) in focus
not every single object in the hierarchy


PS.:

about the "int" event:
I wouldn't use an int type either, but kloffy's question was about getting some ideas, not concrete implementations right? ;)

cheers

[Edited by - ze moo on April 7, 2006 2:17:35 AM]

Share this post


Link to post
Share on other sites
I've got another question. I thinking about using Interfaces like IRenderable or IInteractive. For example CButton would be IRenderable and IInteractive, a textbox or an image would be IRenderable only, etc...

class IRenderable
{
// render
virtual void render(){};
}

class IInteractive
{
// bind functions to events
virtual void bind(unsigned int _eventtype, boost::function<void()>* _function){ functions[_eventtype] = _function; }

// handle events
virtual void handle_event(unsigned int _eventtype){ (*functions[_eventtype])(); }

private:
std::map<unsigned int, boost::function<void()>* > functions;
}

class CGUIObject
{
// GUI object base class
}

class CButton : public CGUIObject, public IRenderable, public IInteractive
{
public:
// everything a button needs
}

class CGame
{
public:
CGame()
{
CButton* pause = new CButton();
pause->bind(/*ON_CLICK_EVENT or something like that*/, new boost::function<void()>(boost::bind(&CGame::pause, this)));
objects["Pause"] = pause;
}
~CGame()
{

}

bool execute()
{
// this is the tricky part
}

void pause()
{
/*...*/
}

private:
std::map<string, CGUIObject*> objects;
}



The problem is finding out whether an object is IRenderable or IInteractive later on.

Again, I've thought of two ways, but I'm not satisfied with either one of them. The first one would use a dynamic cast:

// same class setup as above

class CGame
{
public:
CGame()
{
CButton* pause = new CButton();
pause->bind(/*ON_CLICK_EVENT or something like that*/, new boost::function<void()>(boost::bind(&CGame::pause, this)));
objects["Pause"] = pause;
}
~CGame()
{

}

bool execute()
{
// this is the tricky part
std::map<string, CGUIObject*>::iterator itGUIObject;

for(itGUIObject = objects.begin(); itGUIObject != objects.end(); itGUIObject++)
{
IInteractive* interact = dynamic_cast<IInteractive*>(itGUIObject->second);
if(interact)
{
interact->handle_event(eventtype);
}
}
}

void pause()
{
/*...*/
}

private:
std::map<string, CGUIObject*> objects;
}



Now, I know this is bad, because dynamic cast is slow. So I came up with another possibility:

class IRenderable
{
// render
virtual void render(){}

// helper
virtual bool is_renderable(){ return true; }
}

class IInteractive
{
// bind functions to events
virtual void bind(unsigned int _eventtype, boost::function<void()>* _function){ functions[_eventtype] = _function; }

// handle events
virtual void handle_event(unsigned int _eventtype){ (*functions[_eventtype])(); }

// helper
virtual bool is_interactive(){ return true; }

private:
std::map<unsigned int, boost::function<void()>* > functions;
}

class CGUIObject
{
// GUI object base class

virtual bool is_renderable(){ return false; }
virtual bool is_interactive(){ return false; }
}

class CButton : public CGUIObject, public IRenderable, public IInteractive
{
public:
// everything a button needs
}

class CGame
{
public:
CGame()
{
CButton* pause = new CButton();
pause->bind(/*ON_CLICK_EVENT or something like that*/, new boost::function<void()>(boost::bind(&CGame::pause, this)));
objects["Pause"] = pause;
}
~CGame()
{

}

bool execute()
{
// this is the tricky part
std::map<string, CGUIObject*>::iterator itGUIObject;

for(itGUIObject = objects.begin(); itGUIObject != objects.end(); itGUIObject++)
{
if(itGUIObject->second->is_interactive())
{
((IInteractive*)(itGUIObject->second))->handle_event(eventtype);
}
}
}

void pause()
{
/*...*/
}

private:
std::map<string, CGUIObject*> objects;
}



However, I don't think this is a very nice solution either. This is pretty basic OOP stuff and I feel kinda stupid, because I can't work it out on my own.

Share this post


Link to post
Share on other sites
To my knowledge, that's pretty much it. Such type detection is the downside to that sort of composition of interfaces.

Share this post


Link to post
Share on other sites
I see, no wonder that I couldn't come up with an elegant way to do it...

Btw.:
The second solution doesn't work the way I posted it. Since I store CGUIObject* in my map, a call to objects["Pause"]->is_renderable() always returns false, since that's the value of is_renderable() in CGUIObject.
(And ((CButton*)objects["Pause"])->is_renderable() fails, because it's ambiguous.)

However, it can be done using this setup:
             ____ CObject ____
/ | \
./ | \.
IInteractive CGUIObject IRenderable
(virtual) (virtual) (virtual)
\ | /
\_____CButton_____/


This is what it would look like in code:

#include <iostream>
using namespace std;

class CObject {
public:
virtual bool is_renderable() { return false; }
virtual bool is_serializable(){ return false; }
virtual bool is_interactive() { return false; }
};

class CRenderable: public virtual CObject {
public:
virtual bool is_renderable() { return true; }
};

class CSerializable: public virtual CObject {
public:
virtual bool is_serializable(){ return true; }
};

class CInteractive: public virtual CObject {
public:
virtual bool is_interactive() { return true; }
};

class CGUIObject: public virtual CObject {
public:
};

class CButton: public CInteractive, public CRenderable, public CGUIObject {
public:
};

class CText: public CRenderable, public CGUIObject {
public:
};

int main () {
CGUIObject* obj1 = new CButton();
CGUIObject* obj2 = new CText();
CGUIObject* obj3 = new CGUIObject();

cout << "Button\n" << obj1->is_renderable() << '\n' << obj1->is_serializable() << '\n' << obj1->is_interactive() << '\n';
cout << "Text\n" << obj2->is_renderable() << '\n' << obj2->is_serializable() << '\n' << obj2->is_interactive() << '\n';
cout << "GUIObject\n" << obj3->is_renderable() << '\n' << obj3->is_serializable() << '\n' << obj3->is_interactive() << '\n';

system("PAUSE");
return 0;
}





What do you think? Good, bad, horrible?

Share this post


Link to post
Share on other sites
Thats an excellent idea btw. I was thinking about using multiple inheritance in my 3d editor project as well to seperate reduntant implementations that were involved with single inheritance designs.

In my case I want to seperate the data representation from the data manipulation in a good oop fashion.


Share this post


Link to post
Share on other sites
Quote:
Original post by Basiror
Thats an excellent idea btw. I was thinking about using multiple inheritance in my 3d editor project as well to seperate reduntant implementations that were involved with single inheritance designs.

In my case I want to seperate the data representation from the data manipulation in a good oop fashion.

Thanks! I'm glad to hear you like my idea. The only unfortunate thing about multiple inheritance is that your not going to get around using dynamic_cast if you want to downcast. (Explanation)

Edit: Actually, nevermind. There might not be any need for casting after all.

#include <iostream>
#include <typeinfo>
using namespace std;

#pragma warning(disable:4250)

class CRenderable;
class CSerializable;
class CInteractive;

class CObject {
public:
virtual CRenderable* get_renderable() { return NULL; }
virtual CSerializable* get_serializable() { return NULL; }
virtual CInteractive* get_interactive() { return NULL; }
};

class CRenderable: public virtual CObject {
public:
virtual void render() {}

virtual CRenderable* get_renderable() { return this; }
};

class CSerializable: public virtual CObject {
public:
virtual CSerializable* get_serializable() { return this; }
};

class CInteractive: public virtual CObject {
public:
virtual CInteractive* get_interactive() { return this; }
};

class CGUIObject: public virtual CObject {
public:
};

class CButton: public CInteractive, public CRenderable, public CGUIObject {
public:
virtual void render() { cout << "Look at me, I'm a Button!\n\n"; }
};

class CText: public CRenderable, public CGUIObject {
public:
};

void print_info(CGUIObject* obj)
{
cout << typeid(*obj).name() << "\n";
cout << "Renderable: " << obj->get_renderable() << "\n";
cout << "Serializable: " << obj->get_serializable() << "\n";
cout << "Interactive: " << obj->get_interactive() << "\n\n";
};


int main () {
CGUIObject* obj1 = new CButton();
CGUIObject* obj2 = new CText();
CGUIObject* obj3 = new CGUIObject();

print_info(obj1);
print_info(obj2);
print_info(obj3);

if(obj1->get_renderable())
{
obj1->get_renderable()->render();
}

system("PAUSE");
return 0;
};




[Edited by - kloffy on April 12, 2006 6:18:50 AM]

Share this post


Link to post
Share on other sites
Sign in to follow this  

  • Advertisement