Welcome, Guest. Please login or register. Did you miss your activation email?

Author Topic: Theory behind a simple GUI  (Read 4254 times)

0 Members and 1 Guest are viewing this topic.

unranked86

  • Newbie
  • *
  • Posts: 37
    • View Profile
Theory behind a simple GUI
« on: July 30, 2011, 10:26:28 pm »
Hi there everybody!
This might be a trivial question or problem. But I have a hard time figuring it out.

Actually I will just copy-paste (really) an e-mail adressed to Nexus, and I hope he doesn't mind it.

Here I go:

Quote
"Hi Nexus!

Let me introduce myself. I am Peter, unranked86 on the sfml forums. I'm a beginner at C++, and SFML, yet I find them very powerful.
I'm sorry to bother you in this way, and sorry about my english. Well, I shall get to point, I think.
Your work on Thor truly amazes me. It is a very good library, and I'm thinking about using it in my future projects.
But, something I really want to ask you is, how should I write a very basic GUI. By very basic, I mean, some menu entries, some buttons, and windows (widgets ?) What I'm interested is in, not concrete code, but some starting point.

What I have so far in theory, is something like this:

MenuSystem class, which manages all the MenuEntries, probably a std::vector or std::list. This class should allow me to add and remove MenuEntry objects, and control the behaviour of them. But, how can I achieve that ? I mean, I obviously can add and remove new items to a std::vector, but how can I make them to execute a certain "command". For example there is a menu with Newgame, Loadgame and Exitgame objects. Like the names suggests Newgame starts a new game, Loadgame loads a game, and Exitgame quits the application. These MenuEntries are just sf::Text objects basicaly wrapped in a class like this (not real code):

class MenuEntry
{
public:
 MenuEntry (unsigned int x, unsigned y, std::string label); // sets the sf::Text object's parameters
 ~MenuEntry ();
 virtual void Execute () = 0; // run the wanted code, like creating a new game
private:
 sf::Text mLabel;
};

This is just a base class, and I derive more classes from this. But my real problem is, which gives me a hard time, how do I tell the MenuSystem class about how to execute the right "command" in the Execute() function ? And how does it know which MenuEntry is selected ? Well, yes, the MenuEntry class needs an accessor funciotn 'bool IsSelected ()', which is true when the user selects it, but how does the code know this ?
I don't know if I'm clear enough. Actually I checked the source of cpGUI, but gained almost nothing from it."

Nexus

  • SFML Team
  • Hero Member
  • *****
  • Posts: 6287
  • Thor Developer
    • View Profile
    • Bromeon
Theory behind a simple GUI
« Reply #1 on: July 30, 2011, 10:43:44 pm »
As promised in the mail, I respond here :)

Quote from: "unranked86"
But my real problem is, which gives me a hard time, how do I tell the MenuSystem class about how to execute the right "command" in the Execute() function ? And how does it know which MenuEntry is selected ? Well, yes, the MenuEntry class needs an accessor funciotn 'bool IsSelected ()', which is true when the user selects it, but how does the code know this ?
I would go for an event-based approach: You feed your GUI system with the events generated by SFML. For example, you can write a function void MenuSystem::OnEvent(const sf::Event& event), which is invoked in your event loop and which itself calls void MenuEntry::OnEvent(const sf::Event& event) for each contained entry. This function in turn reacts corresponding to each event. It can check whether the event parameter has type sf::Event::MouseButtonPressed, and in this case whether the mouse coordinates are inside the menu entry's bounds. If both conditions are true, you can call Execute().
Code: [Select]
void MenuEntry::OnEvent(const sf::Event& event)
{
    switch (event.Type)
    {
        case sf::Event::MouseButtonPressed:
            // Get mouse position from event.MouseButton.X and Y
            // If mouse is over the menu entry, call Execute()
            break;

        // maybe other events        
    }
}

The advantage of this approach is that it scales quite well: Your GUI can also react to other user interaction. For example, you can catch sf::Event::MouseMoved events and highlight an entry if the mouse is over it. Or open a menu if a certain key is pressed.

 
Quote from: "unranked86"
But, how can achieve that ? I mean, I obviously can add and remove new items to a std::vector, but how can I make them to execute a certain "command".
Are you familiar with the concepts of function pointers and callables in C++? If not, you could take a look at Boost.Function. It is basically a class template that allows you to store functions, member functions and functors in variables, deal with them like any other C++ object, and call them at any time with any arguments. When you use std::tr1::function or std::function (which are the same as boost::function, but the one in namespace std is only available in C++0x), you don't need a virtual Execute() method anymore.

A basic example to see how std::function works:
Code: [Select]
int Add(int x, int y)      { return x + y; }
int Multiply(int x, int y) { return x * y; }

#include <functional>

int main()
{
    // Create function object that returns int and takes two int parameters
    // Initialize it with function pointer to Multiply()
    std::function<int(int, int)> fn = &Multiply;

    // Call function via fn, the result is 3*2
    int result = fn(3, 2);

    // Assign function pointer to Add() to object
    fn = &Add;

    // Call function again, now the result is 3+2
    result = fn(3, 2);

This concept is even more powerful when you use binders (Boost.Bind) to forward arbitrary arguments to existing functions.
Zloxx II: action platformer
Thor Library: particle systems, animations, dot products, ...
SFML Game Development:

danman

  • Hero Member
  • *****
  • Posts: 1121
    • View Profile
    • Email
Theory behind a simple GUI
« Reply #2 on: July 31, 2011, 12:41:49 am »
I work on a GUI for SFML, and i've done that design :

Code: [Select]

class Widget
{
....
protected:
    virtual bool IsValidMouseEvent ( x, y );
    virtual void OnMouseMoveEvent( sf::Event& event );
    virtual void OnMouseButtonPressedEvent( sf::Event& event );
    virtual void OnMouseClickEvent( sf::Event& event );
    virtual void OnMouseDoubleClickEvent( sf::Event& event );
... etc

private:
    zin::EventHandler m_EventHandler;

};



these events are provided by the event handler, you can imagine this track :
window event => main => gui application => event handler => widgets => event handlers => widget => ...

it provides a very powerful and lazzy concept to write new widgets, without care about the "view" of the widget (because it's the event handler job), because i use a "primitive" class that can define absolutely anything (a polygon, a rectangle, a circle ...) to define his reactive area, and my friend had worked on an event-based-network library to implement complex behavior like double click with timeout in a few lines ( 5 lines approximately, and 2 locals variables) .

One problem we had actually is the lack of a vectorial system to build the GUI design, because a extended image on a button is uggly :( .

I think you may try to get information with Qt or GTK internal concepts, or see SFGUI ;)

Sorry for my very bad english and these plain explanations about this domain, but it's a very big debate ;) .

edit: ah, i forgot to say that : don't inherit from sf::Drawable, it's just problems you will have later ;) .
Pointilleur professionnel

unranked86

  • Newbie
  • *
  • Posts: 37
    • View Profile
Theory behind a simple GUI
« Reply #3 on: July 31, 2011, 02:53:06 pm »
After re-reading what I wrote yesterday, I realized I was talking about a completely different thing what I actually have. I am terribly sorry about this, but I had some beers ;)

Well, I will try to explain the real thing this time.
So, I have a MenuEntry class like this:
Code: [Select]

class MenuEntry
{
public:
MenuEntry (std::string label, int x, int y, const sf::Font& font);
~MenuEntry ();

void SelectEntry ();
void DeSelectEntry ();
void Update (); // this function checks the IsSelected boolean, and changes the color of the sf::Text object
void Draw (sf::RenderTarget& target);

private:
bool IsSelected;
int mPosX;
int mPosY;
sf::Text mLabel;

};

And I create instances of it:
Code: [Select]

MenuEntry newGame(...);
MenuEntry exitGame(...);


