LJ Archive

Qt GUI Toolkit

Eirik Eng

Issue #31, November 1996

This GUI toolkit makes porting graphics to multiple platforms a snap.

Developing applications with graphical user interfaces (GUI) takes time and can be hard work. Making these applications work across different operating systems can be even more complex. Traditionally, applications have been developed for one platform, and then large amounts of the code have been rewritten to port the application to other platforms. Multi-platform GUI toolkits have changed that procedure.

A multi-platform GUI toolkit makes it easier to port applications between platforms. Developing applications with a GUI toolkit is also considerably easier and a lot less work than using a window-system directly (e.g., X11 or Windows). The Qt toolkit is a multi-platform C++ GUI toolkit (class library) that has been developed over a 4 year period. The company Troll Tech AS was founded 2 1/2 years ago to secure future development of Qt.

As one of the Qt developers, I can give you an introduction to and overview of Qt. In the process, I'll throw in my 2 cents worth of general GUI-programming techniques.

The following sections can be found in this article:

  • The Qt Story—background information about Qt

  • Signals and Slots—Qt's object communication mechanism

  • The Qt Paint Engine—drawing graphics with Qt.

  • Qt Event Handling—how to get those user clicks and window system events in Qt

  • Double-buffering—a well known and very useful GUI programming technique

  • Making Your Own Software Components—how to code a new software building block

  • Dialog Boxes—putting it all together and making it run

  • Hints and Tips—my 2 cents worth of GUI-programming experience.

The Qt Story

The first publicly available version of Qt was released in May 1995. Version 0.98 was recently released (July 1996) and included complete source code for X11. Version 1.0 is scheduled for September 1996. Qt for X11 has a non-commercial license which grants any developer the right to use Qt to develop software for the free software community. The non-commercial version of Qt includes the full X11 source code. With this license, Troll Tech hopes to promote the development of high quality free software. Qt is an emulating GUI toolkit which allows programmers a choice between the Motif and the Windows look and feel. It implements its own widgets (user interface elements), and the X11 version of Qt is implemented directly on top of Xlib and uses neither Xt nor Motif. Practically all classes and member functions in Qt are documented. The documentation is available in HTML, postscript, text and as manual pages. The HTML version is fully cross-referenced with links to code examples. In addition, there is a tutorial for the Qt beginner. You can see the documentation on the web at http://www.troll.no/qt/.

Troll Tech has used Linux as its main development platform for over 2 years. All X11 development is done first on Linux, then the source is moved to other platforms for testing and porting. Qt currently runs under several variants of UNIX, Windows 95 and Windows NT.

Signals and Slots

Let's first look at the part of Qt that probably differs most from other GUI toolkits—the mechanism for object communication. One of the most feared and hackish aspects of GUI programming has always been the dreaded callback-function. In most toolkits, widgets have a pointer to a function for each action they trigger. Anyone who has worked with function pointers knows that this can get quite messy. Qt has approached the problem of communication between GUI objects (and other objects for that matter) in a totally new way. Qt introduces the concepts of signals and slots, that eliminate the need for pointers to functions, and provide a type-safe way to send arguments of any type. All Qt objects (classes that inherit from QObject or its descendants, e.g., QWidget) can contain any number of signals and slots. When an object changes its internal state in a way that might be interesting to the outside world, it emits a signal (not to be confused with UNIX interprocess signals), and then goes on happily minding its own business, never knowing or caring if anybody receives the signal. This important feature allows the object to be used as a true software component. Slots are member functions that can be connected to signals. A slot does not know or care if it has a signal connected to it. Again, the object is isolated from the rest of the world, and can be used as a true software component. These two simple concepts make up a powerful component programming system. They may seem awkward when encountered for the first time, but they are a lot more intuitive and easier to both learn and use than the alternatives. Let's look at how signals and slots are specified in a class declaration. The following class is a stripped down version of the class shown in code Listing 3:

class PixmapRotator : public QWidget
{
    Q_OBJECT
public:
    PixmapRotator( QWidget *parent=0,
        const char *name=0 );
public slots:
    void  setAngle( int degrees );
signals:
    void  angleChanged( int );
private:
    int         ang;
};

