2. The Qt Object Model

Qt is based around the Qt object model. This architecture is what makes Qt powerful and easy to use. It is all based around the QObject class and the moc tool.

By deriving classes from the QObject a number of benefits are inherited. They are listed below:

Each of these features are discussed below. Before continuing it is important to remember that Qt is standard C++ with some macros, just as any other C/C++ application. There is nothing odd or non-standard with it, which is pretty much proven by the fact that the code is very portable.

Easy Memory Management

When creating an instance of a class derived from QObject it is possible to pass a pointer to a parent object to the constructor. This is the basis of the simplified memory management. When a parent is deleted all its children are deleted too. This means that a class derived from QObject can create instances of QObject-children passing this as parent without worrying about their destruction.

For better understanding, here follows and example. Example 2-1 shows our example class. It is derived from QObject and reports everything that happens to it to the console. This will make it obvious what takes place when.

// A verbose object tells us what it is doing all the time
class VerboseObject : public QObject
{
public:
  VerboseObject( QObject *parent=0, char *name=0 ) : QObject( parent, name )
  {
    std::cout << "Created: " << QObject::name() << std::endl;
  }
  
  ~VerboseObject()
  {
    std::cout << "Deleted: " << name() << std::endl;
  }
  
  void doStuff()
  {
    std::cout << "Do stuff: " << name() << std::endl;
  }
};

Example 2-1

To make something useful with the class, a main routine is required. It is shown in example 2-2. Notice that the first thing that happens is that a QApplication instance is created. This is required for nearly all Qt applications and is good practice to avoid unnecessary problems. The code creates a memory hierarchy shown in figure 2-1.

int main( int argc, char **argv )
{
  // Create an application
  QApplication a( argc, argv );
  
  // Create instances
  VerboseObject top( 0, "top" );
  VerboseObject *x = new VerboseObject( &top, "x" );
  VerboseObject *y = new VerboseObject( &top, "y" );
  VerboseObject *z = new VerboseObject( x, "z" );
  
  // Do stuff to stop the optimizer
  top.doStuff();
  x->doStuff();
  y->doStuff();
  z->doStuff();
  
  return 0;
}

Example 2-2

Make sure to understand how the tree is translated into the code from example 2-2 and vice versa.

The memory hierarcy

Figure 2-1 The memory hierarcy

Example 2-3 shows an example run of the example code. As can be seen all objects are deleted, parents first and then chilren. Notice that each branch is deleted completely before the next is started upon. This can be seen as z is deleted before y.

$ ./mem
Created: top
Created: x
Created: y
Created: z
Do stuff: top
Do stuff: x
Do stuff: y
Do stuff: z
Deleted: top
Deleted: x
Deleted: z
Deleted: y
$

Example 2-3

If the parent reference is removed from x and y as shown in example 2-4, a memory leak occurs. This is as a test run documented in example 2-5.

  ...
  VerboseObject *x = new VerboseObject( 0, "x" );
  VerboseObject *y = new VerboseObject( 0, "y" );
  ...

Example 2-4

$ ./mem
Created: top
Created: x
Created: y
Created: z
Do stuff: top
Do stuff: x
Do stuff: y
Do stuff: z
Deleted: top
$

Example 2-5

The memory leak shown above does not do any harm in the current situation, as the application ends just after it has happended, but in another situation this could be a potential threat to the overall system stability.

Signals and Slots

The signals and slots are what makes the different Qt components as reuseable as they are. They provide a mechanism through which it is possible to expose interfaces that can be freely interconnected. For example a menu item, push button, toolbar button and any other item can expose signal corresponding to "activated", "clicked" or any other appropriate event. By connecting such a signal to a slot of any other item, the event automatically calls the slots.

A signal can also include values, thus making it possible to connect a slider, a spinbox, a knob or any other value generating item to any item accepting values, for example another slider, knob or spinbox, or something completely different such as an LCD display.

The key advantage of the signals and slots is that the caller does not have to know anything about the receiver and vice versa. This makes it possible to integrate many components easily without the component's designer having actually thought about the used configuration. This is truly loose coupling.

Function or Slot?
To make things a bit complicated, the development environment shipped with Qt 3.1.x and later sometimes refers to slots as functions. As they behave the same way is most cases this is not a problem, but throughout this text the term "slot" will be used.