But I want to make my life easier, so I created a manager-like class, called MenuSystem. So decided to store all my MenuEntry objects in a container, and then I can update and draw them with one line of code, instead writing:
Code: [Select]

menu1.Update();
menu2.Update();
....
menuN.Update();

menu1.Draw();
menu2.Draw();
....
menuN.Draw();

So at the moment I update and draw them like this:
Code: [Select]

MenuSystem test;

MenuEntry menu1, menu2 ....menuN;
test.AddEntry(menu1);
test.AddEntry(menu2);
....
test.AddEntry(menuN);

// in main loop
test.UpdateEntries();
test.DrawEntries();


This is still not the thing I really want. Because I have to add all MenuEntry objects to the manager by hand, which is not cool. But works. The real problem however is this: this MenuSystem class only stores, updates and draws the MenuEntry objects, but it doesn't know what they do. So I can't tell this manager to do something like this:
Code: [Select]

if "newGame" is selected, create a new game
if "exitGame" is selected, close the application

What I tried to do, searching in the vector with find from <algorithm> didn't worked. Maybe I used in a wrong way.

I may be just overcomplicating this, but I'm stuck. Should I forget about the manager, and just hard code the menu ? But I really want a nice and reusable code.

Nexus

  • SFML Team
  • Hero Member
  • *****
  • Posts: 6287
  • Thor Developer
    • View Profile
    • Bromeon
Theory behind a simple GUI
« Reply #4 on: July 31, 2011, 03:39:07 pm »
I think the above advice still applies to your problem, it would just need some refactoring of your design ;)

In fact I think an event-based approach is better, or how do you make sure that IsSelected() is only once true? That is, if the mouse button is held down during multiple frames, how to avoid that multiple games are created?

Anyway, I don't really see the problem... Can't you just loop through every MenuEntry in MenuSystem and return true if the passed argument (e.g. "newGame") matches the entry's name?
Zloxx II: action platformer
Thor Library: particle systems, animations, dot products, ...
SFML Game Development:

unranked86

  • Newbie
  • *
  • Posts: 37
    • View Profile
Theory behind a simple GUI
« Reply #5 on: July 31, 2011, 05:22:30 pm »
Based on your advice about the OnEvent() I rewrited the MenuSystem class, and it is just working perfect :) Now every MenuEntry has a "real label" which is an enum. So I add a new entry like this:
Code: [Select]

Test.AddMenuEntry("New Game", NEW);
Test.AddMenuEntry("Load Game", LOAD);
Test.AddMenuEntry("Options", OPTIONS);
Test.AddMenuEntry("Exit Game", EXIT);

I modified the whole thing, so the MenuSystem automatically sets the position of all the entries on the screen, based on their order in the std::vector. (This is just a bonus thing, which I gained by refactoring the stuff :) )
And then, updating and drawing them is the same as before:
Code: [Select]

Test.UpdateEntries();
Test.DrawEntries(App);


Quote from: "Nexus"

In fact I think an event-based approach is better, or how do you make sure that IsSelected() is only once true? That is, if the mouse button is held down during multiple frames, how to avoid that multiple games are created?


Hopefully my state manager will take care of this problem. Technically, when a menu is selected, it returns a variable to the state manager and some other part of the application will get the control, so there is no chance that there will be multiple games created. But I see your point, and I will test it later, to make sure it does not happen.
Thank you very much, I gained a lot from this topic!

Nexus

  • SFML Team
  • Hero Member
  • *****
  • Posts: 6287
  • Thor Developer
    • View Profile
    • Bromeon
Theory behind a simple GUI
« Reply #6 on: August 01, 2011, 12:47:32 pm »
An issue with your code I noticed:
Code: [Select]
class MenuEntry
{
public:
   MenuEntry (std::string label, int x, int y, const sf::Font& font);
   ~MenuEntry ();
   ...
};

