Programming with Qt, Part 2

In the previous installment of this series, we implemented two very simple example programs, which nevertheless demonstrated quite a few of the core concepts of Qt programming. This month, let's will take a step back and look at some of the fundamentals of programming with Qt.

In the previous installment of this series, we implemented two very simple example programs, which nevertheless demonstrated quite a few of the core concepts of Qt programming. This month, let’s will take a step back and look at some of the fundamentals of programming with Qt.

The class diagram in Figure One shows the static class structure of some of the more important Qt classes. Most classes ultimately derive from Qt. Qt doesn’t declare any member variables or functions, and contains only a number of public enums (such as DateFormat { TextDate, ISODate, LocalDate }); and since it doesn’t declare any member variables or functions, it merely provides a convenient grouping of the common enums without polluting the global namespace.

Figure One: Part of the Qt class hierarchy

The most important direct descendant of Qt is the QObject class. This common base class provides the necessary infrastructure for Qt’s language extensions. By inheriting from QObject it’s possible for custom objects to integrate seamlessly with Qt’s object model. We’ll investigate how that’s achieved below.

Three important direct subclasses of QObject are QApplication, QWidget, and QCanvas.

Each program that makes use of Qt’s GUI elements must first instantiate exactly one QApplication object, which handles initialization of the underlying windowing system. Instantiation of QApplication also provides the main event loop and some signals and slots related to the entire application’s lifecycle (such as the quit() slot).

The QWidget class is the common base class of most of Qt’s GUI elements. Besides providing generic functions for basic geometry and look-and-feel management, QWidget handles mouse, keyboard, and other events generated by the user interface.

(In Qt, every widget is rectangular and knows how to paint itself to the screen. Each widget is clipped by its parent and by widgets covering it. A widget without a parent is always a top-level widget, and is displayed in its own window.)

QWidget also inherits from QPaintDevice, an object that can be drawn on using Qt’s general-purpose drawing tool QPainter. Other subclasses of QPaintDevice are QPixmap (for off-screen buffers), QPrinter (for “painting” to a hardcopy), and QPicture (for storing and replaying a sequence of QPainter commands).

To draw onto a QPaintDevice, a QPicture is passed to a QPainter (typically in the QPainter‘s constructor), and can then be drawn upon using QPainter‘s drawLine(), drawRect(), or even drawCubicBezier() methods. Line and fill colors are set using QPainter‘s setPen() and setBrush() methods, respectively.

There is also a bitblt() function (a standalone function, not a member function) that copies pixels from one QPaintDevice to another. bitblt() is typically used to copy the contents of some backing store from memory to the screen. To build up a complicated graphic, for instance, one would perform all the graphics commands on a QPixmap, and bitblt() the results to a visible widget once the drawing is complete.

It’s important to remember that QWidget is not an abstract class and can therefore be instantiated directly, for instance to create simple dialogs. However, for most applications, you can find more suitable subclasses of QWidget to create user interfaces. For example, the subclasses of QFrame — itself a direct descendant of QWidget — provide smart, yet lightweight ways to arrange other GUI elements.

For graphics such as animations, you can use an alternative approach based on QCanvas. QCanvas doesn’t subclass QPaintDevice, and therefore can’t be drawn on using QPainter. Instead, objects of classes extending QCanvasItem are instantiated and can be placed and removed on a QCanvas. Compared to subclasses of QWidget, the subclasses of QCanvasItem are very lightweight: they don’t define signals and slots, for instance, and therefore cannot respond to user events directly. They are pure graphics elements, and not part of the active user interface.

So, to generate animated graphics, it’s possible to give a QCanvasItem a velocity, such that they move “automatically” across a QCanvas whenever QCanvas‘s advance() slot is activated. However, since QCanvas is not a widget, it cannot be rendered to the screen directly. Instead, use the QCanvasView widget (an indirect subclass of QFrame) to display a QCanvas object.

A Peek Under the Covers