Signals and slots are specified syntactically using C++ categories in the class declaration. This class defined above has a slot called setAngle. Slots are normal member functions and must have an access specifier. They are, as with other member functions, implemented by the programmer, and can be overloaded or virtual.

The PixmapRotator class has a single signal, angleChanged, which it emits when its angle has changed value. Signals are declared in the class declaration by the programmer but the implementation is generated automatically. To emit a signal, type:

emit signal( arguments )

The implementation of the slot looks like this:

void PixmapRotator::setAngle( int degrees )
{
// keep in range <-360, 360>
    degrees = degrees % 360;
// actual state change?
    if ( ang == degrees )
        return;
    ang   = degrees;            // a new angle
    emit angleChanged( ang );   // tell world
        ...
}

Note that setAngle only emits the signal, if the value actually changed (as the name of the signal implies). A signal should only be emitted when a state change has occurred.

To connect a signal to a slot the QObject static member function connect is used, for example:

connect( scrollBar, SIGNAL(valueChanged(int)),
                 rotator,   SLOT(setAngle(int)) );

Here the QScrollBar scrollBar's signal valueChanged is connected to the PixmapRotator rotator's slot setAngle. This statement assures that whenever the scrollbar changes its value (e.g., if the user clicks on one of its arrows) the angle of the PixmapRotator object will change accordingly. The two objects can interact without knowing about each other as long as a connection is set up by a third party.

As you can see, signals and slots can have arguments. The last argument(s) from a signal can be discarded, but otherwise the arguments must match for a connection to be made.

An arbitrary number of slots can be connected to a single signal and vice versa.

Technically, signals and slots are implemented using the Qt meta object compiler (moc). It parses C++ header files and generates C++ code necessary for Qt to handle signals and slots. The signals, slots and emit keywords are macros, so the compiler preprocessor changes or removes them.

Signals and slots are efficient. Of course they are not as fast as a direct function pointer call, but the difference is small. A signal triggering a slot has been measured to approximately 50 microseconds on a SPARC2.

The Qt Paint Engine.

Qt contains a device independent drawing engine, implemented in the class QPainter. QPainter is highly optimized and contains several caching mechanisms to speed up drawing. Under X11, it caches GCs (graphics contexts), which often make it faster than native X11 programs. QPainter contains all the functionality one would expect from a professional 2D graphics library.

The coordinate system of a QPainter can be transformed using the standard 2D transformations (translate, scale, rotate and shear). These transformations can be done directly or via a transformation matrix (QWMatrix), exactly as in postscript. Here is a small example taken from www.troll.no/qt showing the use of coordinate transformations:

void LJWidget::drawLJWheel( int x, int y, QPainter *p )
{
// set center point to 0,0
        p->translate( x, y );

// 24 point bold Times
        p->setFont(QFont("Times", 24, QFont::Bold));

// save graphics state
        p->save();

// full circle
        for( int i = 0 ; i < 360/15 ; i++ ) {

// rotate 15 degrees more
        p->rotate( 15 );

// draw rotated text
        p->drawText( 0, 0, "Linux" );
    }
    p->restore();       // restore graphics state
    p->setPen( green ); // green 1 pixel width pen
// draw unrotated text
    p->drawText( 0, 0, "Linux Journal" );
}

This member function draws a text “wheel” with the center given at a specified point. First the coordinate system is transformed so that the given point becomes the point (0,0) in the new coordinate system. Next a font is set, and the graphics state is saved. Then the coordinate system is rotated 15 degrees at a time, clockwise, and the text “Linux” is drawn to form a textual “wheel”. The graphics state is then restored , the pen set to a green pen and the text “Linux Journal” is displayed. Note that it is not strictly necessary to save the graphics state since we do a full 360 degree rotation. Saving the graphics state is strictly defensive programming—if we were to change the for loop, which is doing the rotation, we could still guarantee that the last text would be output horizontally.

Qt has a font-abstraction implemented in the QFont class. A font can be specified in terms of the font family, point size and several font attributes. If the specified font is not available, Qt uses the closest matching font.

The drawLJWheel function can be used to generate output on any device since it merely uses a pointer to a QPainter. It does not know what kind of device the painter is operating on. The function is put into a widget in code, see http://www.troll.no/qt. Running it produces the result shown in Figure 1.

Figure 1. LJ Widget Output

Support Classes

Qt also contains a set of general purpose classes and a number of collection-classes to ease the development of multi-platform applications. The hardest part of generating portable code has always been operating system dependent functions. Qt has platform independent support for these functions, such as time/date, files/directories and TCP/IP sockets. Sometimes it might be necessary to use the underlying window system resources directly, e.g., when interfacing with other libraries. Qt gives direct access to all low-level window IDs and other resource IDs. Troll Tech has used this access to write a small widget that makes it possible to use OpenGL/mesa within a Qt widget.

Qt Event Handling

The structure of any GUI program is based on events. This basis is the main difference between GUI programming and non-GUI programming. A GUI program does not have “control” over the application; it merely waits for an event, does something as a response, and then waits for the next one.

A program typically sets up a top level widget to call the main event loop, which then dispatches events as they are received from the user or other parts of the system.

This model can be elegantly applied in an object-oriented language by using subclasses and reimplementation of virtual functions in the classical C++ event mechanism that is also used by Qt. The QWidget class contains one virtual function for each event type. A new type of widget is made by subclassing QWidget (or one of its descendants). You can simply reimplement an event function for each type of event you wish to receive. The event functions together with the Qt paint engine make up a powerful toolbox for creating custom widgets.

By far the most important event a widget receives is the paint event. It is called by the main event loop whenever the widget needs to draw a part of itself. Below is an example of a simple paint event, taken from code, http://www.troll.no/qt:

void CustomWidget::paintEvent( QPaintEvent *e )
{
// necessary to draw?
    if ( rect.intersects( e->rect() )) {
        QPainter p;
        p.begin( this );         // paint this widget
        p.setBrush( color );     // fill color
        p.drawRect( rect );      // draw rectangle
        p.end();
    }
}

CustomWidget contains the member variable rect with type QRect, containing a rectangle with a one pixel black outline and filled with a color. Another member variable is color with type QColor, containing the color used to fill the rectangle.

First we check if the rectangle intersects the part of the widget that is to be updated. If it does, we instantiate a QPainter, open it on the widget, set its brush to the correct color and draw the rectangle (the default pen is one line thick and black).

All event functions take a pointer to an event object as their single argument. QPaintEvent contains the rectangular area of the widget that must be redrawn.

In the same widget we also receive resize events like this:

void CustomWidget::resizeEvent( QResizeEvent * )
{
// widget size - 20 pixel border
    rect = QRect( 20, 20, width() - 40,
        height() - 40 );
}

This event function sets the rectangle to be the size of the widget minus a 20 pixel border on all sides. It is never necessary to repaint a widget in a resize event since Qt always sends a paint event after the resize event when a widget has been resized.

CustomWidget also receives mouse press, move and release events like this:

void CustomWidget::mousePressEvent( QMouseEvent *e )
{
// left button click
    if ( e->button() == LeftButton &&
// on rectangle?
         rect.contains( e->pos() ) ) {
// set rectangle color to red
         color   = red;
// remember that it was clicked
         clicked = TRUE;
// repaint without erase
         repaint( FALSE );
    }
}
void CustomWidget::mouseMoveEvent(QMouseEvent *)
{
// clicked and first time?
    if ( clicked && color != yellow ) {
        color = yellow;    // set color to yellow
        repaint( FALSE );  // repaint without erase
    }
}
void CustomWidget::mouseReleaseEvent(QMouseEvent *e)
{
    if ( clicked ) {       // need to reset color
        color   = green;   // set color to green
        repaint( FALSE );  // repaint without erase
        clicked = FALSE;
    }
}

The mouse press event sets the rectangle color to red if the left mouse button is clicked inside the rectangle. When the mouse is moved after a click on the rectangle the color will change to yellow. Finally the color is reset to green when the mouse button is released.

The calls to repaint cause the entire widget to be redrawn. The FALSE argument instructs Qt not to erase the widget (fill it with the background color) before sending the paint event. We can use FALSE, because we know that paintEvent will draw a new rectangle covering the old one. Painting in this manner reduces flickering considerably; otherwise, double-buffering should be used.

The full widget code can be found in www.troll.no/qt . Running it produces the result shown in Figure 2. Custom Widget Output

Double-buffering

Flickering is a common problem in graphics programming. Some GUI programs do updating by clearing the area of a widget and then draw the different graphics elements. This process normally takes enough time for the eye to notice the clearing and drawing process. The widget flickers, the program looks unprofessional and fatigues the eyes of the users.

A technique called double-buffering can be used to solve this problem. A pixmap (i.e., pixel map—an off-screen memory segment used as if it were a part of the screen raster buffer) is used, and all drawing is done off-screen on this pixmap. The pixmap is then transferred to the screen in one lightning-fast operation. This pixmap transfer is normally so fast that on most systems it appears instantaneous to the human eye.

Sometimes a pixmap the size of the widget to update is used, in other cases, only certain parts of the widget are double-buffered. Which method will be most effective must be considered in each case.

Pixmaps often contain large amounts of data and are often slow to create and handle (in CPU-time). A good technique is to store the buffer pixmap as a part of the widget. When the widget needs to update itself (in Qt, whenever it receives a paint event), it simply copies the required part of the buffer pixmap to the part of the widget that must be repainted.

Often, it is useful to include a dirty flag as a part of the widget. All state changes (i.e., changes to member variables) that affect the visual appearance of the widget can then simply set this flag to TRUE telling the widget to repaint itself. The paint event function then checks the dirty flag, and updates the buffer pixmap before it updates the screen. This ensures that all widget painting code is in one place, making the widget easier to maintain and debug.

I've found this technique to be very useful and powerful and have used it on a variety of GUI-systems, as well as Qt.

Making Your Own Software Components

OK, now that we've looked at different parts of Qt, let's use it to build a custom-made software component that can display an image and rotate it by an angle. This widget should contain slots with instructions to let the user choose a file on disk, and it should have the ability to print the rotated image on a printer.

As a start we decide to give it the following signals and slots:

public slots:
    void  setAngle( int degrees );
    void  load();
    void  print();
signals:
    void  angleChanged( int );
    void  filePathChanged( const char * );

We can now set the rotation angle setAngle, let the user choose a new image file load, and print the image print. We choose to implement the functionality we need for the first version first. Later this component can be expanded to include slots like setPixmap(QPixmap) or setFilePath(QString).

The two signals tell the world about a change in the rotation angle (angleChanged) or the image file being displayed (filePathChanged).

Next, we include two member functions to fetch the angle and file path:

public:
    int   angle()          const { return ang;  }
    const char *filePath() const { return name; }

And we include the following member variables:

private:
    int         ang;
    QString     name;
    QPixmap     pix;
    QPrinter    printer;
    QFileDialog fileDlg;
    QPixmap     bufferPix;
    bool        dirty;

By setting these variables, we store an angle, file name, pixmap, printer and file selection dialog in the component. We want the widget to update itself smoothly and have decided to use the double-buffering technique, so we store a buffer pixmap. In addition, we have a dirty flag which is set when the widget needs to update the buffer pixmap. The combination of a buffer pixmap and a dirty flag is a very useful and powerful GUI technique that can be used in a large range of widgets.

Also, the widget has a paint event function and a resize event function, see code Listing 3 for the full class declaration.

Since we want to be able to paint to both the screen and a printer, we put the drawing code in a private member function that operates on a QPainter:

void PixmapRotator::paintRotatedPixmap(QPainter *p)
{
// need device width and height
    QPaintDeviceMetrics m( p->device() );
// center point
    p-$gt;translate( (m.width())/2,
                (m.height()) / 2 );
    p->rotate( ang );
    p->drawPixmap( - (pix.width())/2,
                - (pix.height())/2, pix );
}

