Sign in to follow this  
bensmith87

GUI Design Questions

Recommended Posts

Hi all, I'm writing a GUI, I have a couple of reasons to do so, to improve my OO skills, and so that I can use it in future projects. Here is what I have written so far. It should compile under Linux if linked with the libs SDL, SDL_ttf and SDL_gfx. So I guess it might compile under winblows also. main.cpp, sample GUI setup ...
#include "video.h"
#include "gui.h"

#include <iostream>


using namespace std;


int handleEvents(GUIManager *gui)
{
  SDL_Event event;

  while(SDL_PollEvent(&event))
  {
		switch(event.type)
    {
			case SDL_KEYDOWN:
        if(event.key.keysym.sym == SDLK_ESCAPE)
          return 0;
				break;

      case SDL_MOUSEBUTTONDOWN:
          gui->onClick(event.button.x, event.button.y);
        break;

			case SDL_QUIT:
				return 0;
				break;
		}
	}
  return 1;
}


class QuitButton : public Button
{
  public:
    bool mQuit;
    QuitButton(string text, int positionX, int positionY, int width, int height) :
      Button(text, positionX, positionY, width, height)
    {
      mQuit = false;
    }

    void onClickVirtual()
    {
      mQuit = true;
    }
};


class MyButton : public Button
{
  public:
    MyButton(string text, int positionX, int positionY, int width, int height) :
      Button(text, positionX, positionY, width, height) { }

    void onClickVirtual()
    {
      cout << "myButton has been clicked\n";
    }
};


class MyCheckBox : public CheckBox
{
  public:
    MyCheckBox(string text, int positionX, int positionY, int width, int height) :
      CheckBox(text, positionX, positionY, width, height) { }

    void onClickVirtual()
    {
      cout << "myCheckBox has been toggled\n";
    }
};


int main()
{
  Video video;
  video.setup(640, 480, 32, 0);

  GUIManager gui;

  Control::loadFont();

  Frame baseFrame(10, 10, 620, 460);

  gui.setRootControl(&baseFrame);

  Frame frame1(10, 10, 200, 200);
  Frame frame2(10, 220, 200, 200);
  Surface surface(220, 10, 390, 440);
  QuitButton quitButton("Quit", 10, 430, 60, 20);

  baseFrame.addChildControl(&frame1);
  baseFrame.addChildControl(&frame2);
  baseFrame.addChildControl(&surface);
  baseFrame.addChildControl(&quitButton);

  MyButton myButton("myButton", 10, 10, 60, 20);
  MyCheckBox myCheckBox("myCheckBox", 10, 30, 80, 20);
  Label myLabel("myLabel", 10, 50);

  frame1.addChildControl(&myButton);
  frame1.addChildControl(&myCheckBox);
  frame1.addChildControl(&myLabel);

  while(handleEvents(&gui) && !quitButton.mQuit)
  {
    video.clear();

    gui.render();

    video.flip();
  }

  return 0;
}

gui.h ...
#ifndef GUI_H_
#define GUI_H_

#include "video.h"

#include <vector>
#include <iostream>


class Control
{
  friend class CompositeControl;
  friend class GUIManager;

  private:

  protected:
    int mPositionRelativeX, mPositionRelativeY;
    int mPositionAbsoluteX, mPositionAbsoluteY;

    static TTF_Font *mFont;
    static int mFontSize;

    static int mNormalFillColor;
    static int mNormalBorderColor;
    static int mFocusFillColor;
    static int mFocusBorderColor;
    static int mTextColor;

  public:
    Control();
    
    virtual void render() = 0;
    virtual void onClick(int clickX, int clickY) = 0;

    static void loadFont();
};


class CompositeControl : public Control
{
  private:

  protected:
    std::vector<Control *> mChildControls;

  public:
    void renderChildren();
    void onClickChildren(int clickX, int clickY);
    void addChildControl(Control *childControl); 
};


class Frame : public CompositeControl
{
  private:
    int mWidth, mHeight;

  protected:

  public:
    Frame(int positionX, int positionY, int width, int height);
    void render();
    void onClick(int clickX, int clickY);
};


class Surface : public Control
{
  private:
    int mWidth, mHeight;
    SDL_Surface *mSurface;
    SDL_Rect mRect;

  protected:

  public:
    Surface(int positionX, int positionY, int width, int height);
    ~Surface();
    void render();
    void onClick(int clickX, int clickY) { clickX = 0; clickY = 0; };
};


class Button : public Control
{
  private:
    int mWidth, mHeight;
    std::string mText;

  protected:

  public:
    Button(std::string text, int positionX, int positionY, int width, int height);
    void render();
    void onClick(int clickX, int clickY);
    virtual void onClickVirtual() = 0;
};


class CheckBox : public Control
{
  private:
    int mWidth, mHeight;
    std::string mText;
    bool mValue;

  public:
    CheckBox(std::string text, int positionX, int positionY, int width, int height);
    void render();
    void onClick(int clickX, int clickY);
    virtual void onClickVirtual() = 0;
};


class Label : public Control
{
  private:
    std::string mText;

  public:
    Label(std::string text, int positionX, int positionY);
    void render();
    void onClick(int clickX, int clickY) { clickX = 0; clickY = 0; };
};


class GUIManager
{
  private:
    Control *mRootControl;

  protected:

  public:
    void setRootControl(Control *rootControl);
    void onClick(int clickX, int clickY);
    void render();
};


#endif

gui.cpp ...
#include "gui.h"

#include <functional>
#include <algorithm>
#include <iostream>

using namespace std;


TTF_Font *Control::mFont = 0;
int Control::mFontSize = 12;

int Control::mNormalFillColor = 0x307080ff;
int Control::mNormalBorderColor = 0x5090a0ff;
int Control::mFocusFillColor = 0x004050ff;
int Control::mFocusBorderColor = 0x002030ff;
int Control::mTextColor = 0xffffffff;


Control::Control()
{
  mPositionRelativeX = 0;
  mPositionRelativeY = 0;
  mPositionAbsoluteX = 0;
  mPositionAbsoluteY = 0;
}


void Control::loadFont()
{
  mFont = TTF_OpenFont("data/arial.ttf", 12);
}


void CompositeControl::renderChildren()
{
  for(vector<Control *>::const_iterator it = mChildControls.begin(); it != mChildControls.end(); ++it)
    (*it)->render();
}


void CompositeControl::onClickChildren(int clickX, int clickY)
{
  for(vector<Control *>::const_iterator it = mChildControls.begin(); it != mChildControls.end(); ++it)
    (*it)->onClick(clickX, clickY);
}


void CompositeControl::addChildControl(Control *childControl)
{
  childControl->mPositionAbsoluteX = mPositionAbsoluteX + childControl->mPositionRelativeX;
  childControl->mPositionAbsoluteY = mPositionAbsoluteY + childControl->mPositionRelativeY;

  mChildControls.push_back(childControl);
}


Frame::Frame(int positionX, int positionY, int width, int height)
{
  mPositionRelativeX = positionX;
  mPositionRelativeY = positionY;
  mWidth = width;
  mHeight = height;
}


void Frame::render()
{
  rectangleColor(SDL_GetVideoSurface(),
    mPositionAbsoluteX, mPositionAbsoluteY,
    mPositionAbsoluteX + mWidth, mPositionAbsoluteY + mHeight, mNormalBorderColor);

  renderChildren();
}


void Frame::onClick(int clickX, int clickY)
{
  if(clickX > mPositionAbsoluteX &&
     clickX < mPositionAbsoluteX + mWidth &&
     clickY > mPositionAbsoluteY &&
     clickY < mPositionAbsoluteY + mHeight)
  {
    onClickChildren(clickX, clickY);
  }  
}


Surface::Surface(int positionX, int positionY, int width, int height)
{
  mPositionRelativeX = positionX;
  mPositionRelativeY = positionY;
  mWidth = width;
  mHeight = height;
  mSurface = 0;
}


Surface::~Surface()
{
  if(mSurface)
    SDL_FreeSurface(mSurface);
}


void Surface::render()
{
  mRect.x = mPositionAbsoluteX;
  mRect.y = mPositionAbsoluteY;
  mRect.w = mWidth;
  mRect.h = mHeight;

  SDL_BlitSurface(mSurface, 0, SDL_GetVideoSurface(), &mRect);

  rectangleColor(SDL_GetVideoSurface(),
    mPositionAbsoluteX, mPositionAbsoluteY,
    mPositionAbsoluteX + mWidth, mPositionAbsoluteY + mHeight, mNormalBorderColor);
}


Button::Button(string text, int positionX, int positionY, int width, int height)
{
  mText = text;
  mPositionRelativeX = positionX;
  mPositionRelativeY = positionY;
  mWidth = width;
  mHeight = height;
}


void Button::render()
{
  boxColor(SDL_GetVideoSurface(),
    mPositionAbsoluteX, mPositionAbsoluteY,
    mPositionAbsoluteX + mWidth, mPositionAbsoluteY + mHeight, mNormalFillColor);

  rectangleColor(SDL_GetVideoSurface(),
    mPositionAbsoluteX, mPositionAbsoluteY,
    mPositionAbsoluteX + mWidth, mPositionAbsoluteY + mHeight, mNormalBorderColor);

  int offset = (mHeight - mFontSize) / 2;

  renderText(mText, mPositionAbsoluteX + offset, mPositionAbsoluteY + offset, mFont); 
}


void Button::onClick(int clickX, int clickY)
{
  if(clickX > mPositionAbsoluteX &&
     clickX < mPositionAbsoluteX + mWidth &&
     clickY > mPositionAbsoluteY &&
     clickY < mPositionAbsoluteY + mHeight)
  {
    onClickVirtual();
  }  
}


CheckBox::CheckBox(string text, int positionX, int positionY, int width, int height)
{
  mText = text;
  mPositionRelativeX = positionX;
  mPositionRelativeY = positionY;
  mWidth = width;
  mHeight = height;
  mValue = false;
}


