Main Page | Namespace List | Class Hierarchy | Alphabetical List | Class List | Directories | File List | Namespace Members | Class Members | File Members | Related Pages | Examples

Getting started with ParaGUI

Author:
Fred Ollinger (follinge@piconap.com).

Ulf Lorenz (ulf82@users.sf.net)

Date:
04/15/2005

Content

  1. Acknowledgements
  2. Introduction and Thanks
  3. The Beginning
    1. Getting Started
    2. The Helloworld program
  4. Signals and Slots
    1. What is this signal stuff anyway?
    2. How ParaGUI does signals
    3. Hello World with button
  5. Creating a dialog
    1. Parent widgets
    2. Coordinates
    3. A simple dialog

Acknowledgements

I would like to acknowledge the original authors of this document: Tony Gale <gale@gtk.org>, Ian Main <imain@gtk.org>. They had originally written this document for GTK. Copies can be found here: http://www.gtk.org/tutorial1.2/.

The first section largely bases on their work adapted for use with ParaGUI. All the decent organization there is Tony and Ian's. Mistakes are my own. Please do not contact them with any questions regarding this document as they had nothing to do with the final version.

From the second section on, the text is not based on any other work.

Introduction and Thanks

ParaGUI is a library for creating graphical user interfaces. It is licensed using the LGPL license, so you can develop open software, free software, or even commercial non-free software without having to spend anything for licenses or royalties.

ParaGUI is built upon SDL (http://www.libsdl.org/), a cross-platform multimedia library designed to provide low level access to audio, keyboard, mouse, joystick, 3D hardware via OpenGL, and 2D video framebuffer. Thus, ParaGUI is very cross-platform as well full featured. The power of SDL is available in ParaGUI.

Special thanks go to Sam Lantinga <slouken@libsdl.org> for the great SDL library.

The primary authors of ParaGUI are:

rotozoom.cpp is based on the LGPL package SDL_rotozoom by A. Schiffler <aschiffler@home.com>. Reworked to fit into the ParaGUI framework by David Hedbor.

PhysicsFS <http://icculus.org/physfs/> is copyrighted by Ryan C. Gordon <icculus@clutteredmind.org> under the LGPL.

Thanks to the many people providing patches, fixes & ideas:

Sorry, if I forgot anyone. If I did, then please email me.

This tutorial is an attempt to document as much as possible of ParaGUI, but it is by no means complete. This tutorial assumes a good understanding of C++, and how to create C++ programs. It would be a great benefit for the reader to have previous SDL programming experience, but it shouldn't be necessary. If you are learning ParaGUI as your first widget set, please comment on how you found this tutorial, and what you had trouble with. This will help other people understand the tutorial better.

The Beginning

Getting Started

The first thing, of course, is to download the ParaGUI source and install it. You can always get the latest version from http://savannah.gnu.org/projects/paragui/ ParaGUI uses autoconf for configuration. Once the source code is unpacked, type ./configure --help to get a list of options. If you intend to internationalize your code, it may be especially useful to configure with --enable-unicode.

The Helloworld program

To begin our introduction to ParaGUI, we'll start with the simplest program possible.

#include <pgapplication.h>

int main() {
    // construct the application object
    PG_Application app;

    app.LoadTheme("default");

    // init a small window
    if(!app.InitScreen(200, 100, 0, SDL_SWSURFACE)){
        printf("Resolution not supported\n");
        exit(-1);
    }

    // Set the window caption
    app.SetCaption("Hello, World!");

    // Enter main loop 
    app.Run();

    return EXIT_SUCCESS;
}

You can compile the above program with gcc using:

g++ `paragui-config --cflags` -c hello.cpp
g++ hello.o -o hello `paragui-config --libs`

The first line compiles the hello.cpp file and creates an object file, the second links the object file with the libraries and creates the executable. paragui-config is a script that can be called to get the correct flags for compiling or linking a program using ParaGUI.

example_1.jpg

The first example program

Now let us have a closer look at the program:

#include <pgapplication.h>
All programs will of course include "pgapplication.h" which contains the class that represents an application in ParaGUI. It handles the main loop and screen attibutes.

A program must have a maximum of one PG_Application. If you try to create more than one, the constructor will exit your program with a console error message.

int main() {
    // construct the application object
    PG_Application app;
The first line is boilerplate C++.

The next line creates the ParaGUI application object. It does not need additional arguments.

    app.LoadTheme("default");
This line loads the theme. Themes are one of the greatest features of ParaGUI as they allow you to change the look and feel of an application without recoding C++. Themes will be explained later. For now, its enough to know that you should include this line as boilerplate code.

    if(!app.InitScreen(200, 100, 0, SDL_SWSURFACE)){
        printf("Resolution not supported\n");
        exit(-1);
    }
This line pops up a window or exits the application gracefully if the window cannot be created for some reason.

    app.SetCaption("Hello, World!");

    // Enter main loop 
    app.Run();
The first line here sets the caption of the window that popped up right now.

Then, the main application loop is started. This line is a bit confusing for people who are unused to dealing with gui toolkits. Why do I need to run my app? Actually, the next line is a little misleading. The application can run without the next line, but it won't be interactive unless you add a great deal of more code.

The app.Run() line inverts the usual way that an application runs. When one writes a typical non-GUI application, the commands are executed one after the other. However, in graphical applications, the program has to react to the user input, mouse movements and such. All the input is internally stored in an event list by SDL. What app.Run() does is going over this list again and again, picking out the interesting events (mouse motion, key pressing etc.) and passing them to the appropriate subwindows. When you finally quit an application, what really happens is that this loop is exited.

Signals and Slots

This section is divided in three parts. The first one explains the basic concept of signals and slots and can be ignored by those who have already worked with Qt, libsigc++ or other libraries using this concept. The second part explains some details on the implementation of this concept in ParaGUI while the third implements a first signal in the hello world program.

What is this signal stuff anyway?

Let us imagine an application that just pops up a window (as our hello world example), but with a button. Now calling app.Run() ensures that all the user activities are handled properly. In this case, if you click on a button, the button receives the information that he is clicked. So far everything is fine. But now you want to do something (usually call a function) when the button is clicked. So how do you tell the button that it should call a function when clicked?

There are several solutions to the problem. An old and particularily crude one was implemented in the first versions of the Borland C++ Builder. The buttons had some (virtual) placeholder functions and whenever you actually implemented a button, you derived from the button class and overwrote this function that is automatically called whenever the button is pressed. Another one is passing messages around that the button is pressed and waiting for one of several registered objects to accept them (as partially been done in ParaGUI up to version 1.0.4).

However, a much finer approach are function pointers. They behave just like a normal pointer, but point to functions instead of variables. You just tell the button the address of the function you want it to call whenever it is pressed and everything works fine. However, function pointers have some huge disadvantages:

So people (Qt is propably the reference here) decided to abstract the concept of a function pointer a bit, fixed these problems along the way and called their new invention "signal/slot system".

In these terms, a "signal" is basically something like a function pointer. You can do two things with a signal: you can "connect" it to a "slot" or you can "emit" it. "Connecting" a signal means that you associate this signal with a "slot" (basically a normal function). When you "emit" a signal, you automagically call all the connected slots with the given parameters. So implementing a button that calls one of your functions means to "connect" your function to the sigClick "signal" of the button. Of course, type safety issues have to be considered (i.e. only signals and slots with the same parameters and return value can be combined).

If you still struggle with the concept of signals and slots, it may be helpful to read the documentation of the libsigc++ library, as it comes with a quite good tutorial of its own.

How ParaGUI does signals

If you haven't found out already, ParaGUI uses libsigc++ for the signal code. However, some things have been customized.

The most important remark is an extension of the type equivalence. Usually, the signal and the slot have to be of the same type. If, for example, we have a look at the signal that a button emits on clicks, we find in pgbutton.h:

template<class datatype = PG_Pointer> class SignalButtonClick
                    : public PG_Signal1<PG_Button*, datatype> {};

Don't be terrified! All the information you should need from this line is that the signal when a button is clicked demands a slot with a bool return value (this is standard for all signals used within ParaGUI; however, as of version 1.1.8, I don't know any class that actually uses the return value) and that a fitting slot should take a PG_Button* and a PG_Pointer as parameter. However, in the case of paragui, it does not need to.

Each object in ParaGUI can have some defined user data, which is stored as a PG_Pointer (typically, this is just a void*). It is set using SetUserData(). This user data can then be supplied with all signals. The PG_Button* is a pointer to the button that raises the signal.

Now let us continue with the button. Further down in pgbutton.h, we find the line

SignalButtonClick<> sigClick;

This says that the signal raised when the button is clicked is called sigClick and of the type seen further above. Due to internal conversions, this signal can be connected to one of the four following functions:

bool long(PG_Button* b, PG_Pointer userdata);
bool short(PG_Button* b);
bool shortest();

So if the signal sends some user data and the button pointer, it is already satisfied with a function that e.g. takes no arguments. Equipped with this knowledge, a second signal declaration should not worry us. We find one in pgslider.h:

template<class datatype> class SignalSlide 
                : public PG_Signal2<PG_ScrollBar*, datatype> {};
...
SignalSlide<long> sigSlide;

Here, the slider emits a signal with three arguments (two plus the usual user data), namely a PG_ScrollBar, a long (the datatype in the original declaration is substituted by a long) and the user data not written explicitely here.

This signal can then be connected to one of the functions

bool func1(PG_ScollBar* s, long val, PG_Pointer userdata);
bool func2(PG_ScrollBar* s, PG_Pointer userdata);
bool func3(PG_ScrollBar* s, long val);
bool func4(PG_ScrollBar* s);
bool func5(long val);
bool func6();

Though this was a rather long theory part, I would like to make some final notes before going over to an actual implementation.

First, libsigc++ does not neccessarily guarantee a specific order of execution (maybe this has changed, and typically this should be the reverse order of connection), so if you connect a signal to multiple slots, make sure order is not of any importance.

If you get a bunch of error messages with the word "signal" occuring everywhere you have propably connected a signal to a slot of the wrong type.

Furthermore, libsigc++ also offers some advanced techniques, such as rebinding (connecting signals to functions of the "wrong" type). If this sounds interesting, I'd strongly encourage you to read the libsigc++ manual (should be installed in the documentation directory). It is quite short and very readable, by the way.

Hello World with button

Now, after all this dry theory, we want to actually do something with signals. Let us equip our hello world example from the previous section with a button. If you click on the button, its text shall be changed.

The complete sourcecode can be found here: button.cpp

example_2.jpg

Test program with button

Let us have a look at the changes.

#include <pgbutton.h>
Of course, now that we use buttons, we also have to include the appropriate header file.

bool ChangeText(PG_Button* b)
{
    b->SetText("Clicked!");
    return true;
}
Furthermore, we need a function that the button calls when clicked. It changes the text of the clicked button and uses the given argument for it.

    PG_Button* btn = new PG_Button(0, PG_Rect(50, 50, 100, 30), "Click Me!");
Now we create a button. The arguments don't need to concern us for now. They will be dealt with in the next section.

    btn->sigClick.connect(slot(ChangeText));
Here we connect the sigClick signal of the button with our function. Note the syntax. Always include the function you want to connect to in a call to slot. This function makes an appropriate slot from the given function. In the next section, an example where the slot is created from a member function of a class will be given.

    btn->Show();
Since the button is not visible by default, we need to explicitely display it.

Creating a dialog

The next logical step is to create an actual dialog. For this, we need to introduce some new concepts; namely parent widgets and the difference between local and global coordinate systems.

Parent widgets

In ParaGUI, every widget, be it a window, a label or a menu item, can have a parent. This parent is supplied at creation time and normally not changed. For this, the first argument, every ParaGUI class will take in the constructor is always a pointer to the parent widget. See for example the PG_Button:

    PG_Button(PG_Widget* parent, const PG_Rect& r = PG_Rect::null, [...]);

The concept of parent widgets has a lot of advantages. If you want to hide a dialog with lots of fancy buttons and stuff, you don't need to explicitely ask every single item to hide. Neither do you have to implement a Dialog::HideEverything function that hides every single item. If you set up the dialog properly (which means that all dialog items have the dialog widget as their parent), just calling the Hide() function of the dialog automatically hides all children as well.

Other features of the parent/child concept are:

Widgets that do not have a natural parent (such as main windows) just set their parent to NULL. Obviously, this means you have to handle them manually in every way.

Coordinates

Here, I would like to explain a bit more about coordinates. I assume that you are familiar with x/y coordinates. The second parameter to (almost?) all constructors is a PG_Rect that defines the area which the widget occupies. It's values are the x/y coordinates of its upper left edge, the width and the height. Note that if the widget extends beyond the area that the parent occupies, this part will not be displayed!

However, the interesting part here is the upper left edge. If the widget in question has no parent, the x/y coordinates will be global, i.e. relative to the upper left edge of the program's window. However, for children, these coordinates are relative to the upper left edge of their parent. This has to be kept in mind when e.g. writing dialogs.

A simple dialog

Now, to summarize this chapter, I would like to introduce a small program that imitates a simple calculator. You have two input fields where you can input numbers and a button to show the result when adding these two values. As usual, I will not spend my breath on catching errors caused by malicious users or something similar.

Further ideas:



The ParaGUI Project - Alexander Pipelka