First we fetch the metrics of the device the painter is operating on. We use the width and height of the device to put the center (0,0) of our coordinate system in the middle of the device. Next we rotate the coordinate system by the wanted angle and draw the pixmap with its center point at (0,0). In other words, the center point of the pixmap is put at the center point of the device.

The paint event function looks like this:

void PixmapRotator::paintEvent( QPaintEvent *e )
{
    if ( dirty ) {      // buffer needs update?
// same size as widget
        bufferPix.resize( size() );
// clear pixmap
        bufferPix.fill( backgroundColor() );
        QPainter p;
// paint on buffer pixmap
        p.begin( &bufferPix );
        paintRotatedPixmap( &p );
        p.end();
        dirty = FALSE;  // buffer now new and clean
    }                   // update exposed region:
    bitBlt( this, e->rect().topLeft(), &bufferPix,
                 e->rect() );
}

If the widget is “dirty”, we need to update the buffer pixmap. We set its size to the size of the widget, clear it and call our local painting function. Don't forget to reset the dirty flag when a buffer pixmap has been updated.

Finally, we use the QPaintEvent pointer to find out which part of the widget must be updated and call bitBlt. bitBlt is a global function that can transfer data from one paint device to another as fast as possible. bitBlt is common GUI shorthand for “bit block transfer”.

With double-buffering and a dirty flag, the resize event function becomes trivial:

void PixmapRotator::resizeEvent( QResizeEvent *e )
{
    dirty = TRUE;               // need to redraw
}

Again, it is never necessary to repaint a widget in a resize event, since Qt automatically sends a paint event after the resize event.

With a common drawing function doing printing is also easy:

void PixmapRotator::print()
{
// opens printer dialog
    if ( printer.setup(this) ) {
        QPainter p;
        p.begin( &printer );    // paint on printer
        paintRotatedPixmap( &p );
        p.end();                // send job to printer
    }
}

First we let the user setup the printer, then we open a painter on that printer, and finally, call the drawing function.

Loading a new image takes a bit more code:

void PixmapRotator::load()
{
    QString newFile;
    QPixmap tmpPix;
    while ( TRUE ) {
// open file dialog
        if ( fileDlg.exec() != QDialog::Accepted )
            return;     // the user clicked cancel
// get the file path
        newFile = fileDlg.selectedFile();
// is it an image?
        if ( tmpPix.load( newFile ) )
            break;      // yes, break the loop
        QString s;      // build a message string
        s.sprintf("Could not load \"%s\"",
                newFile.data() );
// sorry!
        QMessageBox::message( "Error", s );
    }
    pix   = tmpPix;     // keep the pixmap
    name  = newFile;    // new file name
    emit filePathChanged( name );  // tell world
    dirty = TRUE;       // need to redraw
    repaint( FALSE );   // paint the whole widget
}

We set up a loop that opens the file dialog box. If the user has selected a file that cannot be loaded, we tell her and let her try again. If a valid file has been selected, we copy the new pixmap and the file path. Finally, we emit a signal to tell the world, mark the widget as dirty and repaint all of it (the FALSE argument means that Qt should not clear the widget before sending the paint event).

We have now made a new software component which can be connected to others through the signal/slot mechanism. See www.troll.no/qt for the full code of the PixmapRotator widget.

Dialog Boxes

Finally, let's use our component to put together a dialog box. Most GUI applications contain these boxes. Dialogs are windows with a number of widgets as children. A typical example of a dialog is an input form for a database with a text field for each database field.

Qt contains the standard widgets needed to build dialogs for most purposes. This custom built dialog box class has been taken from www.troll.no/qt

class RotateDialog : public QDialog
{
    Q_OBJECT
public:
    RotateDialog( QWidget *parent=0,
                const char *name=0 );
    void resizeEvent( QResizeEvent * );
private slots:
    void updateCaption();
private:
    QPushButton   *quit;
    QPushButton   *load;
    QPushButton   *print;
    QScrollBar    *scrollBar;
    QFrame        *frame;
    PixmapRotator *rotator;
};