void CheckBox::render()
{
  int boxSize = 8;
  int offsetBox = (mHeight - boxSize) / 2;
  int offsetText = (mHeight - mFontSize) / 2;

  rectangleColor(SDL_GetVideoSurface(),
    mPositionAbsoluteX, mPositionAbsoluteY + offsetBox,
    mPositionAbsoluteX + boxSize, mPositionAbsoluteY + offsetBox + boxSize, mNormalBorderColor);

  if(mValue)
  {
    boxColor(SDL_GetVideoSurface(),
      mPositionAbsoluteX + 2, mPositionAbsoluteY + offsetBox + 2,
      mPositionAbsoluteX + boxSize - 2, mPositionAbsoluteY + offsetBox + boxSize - 2, mNormalFillColor);
  }

  renderText(mText, mPositionAbsoluteX + offsetBox + boxSize, mPositionAbsoluteY + offsetText, mFont); 
}


void CheckBox::onClick(int clickX, int clickY)
{
  if(clickX > mPositionAbsoluteX &&
     clickX < mPositionAbsoluteX + mWidth &&
     clickY > mPositionAbsoluteY &&
     clickY < mPositionAbsoluteY + mHeight)
  {
    mValue = !mValue;
    onClickVirtual();
  }  
}


Label::Label(string text, int positionX, int positionY)
{
  mText = text;
  mPositionRelativeX = positionX;
  mPositionRelativeY = positionY;
}


void Label::render()
{
  renderText(mText, mPositionAbsoluteX, mPositionAbsoluteY, mFont); 
}


void GUIManager::setRootControl(Control *rootControl)
{
  mRootControl = rootControl;
  mRootControl->mPositionAbsoluteX = mRootControl->mPositionRelativeX;
  mRootControl->mPositionAbsoluteY = mRootControl->mPositionRelativeY;
}


void GUIManager::onClick(int clickX, int clickY)
{
  mRootControl->onClick(clickX, clickY);
}


void GUIManager::render()
{
  mRootControl->render();
}

The inspiration for the design come from this quote on an old thread in these forums. sb01234
Quote:
Some thoughts on overall structure of the management system: Form a tree of controls, using the composite pattern. Panels etc can have children but buttons etc can't. Store a (smart) pointer to the root control in a Manager class. The Manager class should have members onClick, onKeyPress etc (choosing which messages to use requires some careful design), which by performing collision detection etc. forward the messages to the appropriate control by traversing the tree. The manager class will need some book-keeping data such as pointers (for example, boost::weak_ptr or your own equivalent) to currently selected control etc in order to be able to know where to forward keypress calls to. All controls should also have methods onClick, onKeyPress etc. Don't fall into the trap of letting the Manager and Controls inherit from the same base class because they have somewhat similar event methods! They are NOT exactly the same and it is likely you will want to change the set of available messages and their signature in the manager and controls independently of one another. The onClick etc of your controls are part of the internal message representation of your GUI system. The onClick etc of your manager class are typically just raw forwarding of windows events to your manager system. The final aspect is how to deal with messages from controls. A common method is to let the user inherit from a button and overriding the onClick method to get a button with a particular behavior. The template method pattern can be useful here: there are always some things a button needs to do when clicked (such as changing appearance), so don't just make one onClick method. Instead, make two! One which is never overridden and called internally by the manager, and another, which is automatically called by the internally used method, and it is this one that the user overrides. Hope this helps!
My questions are... 1. What do you think of the design thus far? 2. Does each button really need a derived class of its own? 3. How do I handle text input using SDL keyboard events? Should the control have its own event handler, or should all key press's be passed to the control? 4. Is it possible to use OpenGL to render to and SDL_Surface? 5. Any other advice? Thanks in advance.

Share this post


Link to post
Share on other sites
Regarding your button question:

In high-level terms, you could look into implementing the Listener pattern to separate your GUI objects and the code responding to events on those GUI objects.

In more detailed terms:

Instead of creating a subclass for each button, you could implement the concept of an EventListener. This is basically what Java does.

The way this works is to define an interface, say MouseEventListener. This interface would provide virtual methods for different mouse events -- perhaps something like onMouseClick, onMouseDown, onMouseUp. In addition, you would have to add some methods to the Button class so that MouseEventListeners can be added, removed, and notified of mouse events.

Then, rather than creating a subclass of button, you would create a subclass of MouseEventListener to implement (or perhaps just invoke) the desired behaviour in response to the event.

To use this new design, you would create a Button instance as normal, but then would also instantiate a MouseEventListener object and register it with the Button. Then, when the user clicks on the button, the GUIManager would report this event to the Button and the Button, in turn, would loop through all of its MouseEventListeners and call the onMouseClick method on each one of them.

One advantage of this implementation is that it decouples your GUI objects (the Button, in this case) with the behaviour invoked in response to GUI actions. This, in turn, can serve to better encapsulate your core logic, which makes code reuse easier. It also helps protect against bugs when/if the GUI changes.

Share this post


Link to post
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now

Sign in to follow this