ClanLib features a complete graphical user interface (GUI) toolkit. A GUI toolkit normally consists of a general GUI component framework, and then some standard components generally needed by most applications. In clanGUI, the different user interface elements are split into components, handled by clan::GUIComponent, where each component covers a specific part of the screen and performs a specific role.
The components are arranged in a tree-like structure, which for a simple application may look like this:
clan::ListView listview(&window)
clan::Scrollbar vertical_scroll(&listview)
clan::Scrollbar horizontal_scroll(&listview)
clan::Button ok_button("Ok", &window)
clan::Button cancel_button("Cancel", &window)
Besides clan::GUIComponent, the other base components of the GUI framework are:
The GUI Manager
clan::GUIManager manages the environment in which GUI components run. The functions of the GUI manager can generally be split into these groups:
-
Handling of the GUI message queue.
-
Root of all top-level components.
-
Container for the basic GUI subsystems: window manager, resource manager, CSS document and so on.
-
Provides a default message loop.
At the heart of the GUI system is the message queue and the message pump. All communication between the window manager and the GUI is done by queueing messages onto the message queue, which then are dispatched to the relevant GUI component. For example, if the user hits a key, this generates a clan::GUIMessage_Input message, which is sent to the currently focused component.
There are many situations where the application will want to filter messages, change where they are going, or simply perform other operations when there are no message to be dealt with. To maximize control over what happens between messages, the application can read and dispatch the messages itself, or it can call the default message loop via clan::GUIManager::exec().
But enough talking, here's how you create a GUI and pump the message queue:
gui_manager.set_resources(resources);
gui_manager.set_css_document(css_document);
button1.set_geometry(
clan::Rect(100, 100, 200, 120));
button1.set_text("Okay!");
button1.func_clicked().set(&on_button1_clicked, &button1);
{
if (!message.is_consumed())
}
{
}
The message pump above does exactly the same as clan::GUIManager::exec, so if you do not need any additional processing, you can just call that one.
Components
As mentioned in the introduction, the screen is split into components, which are rectangular areas where each component draws itself and processes input. Components can be split into two types: top-level components and child components.
The top-level components are constructed using a clan::GUITopLevelDescription class, which is passed on to the window manager for further interpretation. The system window manager creates a clan::DisplayWindow for each top-level component, where the information in the description class is used to give that window a title, icon, border and other styling properties. The texture window manager creates a new texture for the window and ignores most of the other properties of the description.
Child components are constructed by only passing a parent component to the constructor of lan::GUIComponent. The window manager is not aware of child components and does not require any special extra data to construct them. A child component is initially created at (0,0) with a (0,0) size, which means that you will have to explicitly set the geometry of the component after you created it.
Component Messages
The GUI system communicates with the component using GUI messages. They are dispatched to the component, which means that clan::GUIManager::dispatch_message() invokes the callback given by clan::GUIComponent::func_process_message(). It can receive messages of the following types:
When the GUI system needs the component to paint itself, it will invoke clan::GUIComponent::func_render(). Likewise, clan::GUIComponent has other callbacks you can hook into to react to certain events.
Example of a component that changes color if mouse is hovering above it:
public:
{
set_type_name("MyComponent");
}
private:
{
clan::Draw::fill(
gc,
client,
}
{
if (message.is_type(clan::GUIMessage_Pointer::get_type_name()))
{
switch (msg_pointer.get_pointer_type())
{
above = true;
break;
above = false;
break;
}
}
}
bool above;
};
Using clanGUI Compared to clanDisplay Rendering
One of the things that confuse people the most when it comes to the GUI is how the screen is updated. In particular, if you are attempting to port an application using clanDisplay to clanGUI, your application typically does something along these lines:
while (!exit)
{
update_game_logic();
render_game(gc);
displaywindow.flip();
}
A common mistake is to assume that if you want to render the GUI, you can simply add a call to gui_manager.exec() just before the displaywindow.flip() line. This will not work. The gui_manager.exec() function will not exit until something in the GUI calls clan::GUIComponent::exit_with_code(), causing only the GUI to be rendered and your game will disappear and hang.
The best way to fix this problem is to get rid of your own while(!exit) loop and create your game screen as a clan::GUIComponent. Since your game probably wants to constantly repaint itself, you can use the clan::GUIComponent::set_constant_repaint() function to achieve this. Also, depending on how often your game logic is to be run, you can either place this in the render callback function or use a clan::Timer to call it at desired intervals.
Here's a simple example how how a game component might look like:
{
public:
: clan::Window(manager, desc)
{
button->
set_text(
"A button on top of my game!");
}
private:
{
update_game_logic();
render_game(gc);
}
This document describes the behavior of various parts of clanGUI into further detail. It is mostly intended for developers working directly on clanGUI, acting as a specification for how things are meant to work (as opposed to how things might actually work due to bugs).
Input messages are processed as follows
-
Input message is delivered to the window manager from the OS
-
Window manager attributes the message to the active window. For the system WM this is always the window the WM_INPUT message was designated, but for the texture WM it does its internal tracking of which toplevel window is the currently active window
-
The GUI manager is notified by the WM about a new message attributed to a specific toplevel window. Each toplevel window has a single focused component that gets the first chance to process this message. The message is delivered to this component
-
If the focus component does not consume the message, the message is routed to the parent window. This continues until a component consumes the message or it reaches the toplevel component
-
If the message is still not consumed after the toplevel component, the accelerator table gets a chance to process the message
-
If the accelerator table does not consume the message, the dialog input message handler processes the message. This is where things like tabbing between controls and the default action of enter is handled
Focus switching details
-
Tab order is controlled by the component tree and the FocusPolicy of the individual components. The focus moving order is depth-first iteration of the component tree.
-
If a child component has focus and is about to be made hidden, clan::GUIComponent::focus_next() is called before making it invisible
-
If the child component being made hidden is still the focused component after focus_next(), the focus component is set to null
-
If the current focus component is null, and a component that accepts focus is made visible, the shown component will get the focus. Otherwise the focus will remain null
-
If <tab>, <alt-tab> or arrow keys reach the dialog input handler, the messages are translated into focus_next() or focus_previous() calls on the currently focused component.
-
If FocusPolicy of the currently focused component is focus_group, all components with the same group-name -attribute will be skipped over
-
If FocusPolicy of the component to be focused is focus_group, the focus is set on the selected component in the group. If no component in the group is set as selected, normal focus changing rules apply.
-
Focus indication (dashed line surrounding text) is drawn on the focused component only after the keyboard has been used to navigate the dialog. Otherwise it is not drawn.
-
Focusable components receive focus on mouse clicks after keyboard navigation has been used.
Dialog input handler details
-
A toplevel window has one single default action component. If <enter> or <space> reaches the dialog input handler, the messages are sent to the default action component
-
The defaulted component is painted with the default state when the following conditions are met:
-
The top level window has activation.
-
The focused component does not consume the enter key.
-
Any component that consumes the enter key may paint itself defaulted when focused.
Geometry details
-
The GUI consists of two types of components: top-level and child components
-
The geometry rectangle of a top-level component is the client area of the windowing system window, but where the origin is the upper left corner of the primary screen (this differs from GetClientRect() where the origin is the client area itself)
-
The geometry rectangle of a child component is its position relative to its parent component
-
Top-level components also have a window frame geometry, which include the caption and border frames of the window. This geometry is retrieved and set using get_window_geometry() and set_window_geometry(). The origin of the rect is also the upper left corner of the primary screen.
-
In Windows, the workspace is the area defined by GetWindowPlacement() and SetWindowPlacement(). This is the screen area that excludes the task bar and is the only 'safe' geometry to be stored and restored in Windows between sessions. When we add support for the workspace area, those functions should be named get_window_placement() and set_window_placement() and should not replace any of the other geometry functions