The RotateDialog class inherits from QDialog and contains 3 pushbuttons, a scrollbar, a frame and the custom made pixmap rotator. The dialog only has three member functions. The constructor initializes the different widgets in the dialog, resizeEvent sets the position and size of the widgets, and the slot updates the caption text of the dialog.

Let's take a look at the constructor:

RotateDialog::RotateDialog( QWidget *parent, const char *name )
        : QDialog( parent, name )
{
    frame = new QFrame( this, "frame" );
    frame->setFrameStyle( QFrame::WinPanel |
                          QFrame::Sunken );
    rotator = new PixmapRotator("this, rotator");
    rotator->raise();  // put it in front of frame
    quit = new QPushButton("Quit", this, "quit");
    quit->setFont(QFont("Times", 14,
                QFont::Bold));
    load = new QPushButton("Load", this, "load");
    load->setFont( quit->font() );
    print = new QPushButton("Print", this,
                        "print");
    print->setFont( quit->font() );
    scrollBar = new QScrollBar(QScrollBar::Horizontal,
                        this, "scrollBar" );
    scrollBar->setRange( -180, 180 );
    scrollBar->setSteps( 1, 10 );
    scrollBar->setValue( 0 );
    connect( quit, SIGNAL(clicked()), qApp,
                SLOT(quit()) );
    connect( load, SIGNAL(clicked()), rotator,
                SLOT(load()) );
    connect( print, SIGNAL(clicked()), rotator,
                SLOT(print()) );
    connect( scrollBar, SIGNAL(valueChanged(int)),
             rotator  , SLOT(setAngle(int)) );
    connect( rotator, SIGNAL(angleChanged(int)),
                SLOT(updateCaption()) );
    connect( rotator,
                SIGNAL(filePathChanged(const char *)),,
                SLOT(updateCaption()) );
    setMinimumSize( 200, 200 );
}

The different widgets have now been instantiated and initialized. We also want to put a frame around the pixmap rotator, so it is raised (popped to the front of the window stack) in order to make sure it is in front of the frame.

The scroll bar is set up to represent a value in the range [-180,180] with line and page steps set to 1 and 10 respectively. A line step is used when the user clicks on a scroll bar arrow, page step when the user clicks between the arrows and the scroll bar slider.

