
Ulf Lorenz (ulf82@users.sf.net)
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.
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.
#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.
The first example program
#include <pgapplication.h>
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 next line creates the ParaGUI application object. It does not need additional arguments.
app.LoadTheme("default");
if(!app.InitScreen(200, 100, 0, SDL_SWSURFACE)){ printf("Resolution not supported\n"); exit(-1); }
app.SetCaption("Hello, World!"); // Enter main loop app.Run();
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.
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.
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.
The complete sourcecode can be found here: button.cpp
Test program with button
#include <pgbutton.h>
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.
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));
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();
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.
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.
Further ideas: