Object oriented GUI concept

Started by
17 comments, last by kloffy 18 years ago
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!
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.
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).
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?
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 fieldsCPauseButton::CPauseButton(Game& gameInstance):   _gameInstance(gameInstance) { }// typical instantiationnew 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]
Ahh, excellent. Thanks!
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.
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.
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.
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;

This topic is closed to new replies.

Advertisement