Then the different widgets are connected. The quit pushbutton is connected to the applications quit slot (qApp is a pointer to Qt's application object). The load and print pushbuttons are connected to their respective slots in the pixmap rotator, and the scrollbar is connected to the pixmap rotators angle value.

Next we connect the rotator's signals to the private slot updateCaption. We are here using a connect function that only takes three arguments, where the this pointer is implicit as the receiver. Note that the slot we connect to has fewer arguments than the signal. All or some of the last arguments of a signal can always be discarded in this way, if we are not interested in receiving them in a slot.

Note how we use the standard widgets to control our custom made widget. PixmapRotator is a new software component which can be plugged into many different standard interface controls. We could easily have added a menu in addition by plugging it into the rotator's slots. Keyboard accelerators or a text field, used to enter a numerical value for the angle, could likewise have been added without a single change to PixmapRotator.

Finally, we tell Qt that this widget should never be allowed to have a size smaller than 200x200 pixels. Note that the class does not have a destructor. Child widgets are always deleted by Qt when the parent is deleted.

There is no interactive dialog builder for Qt at the time of this writing (July 1996), but there is one in the works that will probably be ready by the time you read this article. Check Troll Tech's home page for details (http://www.troll.no/). The resize event is implemented as follows:

const int border     = 10;
const int spacing    = 10;
const int buttonH    = 25;
const int buttonW    = 50;
const int scrollBarH = 15;
void RotateDialog::resizeEvent( QResizeEvent * )
{
    quit->setGeometry(border, border, buttonW,
                        buttonH );
    load->setGeometry((width() - buttonW)/2,
                        border, buttonW, buttonH );
    print->setGeometry(width() - buttonW - border,
                        border, buttonW, buttonH );
    scrollBar->setGeometry( border,
                quit->y() + quit->height() + spacing,
                width() - border*2, scrollBarH );
    int frameTop = scrollBar->y() +
                scrollBar->height() + spacing;
                frame->setGeometry( border, frameTop,
                width() - border*2,
                height() - frameTop - border );
    rotator->setGeometry( frame->x() + 2,
                frame->y() + 2, frame->width() - 4,
                frame->height() - 4 );
}

Each widget is moved and resized according to the dialogs width and height. The three buttons are placed 10 pixels from the top of the dialog, one on each side and one in the middle. The scrollbar is placed right beneath them, followed by the frame. The rotator is put inside of the frame.

Writing code like the above is not difficult, but it's not always easy to read. A geometry manager solves resizing of dialogs in a more elegant way. Qt's geometry manager is currently under internal testing at Troll Tech and will soon be added to the toolkit. The slot is implemented like this:

void RotateDialog::updateCaption()
{
    QString s;
// we do not want the full path
    QFileInfo fi( rotator->filePath() );
    s = fi.fileName();          // only the filename
    if ( rotator->angle() != 0 ) {   // rotated?
        s += " rotated ";
        QString num;
// convert number to string
        num.setNum( rotator->angle() );
        s += num;
        s += "degrees";
    }
    setCaption( s );    // set dialog caption
}

We build a message string by using the image file name and its rotation angle. The string is then used as the dialog's caption.

See www.troll.no/qt for the full code. When run, the user can press on the load button and Qt's standard file dialog will pop up. If the print button is clicked, the standard print dialog pops up letting the user choose if the output should go to a file or to a printer. Under X, Qt generates postscript printer output; under Windows, the Windows printer driver system is used.

In Figure 3 you can see a screen shot of the RotateDialog together with the print dialog.

Hints and Tips

If you are new to GUI programming, you might find that your first widget comes up blank on the screen or not at all. There are two very common reasons for this to happen.

First of all, a widget's window on the screen can be overwritten at any time by the window system. The window system does not store the contents for you, it just calls the widget's paint event function when the widget needs to be refreshed. Thus, if you draw on the widget but the paint event function doesn't reproduce the drawing exactly, you might not see any effect on the widget. Generally, the best method is to do all your drawing in the paint event function, and just instruct the widget to do a repaint whenever its state changes.

Secondly, when you create a widget, it is not visible. In Qt, you have to call the show member function to make widgets visible; other toolkits have a similar function.

Finally, I would like to mention a few points I consider important when designing and implementing GUI programs:

  • Keep it simple. When you build new widgets and dialogs try to keep their interfaces as small and elegant as possible. Don't add a lot of functions that might be nice to have in the future.

  • Don't crowd the screen. Try to keep your dialogs as intuitive and minimalistic as possible. A dialog should be both functional and pleasing to the eye.

  • Use double-buffering. At least when all or parts of a widget will change significantly over time. Programs that don't flicker look a lot more professional than the ones that do.

  • Cache like crazy. If there are parts of your program that do time-consuming operations, e.g., generation of pixmaps, save the result so that you don't have to do the same operations over and over.

  • Keep your member variables private—this is good object-oriented practice. In GUI programming it is even more important. A change in a variable often means that you have to update the screen. If you set variables via member functions, you can guarantee that the screen is always up-to-date.

  • Put your drawing code in one place—preferably in a single member function. Bugs in the relationship between the values in your member variables and what the widget displays are then located in one place. (You will be glad you did.)

  • Use standard types as arguments to signals and slots. If your signal contains an int, it can be connected to a large number of slots. If it contains an argument of the type MyNumber, the widget or dialog will not be as useful as a component.

Where to Find Qt

The Qt source and binary versions for several platforms (including Linux of course) can be downloaded from the net at ftp://ftp.troll.no/qt/.

Eirik Eng (iriken@troll.no) is a co-founder of Troll Tech AS, and works there as a developer. He has a siv.ing. (M.S.) degree from the Norwegian Institute of Technology and has worked with GUIs and OOP since 1991. His main hobby is office gardening (perfect for people who spend a lot of time at the office). This year he is especially proud of his 4 foot high (and still growing) eggplants and his tamarind tree.

LJ Archive