Qt provides a set of language extensions that provide a degree of flexibility at run-time that’s unusual for a statically typed language like C++. Because C++ has no external run-time environment (as in Java), the additional information required to make these features work has to be added during the compilation cycle. Qt uses pre-processor macros and its own code-generator, moc, together with more conventional object-oriented techniques and patterns to implement its language extensions.

Let’s peek under the covers a little bit and try to unravel how all of these things play together. Here, let’s focus on signals and slots — properties and RTTI are implemented in a similar fashion.

The run-time language extensions fall into four groups:

1. Intra-process communication based on signals and slots. This was introduced in Part 1 of this series, and is discussed further below.

2. Run-time type information (RTTI). Information such as the name of the class and data about superclasses inherited by the current object can be obtained at run-time using the className() and inherits() member functions (the latter takes a string containing a class name as argument and returns a boolean value).

3. Limited object life-cycle management and garbage collection. Subclasses of QObject can arrange themselves in parent/child trees, employing the composite pattern. By passing a pointer to the parent object to the child’s constructor, the newly created child is adopted by the parent. As mentioned last month, a parent object deletes all of its children in its own destructor. Of course, Passing a NULL-pointer to the child’s constructor creates a child with no parent. If the object is a widget, it becomes a top-level window. The list of an object’s children is available through the children() member function, and can be manipulated using insertChild() and removeChild(). One caveat: once created, an object cannot change its parent.

4. Reflection. It’s possible to call getter/setter methods for properties defined in subclasses of QObject knowing only the name of the property and without having to downcast from QObject to the actual subclass, as in the following example:

SomeClass *c = new SomeClass(parent);
QObject *p = c;

p->setProperty(“a”, a);

This mechanism requires the use of the Q_PROPERTY macro in the declaration of the subclass to map the property name to the actual accessor methods. A list of all defined property names is available through the class’ QMetaObject. The type of the property must be supported by QVariant, an opaque data type that accepts most primitive C++ data types, as well as those Qt classes, which are most likely to be used as properties (such as QColor and QSize).

To take advantage of any of these facilities, a class must extend QObject and contain the pre-processor macro Q_OBJECT somewhere in the private part of its declaration. The definitions for this and related macros can be found in the qobjectdefs.h header file.

The Q_OBJECT macro adds a number of function declarations and one private static data member of type QMetaObject. Most of the additional information required for the signal/slot mechanism, as well as the other language extensions, is encapsulated in this class. Since the data member is declared static, there’s exactly one instance of QMetaObject per class. The QMetaObject provides member functions such as className() and superClassName(), but its most interesting functions are probably signalNames() and slotNames(). How can the meta-object resolve this information at run-time? Enter the code-generator moc. Since moc is run after the code is written, but before it’s compiled or even pre-compiled, moc can preserve information contained in the source code that’s thrown away by the compiler. It generates code for the additional functions declared by the Q_OBJECT macro, as well as for the functions implementing signals.

For each function declared as a slot, moc generates code that initializes the meta-object with the real name of the function implementing the slot, as well as the number and types of its arguments. This information is maintained in an array inside QMetaObject — in essence, QMetaObject maintains its own vtable! Qt defines a number of auxiliary classes (which are not part of the public and documented API) in the private subdirectory of $QTDIR/include to manage this data. The meta-object’s accessor method metaObject() forwards to a moc-generated function that contains code for all the initializer calls mentioned above, and by using a lazy initialization idiom, guarantees that the meta-object is properly set up before it is being used.

Arguments are passed among signals and slots using a class mimicking an old-style union: the class has one data member for each built-in C++ data type. Arguments of other types (such as objects) are passed around as void pointers, which are suitably cast before being used. Since the moc generates code, it can generate code for a cast — something profoundly impossible to do using plain C/C++.

Finally, control is dispatched to slots through another moc-generated function. Here, ordinary function calls to all programmer-declared slots have been generated and the suitable one is chosen using a large conditional statement based on a name lookup performed in QObject‘s connect() function. Functions implementing the slots are called directly, not through function pointers — the code for the function call is right there in the generated code!