If you declare the destructor, you usually also have to declare copy constructor and copy assignment operator (Rule of the Big Three). But I don't think in your example there is a need for a manually-defined destructor, so just omit it.
Zloxx II: action platformer
Thor Library: particle systems, animations, dot products, ...
SFML Game Development:

unranked86

  • Newbie
  • *
  • Posts: 37
    • View Profile
Theory behind a simple GUI
« Reply #7 on: August 01, 2011, 01:53:40 pm »
Quote from: "Nexus"

If you declare the destructor, you usually also have to declare copy constructor and copy assignment operator (Rule of the Big Three). But I don't think in your example there is a need for a manually-defined destructor, so just omit it.


Heh, thanks for pointing that out! :) After reading that rule, I realized that my current project is full of errors, and I just mindlessly use raw pointers, new and delete, in constructors and destructors respectively, without declaring copy constructors and assignment operators, and I thought I'm doing the right thing. It's about time I start fixing them :)

Nexus

  • SFML Team
  • Hero Member
  • *****
  • Posts: 6287
  • Thor Developer
    • View Profile
    • Bromeon
Theory behind a simple GUI
« Reply #8 on: August 01, 2011, 03:25:21 pm »
The better approach is not to use manual memory management (new and delete) but instead RAII classes like STL containers or smart pointers, that automatically copy and destroy objects when necessary. Then, the compiler-generated copy constructor, copy assignment operator and destructor do the right thing.

The need to define the Big Three is often a hint that you don't abstract your design enough from low-level features (such as new/delete). If you look at Thor, most of the classes don't implement the Big Three, since the compiler-generated functions work correctly. Classes without meaningful copy semantics are made noncopyable (copy constructor and assignment operator are private), I achieve this by deriving from a NonCopyable base class.
Zloxx II: action platformer
Thor Library: particle systems, animations, dot products, ...
SFML Game Development:

unranked86

  • Newbie
  • *
  • Posts: 37
    • View Profile
Theory behind a simple GUI
« Reply #9 on: August 01, 2011, 03:48:02 pm »
Quote from: "Nexus"
The better approach is not to use manual memory management (new and delete) but instead RAII classes like STL containers or smart pointers, that automatically copy and destroy objects when necessary. Then, the compiler-generated copy constructor, copy assignment operator and destructor do the right thing.

The need to define the Big Three is often a hint that you don't abstract your design enough from low-level features (such as new/delete). If you look at Thor, most of the classes don't implement the Big Three, since the compiler-generated functions work correctly. Classes without meaningful copy semantics are made noncopyable (copy constructor and assignment operator are private), I achieve this by deriving from a NonCopyable base class.


This is exactly what I did in the last few hours. I changed the raw pointers to std::tr1::shared_ptr (it suits my needs), and where it was needed I derived my classes from a NonCopyable class, like you said.
Well, reading the documents about smart pointers took more time than modify my code, to be honest :)
Again, thanks for the advice :)

Nexus

  • SFML Team
  • Hero Member
  • *****
  • Posts: 6287
  • Thor Developer
    • View Profile
    • Bromeon
Theory behind a simple GUI
« Reply #10 on: August 01, 2011, 04:33:40 pm »
With std::tr1::shared_ptr, you have to be careful.

First, it doesn't copy the stored pointer, but share it (as the name suggests). That might lead to unintuitive behavior if you copy a object, but indeed its members are shared among the original and newly created object. If you change an attribute of the old object, the new (seemengly independent) object is also affected. Unfortunately, smart pointers with true deep copy semantics are difficult to find, that's why I have written thor::CopiedPtr. Meanwhile, I really use this smart pointer a lot.

Second, std::tr1::shared_ptr is the most expensive smart pointer and has a lot of overhead you don't always need: Thread safety, the dynamic storage for a reference counter, support for custom deleters. In many cases, a more lightweight smart pointer (e.g. std::unique_ptr in C++0x) is enough.
Zloxx II: action platformer
Thor Library: particle systems, animations, dot products, ...
SFML Game Development:

 

anything