In order to be able to use the signals and slots each class has to be declared in a header file. The implementation is best placed in a separate cpp file. The header file is then passed through a Qt tool known as the moc. The moc produces a cpp containing the code that makes the signals and slots happen (and more). Figure 2-2 illustrates this flow. Notice the naming convention used (the moc_ prefix) in the figure.

The moc flow

Figure 2-2 The moc flow

This additional compilation stage may seem to complicate the building process, but there is yet another Qt tool, qmake. It makes it as simple as qmake -project && qmake && make to build any Qt application. This will be described in detail futher on in this tutorial.

What are signals and slots - for real?
As mentioned earlier, a Qt applicaion is 100% C++, so what are signals and slots, for real? One part is the actual keywords, they are simply replaced by proper C++ by the pre-processor. The slots are then implemented as any class member method while the signals are implemented by the moc. Each object then holds a list of its connections (which slots are activated by which signal) and its slots (which are used to build the connections table in the connect method. The declarations of these tables are hidden in the Q_OBJECT macro. Of course there are more to this, but all can be seen by looking in a moc-generated cpp-file.

A basic demonstration of the loose coupling that signals and slots provide is demonstrated in the example below. First, the recieving class is shown in example 2-6. Notice that nothing stops a class from both recieving and sending, i.e. a single class can have both signals and slots.

class Receiver : public QObject
{
  Q_OBJECT
  
public:
  Receiver( QObject *parent=0, char *name=0 ) : QObject( parent, name )
  {
  }
  
public slots:
  void get( int x )
  {
    std::cout << "Received: " << x << std::endl;
  }
};

Example 2-6

This class starts with the Q_OBJECT macro which is needed if any other features than the simplified memory management is to be used. This macro includes some key declarations and serves as a marker to the moc that indicates that the class is to be parsed. Also notice that a new section called public slots has been added to the syntax.

Examples 2-7 and 2-8 contains the implementations of the sending classes. The first class, SenderA, implements the send signal that is emitted from the doEmit member function. The second sending class, SenderB emits the signal transmit from the member function doStuff. Notice that these classes have nothing in common except from inheriting QObject.

class SenderA : public QObject
{
  Q_OBJECT
  
public:
  SenderA( QObject *parent=0, char *name=0 ) : QObject( parent, name )
  {
  }
  
  void doSend()
  {
    emit( send( 7 ) );
  }
  
signals:
  void send( int );
};

Example 2-7

class SenderB : public QObject
{
  Q_OBJECT
  
public:
  SenderB( QObject *parent=0, char *name=0 ) : QObject( parent, name )
  {
  }
  
  void doStuff()
  {
    emit( transmit( 5 ) );
  }
  
signals:
  void transmit( int );
};

Example 2-8

To demonstrate how the moc code is merged with the code written by the human programmers, this example does not use the classic qmake-method but instead includes the code explicitly. To invoke the moc, the following command line is used: $QTDIR/bin/moc sisl.cpp -o moc_sisl.h. The first line of example 2-9 includes the resulting file, moc_sisl.h. The example code also contains the code creating the different object instances and connecting the different signals and slots. This piece of code is the only place aware of both the receiver class and the sending classes. The classes themselves only knows the signature, also known as the interface, of the signals to emit or receive.

#include "moc_sisl.h"

int main( int argc, char **argv )
{
  QApplication a( argc, argv );
  
  Receiver r;
  SenderA sa;
  SenderB sb;
  
  QObject::connect( &sa, SIGNAL(send(int)), &r, SLOT(get(int)) );
  QObject::connect( &sb, SIGNAL(transmit(int)), &r, SLOT(get(int)) );
  
  sa.doSend();
  sb.doStuff();
  
  return 0;
}        

Example 2-9

Finally, example 2-10 shows the result of a test run. The signals are emitted and the receiver displays the values.

$ ./sisl
Received: 7
Received: 5
$

Example 2-10

Values in the connection?
A common misconception is that it is possible to define the values to be sent along with a signal when doing the connection, e.g. connect( &a, SIGNAL(signal(5)), &b, SLOT(slot(int)) );. This is not possible. Only the signatures of the signals are used in the connect call. When emitting the signal a parameter can be provided, and only then. The correct version of the previous code would be connect( &a, SIGNAL(signal(int)), &b, SLOT(slot(int)) ); and the value (5) would be specified when emitting the signal.

Properties

Qt objects can have properties. These are simply a value that has a type and, atleast, a reading function, but possibly also a writing function. These are, for example, used by the Designer to show the properties of all the widgets. The official properties documentation can be found here.

Properties are not only a great way of organizing the code and defining what function affects which property. They can also be used as a primitive form of reflection. Any QObject pointer can access the properties of the object being pointed at. Even if it is a derived, more complex, class.

The code in example 2-11 demonstrates how a class with properties is declared. The showed code belongs to the file propobject.h. The Q_PROPERTY macro in combination with the Q_ENUMS macro does the trick.

#ifndef PROPOBJECT_H
#define PROPOBJECT_H

#include <qobject.h>
#include <qvariant.h>

class PropObject : public QObject
{
  Q_OBJECT

  Q_PROPERTY( TestProperty testProperty READ testProperty WRITE setTestProperty )
  Q_ENUMS( TestProperty )

  Q_PROPERTY( QString anotherProperty READ anotherProperty )
  
public:
  PropObject( QObject *parent=0, char *name=0 );
  
  enum TestProperty { InitialValue, AnotherValue };
  
  void setTestProperty( TestProperty p );
  TestProperty testProperty() const;
  
  QString anotherProperty() const { return QString( "I'm read-only!" ); }

private:
  TestProperty m_testProperty;  
};

#endif

Example 2-11

Notice that there are no commas between the parameters to the Q_PROPERTY macro. The syntax is that first the type of the property is declared, then the name follows. After that a keyword, either READ or WRITE, is encoutered and followed by the corresponding member function.

Read and write member functions
There is nothing special about the read and write member functions. The only restriction is that the reader must be of the parameters type and take no arguments (i.e. it has to be of the type void) while the writer must accept only one argument and it has to be of the parameter type.

It is common practice to declare write functions as slots. This increases the ease of re-use since most write functions are natural slots. Some of these are also candidates for emitting signals.

Example 2-12 shows the code of propobject.cpp. This is the implementation of the property object class.

#include "propobject.h"

PropObject::PropObject( QObject *parent, char *name ) : QObject( parent, name )
{
  m_testProperty = InitialValue;
}

void PropObject::setTestProperty( TestProperty p ) { m_testProperty = p; }
PropObject::TestProperty PropObject::testProperty() const { return m_testProperty; }

Example 2-12

Notice that the constructor accepts the QObject arguments parent and name. These are passed on to the base class. The member functions are only trivial implementations of a read-write property and a read-only property.

Finally, example 2-13 shows the code of main.cpp. This code accesses the properties through the standard QObject interface instead of direct access. The test run shown in example 2-14 shows that the code actually works. Notice that the enum is shown as it is treated by the computer internally, i.e. as an integer.

#include <qapplication.h>
#include <iostream>

#include "propobject.h"

int main( int argc, char **argv )
{
  QApplication a( argc, argv );

  QObject *o = new PropObject();
  
  std::cout << o->property( "testProperty" ).toString() << std::endl;
  
  o->setProperty( "testProperty", "AnotherValue" );
  std::cout << o->property( "testProperty" ).toString() << std::endl;
  
  std::cout << o->property( "anotherProperty" ).toString() << std::endl;
  
  return 0;
}

Example 2-13

$ ./prop
0
1
I'm read-only!
$

Example 2-14

Self-knowledge

Each Qt object has a meta object. This object is represented by an instance of the QMetaObject class. It is used to provide information about the current class. The meta object can be accessed through the QObject::metaObject() member function. The meta object provides some useful functions listed below.

Gives the name of the class, e.g. PropObject in the example of the previous section.

Gives the meta object of the super class, or 0 (null) if there is none.

Gives the names of the names of the properties and the meta data for each property as a QMetaProperty.

Gives the names of the slots of the class. If the optional parameter, super, is set to true the slots of the super classes are included too.

Gives the names of the signals of the class. An optional parameter, super, is available as for the signalNames member function.

The member functions listed above are just hints. There is more meta information available. Look at the official documentation for the details.

Summary

The source code from this chapter can be downloaded from here.

Recommended Reading

This is a part of digitalfanatics.org and is valid XHTML.