The information maintained by Qt for signals is very similar to what’s maintained for slots. In addition, there are the actual implementations of the signal functions, which provide proper wrapping of the signals’ arguments (if any) and forward control to the publish/subscribe mechanism, which maintains the information about which slots are subscribed to the currently active signal.

Several things may occur to you:

First of all, signals are not asynchronous, although they give the illusion that they are. If a signal is connected to a slot, and the slot never returns, the application blocks forever. Control never passes back to the event-loop, rendering the user interface dead.

If a single signal is connected to multiple slots, the slots are executed one after another, not in parallel. The order of processing processed is fixed by Qt’s dispatching mechanism, but is otherwise undefined.

Finally, since the connection between signals and slots is made at run-time (and can in principle also be disconnected and re-connected), the linker does not check that signals and slots passed to connect() exist! Instead, if the attempt to make the connection fails at run-time, Qt’s message handler prints an appropriate message to the console — something sloppily written Qt applications frequently do.

To suppress this behavior, one can install a custom message handler using the qInstallMsgHandler(QtMsg Handler) function. The type of the argument to this function is a typedef for a function pointer to a function with the following signature:

void handler( QtMsgType, const char * )

where QtMsgType is a global enum that can take on the values { QtDebugMsg, QtWarningMsg, QtFatalMsg }. Consequentially, defining a handler() function with an empty body and registering it using the following call…

qInstallMsgHandler( handler );

…prevents warning and error messages from being printed to the console.

Basic Tools: moc and qmake

moc is fairly simple to use. Its most important command line option is -o, which lets you specify the name of the output file. Other options tell moc whether to generate a file that can be #included into the source code, or one that can be linked, etc.

However, as in any compile-and-build system, the code generated at various stages of the compile cycle can get out of sync. To be on the safe side, it’s a good idea to run moc every time the application is recompiled. Since moc is really fast, this method doesn’t pose any hardship. However, any multi-step build process calls for automation, using that old stalwart: make.

To help quell make’s temperament (did you remember your tabs?), Qt ships with a Makefile generator called qmake. qmake reads a “project” file that must have the extension .pro. The project file assigns values to variables, which are later read by qmake to generate the actual Makefile. The most important variables are SOURCES and HEADERS, which contain the names of the source and header files making up the current project. Values can be assigned to variables:

SOURCES = file1.cpp file2.cpp

or added to the current content:

SOURCES = file1.cpp

SOURCES += file2.cpp

There are additional variables, such as CONFIG for compiler and linker flags, and TARGET for the name of the executable. Project files can also contain conditional statements to target different platforms. There’s even a -= operator that removes a value from the variable without clobbering the rest of the variable’s content.

qmake reads platform-specific information (such as the name of the compiler) from configuration files below $QTDIR/ mkspecs, and generates a Makefile that’s appropriate.

The resulting Makefile contains the compilation steps for moc, as well as the options necessary to link against the Qt libraries.

Finally, qmake can entirely bootstrap a simple project. If invoked with the -project option, qmake looks in the current directory for C++ source and header files and generates a project file with the same base name as the current directory. If that project file is then used as input to qmake, the resulting Makefile will compile and link all appropriate files and finally create an executable with the same name as the current directory!

All About Relationships

This middle installment of a three part series on Qt programming surveyed some of Qt’s most important classes and their inheritance relationships, and explained how Qt’s language extensions are realized. It also briefly introduced Qt’s build tools, moc and qmake.

In the next and last installment, we’ll take a look at some of the other development tools that ship as part of the Qt package.

Philipp K. Janert, Ph.D. is a server programmer and project manager. He maintains the beyondCode.org website and his writings have appeared on the O’Reilly Network, in IBM developerWorks, and in IEEE Software. Contact him at janert@ ieee.org.

Comments are closed.