Gui II: Widget class design

Published October 21, 2014
Advertisement
Last entry: https://www.gamedev.net/blog/1930/entry-2260396-gui-i-learning-about-signalsslots/

So this time around, I'm going to talk about the engines GUIs main class, called "Widget". I took an approach similar to what QT does,
in that every widget has to derive from that class. I'm not going to show the whole thing, because it just is way too huge at this point.
Conceptually though, it looks like this:

Events/Signals:class Widget{public: // signals Signal<> SigClose; Signal<> SigClicked; Signal<> SigReleased; Signal SigActive; Signal SigMouseMove; //projected mouse position Signal SigDrag; Signal SigResize; Signal SigUpdateChildren; //events virtual void OnClick(void); //mouse clicked virtual void OnHold(bool bMouseOver); //mouse hold virtual void OnDrag(Vector2 vDistance); //mouse hold & moved virtual void OnMove(Vector2 vMousePos); //mouse moved virtual void OnRelease(bool bMouseOver); //mouse released virtual void OnMoveOn(void); //mouse moved on virtual void OnMoveOff(void); //mouse moved off virtual void OnActivate(bool bActive); //object focus altered virtual void OnClose(void); //object closed virtual void OnRedraw(void); //object needs to be redrawn virtual void OnDisable(void); //object gets disabled virtual void OnEnable(void); //object gets enabled virtual void OnParentResize(int amountX, int amountY); //parent object got resized};
So we have a bunch of signals that let other widgets or user code hook up to certain events. And then we have the events themselfs, represented by
virtual functions. Those are called i.e. in the input handling routine, and eigther perform basic widget routines like calling the correct signal, or
are simply implemented out empty just so that other widgets can derive from them, in order to achieve specific behaviour. For example, take a look at
this button-click event:void BaseButton::OnClick(MouseType mouse){ if(mouse == MouseType::LEFT) { // in case button is pushed down if(m_bDown) { if(m_downResponseState) // check if button is not locked Widget::OnClick(mouse); return; } m_cTimer.Reset(); // reset repeat state timer OnStateChange(2); // change button appearance } Widget::OnClick(mouse);};
Now I know that it is belived to be bad practice to derive from virtual functions like I did, for the reasons of
1) deriving from public virtual functions (seperation of interface/implemenation) and
2) calling base-function behaviour.

And I agree with this. Keep in mind though, this class was basically the first big thing I designed to be reusable, so there is probably a lot of design smells. Its functional though to this point,
and didn't get to cluttered to be unworkable (though I wouldn't want to know the memory footprint of this class due to all the signals and virtual functions, lol). Anyways, if I was to reimplement that
or refactore it at any point, I'd still need to be able to perform the base-classes behaviour conditionally somehow. As you can see in the button example, if the button is both down and locked, it should not emit a clicked
signal. This means I can't just do:class Widget{public: void OnClick(MouseType mouse) { SigClicked(); Clicked(mouse); } private: virtual void Clicked(MouseType mouse); // override this in child classes like button};
I think the closes to what I want would be to use a return value to determine whether or not base class function should execute:class Widget{public: void OnClick(MouseType mouse) { if(Clicked(mouse)) SigClicked(); } private: virtual bool Clicked(MouseType mouse); // override this in child classes like button};
Though I don't know if this isn't even more hacky. Maybe I'll figure out a better solution at some point.

Composition:

One last thing for this time around. I'm aware that inheritance isn't your bread and butter, especially for complex gui routines. Take a scrollbar for example. While I need class "Scrollbar" to derive from "Widget" in order to receive
events, I don't want to implement it completely new. A scrollbar is basically two buttons with a slider. This is why the widget class has a system for parenting other widgets.class Widget{public: virtual void AddChild(Widget& child); virtual void RemoveChild(Widget& child); virtual void UpdateChildren(void); // events virtual void OnParentUpdate(int x, int y, float f); //parent object got update};
A number of widgets can be added and removed from another widget. This will achieve the following things:

- Child widgets will have their origin in the parents position, so that they are chained together
- Updating widgets happens iteratively through the parent-child chain
- If the parent is set invisible, its child also aren't rendered
- If the parent is disabled (receives no events), its childs are also disabled
- Unless set otherwise, parents will clip their children on rendering

As to why those functions are virtual: This is to allow widgets to dispatch widget parenting to their own child-widgets. For example, a window has a content-area by default, where widgets are inserted.class Window : public Widget{public: void AddChild(Widget& child) override { m_area.AddChild(child); } void RemoveChild(Widget& child) override { m_area.RemoveChild(child); }private: Area m_area;};
For adding childs to the windows topbar, there can still be eigther a "GetTopbar" or "AddTopChild/RemoveTopChild"-function. This just reduces the amount of work and thought, since in 99% of the time one would want to add
widgets to the windows main area and not the topbar (at least I do).

Thats is for now, thanks for reading. Next time, I'll cover how Widgets can be positioned and configured to behave a certain way via properties.
0 likes 2 comments

Comments

coremarq
On your issue of reporting a click when a button is locked. Could you not just report the buttons state when signaling a click and let the user determine whether they want to respond to the click or not? Think about sliders and radial dials. It's nice to know the user clicked or dragged them. But don't you need to report how much and in what position/state those are in? Why can't locked state be part of it. The guy can and should be responsible for disallowing changes to a locked widget... but shouldn't the user determine how they what to handle the response?
October 21, 2014 05:03 PM
Juliean

Actually, most widgets do report the state that actually matters to the user. The "SigClicked" which I avoid this way in the buttom, offers a general stateless notification on that this specific widget has been clicked on, so that I can hook up simple functionality to it. There is a "core::Signal<bool> SigButtonChangeState;" that is called when the button really changes position, and equivalent things exist for all other widgets that require it. To be fair, I don't completely recall why I'm not calling the parents OnClick() if the button is down & locked, because looking at it right now, it doesn't make much sense. The button class was one of the first widgets I designed, and it sure shows.

But I hope you still get the general idea. If I have a slider that is locked in place for some reason, I also wouldn't want the Signal to be called, even though the normal slider signal passes on the slider position state.

October 21, 2014 05:55 PM
You must log in to join the conversation.
Don't have a GameDev.net account? Sign up!
Advertisement