Signale und Slots

Signale und Slots gehören zu den wichtigsten Konzepten des Qt-Frameworks und dienen der Verständigung von Qt-Objekten untereinander. In anderen Bibliotheken wie z.B. Gtk+ werden stattdessen Callback-Funktionen verwendet. Das Signal/Slot-Konzept verhält sich ähnlich, ist jedoch klassenbasiert.
Wird ein Signal ausgelöst werden alle damit verbunden Slots ausgeführt. Sollte der Compiler einmal seltsame Fehlermeldungen ausgeben oder Slots werden nicht ordnungsgemäß aufgerufen, hilft es oft qmake erneut auszuführen und das Programm komplett neu zu kompilieren.
Um das Signal/Slot-Konzept zu ermöglichen, wird ein Meta-Object-Compiler (MOC) benötigt. Dieser analysiert den Quellcode vor dem Kompiliervorgang. Dabei speichert er benötigte Informationen in eigenen Dateien ab und wandelt Qt-spezifische Konstrukte über Makros in gültigen C++-Code um. Dieses Vorgehen wird aufgrund der Verwendung von C-Makros oft kritisiert, ist aber für eine benutzerfreundliche Implementierung des Konzepts notwendig.

Signale mit Slots verbinden

Signale und Slots können durch die statische Methode QObject::connect() verbunden werden. Die Methode hat folgende Signatur:

bool QObject::connect( const QObject *sender, const char *signal, 
                        const QObject *receiver, const char *method, 
                        Qt::ConnectionType type = Qt::AutoConnection )

sender: Zeiger auf das Objekt, das das Signal auslöst
signal: Name des Signals (dazu wird das Makro SIGNAL() verwendet)
receiver: Zeiger auf das Objekt, dessen Slot ausgeführt werden soll
method: Name des Slots (dazu wird das Makro SLOT() verwendet)
type: Der Default-Wert passt im Normalfall und sollte deshalb nicht überschrieben werden. Genaueres dazu findet sich im Kapitel Threads.
Rückgabewert: true bei Erfolg, false bei Fehler

Bei Angabe der Funktionen werden nur der Name und die Typen der Parameter verwendet (die Namen der Parametervariablen und der Rückgabetyp der Methode werden nicht angegeben!). Gibt es die in der Funktion verwendeten Signale oder Slots nicht, erhält man keinen Compiler-Fehler aber eine Ausgabe zur Laufzeit. Verbindungen können über QObject::disconnect() wieder aufgelöst werden.

Verbindungen ohne Parameter

Im folgenden Beispiel wird das Programm beendet, wenn der Button betätigt wird:

// main.cpp
#include <QApplication>
#include <QPushButton>
 
int main( int argc, char *argv[] )
{
  QApplication app( argc, argv );
  QPushButton button( "proggen.org" );
 
  QObject::connect( &button, SIGNAL( clicked() ), &app, SLOT( quit() ) );
  button.setWindowTitle( "proggen.org" );
  button.resize( 150, 150 );
  button.show();
 
  return app.exec();
}



Löst das Objekt button das Signal clicked() aus, führt das Objekt app den Slot quit() aus. clicked() bedeutet in diesem Fall aber eher „betätigt“, da der Button auch mit der Tastatur ausgelöst werden kann.

Verbindungen mit Parameter

Als nächstes wollen wir das soeben erhaltene Wissen nutzen um den Wert einer Spinbox mit dem eines Sliders zu synchronisieren. Ändert sich der Wert eines Widgets, soll das andere automatisch auf den gleichen Wert angepasst werden. Das Programm kann mit durch Betätigung des Buttons beendet werden. Dazu ist es nötig, dass das Signal den neuen Wert als Parameter an den Slot weitergibt.

// main.cpp
#include <QApplication>
#include <QPushButton>
#include <QSpinBox>
#include <QSlider>
 
int main( int argc, char *argv[] )
{
  QApplication app( argc, argv );
  QSpinBox spinBox;
  QSlider slider;
  QPushButton button( "Beenden" );
 
  // Größe der Spinbox fixieren
  spinBox.setFixedSize( 150, 150 );
  // Wertebereich für Spinbox setzen
  spinBox.setMinimum( 0 );
  spinBox.setMaximum( 100 );
  // Größe des Sliders fixieren
  slider.setFixedSize( 150, 200 );
  // Wertebereich für Slider setzen
  slider.setMinimum( 0 );
  slider.setMaximum( 100 );
  // Größe des Buttons fixieren
  button.setFixedSize( 150, 100 );
 
  // Titel setzen
  spinBox.setWindowTitle( "QSpinBox" );
  slider.setWindowTitle( "QSlider" );
  button.setWindowTitle( "QPushButton" );
 
  // Verbindungen aufbauen
  QObject::connect( &spinBox, SIGNAL( valueChanged( int ) ), &slider, SLOT( setValue( int ) ) );
  QObject::connect( &slider, SIGNAL( valueChanged( int ) ), &spinBox, SLOT( setValue( int ) ) );
  QObject::connect( &button, SIGNAL( clicked() ), &app, SLOT( quit() ) );
 
  // Widgets anzeigen
  spinBox.show();
  slider.show();
  button.show();
 
  return app.exec();
}



Bis auf die 3 Aufrufe von QObject::connect() sollte das Programm selbsterklärend sein.
Wird der Wert der Spinbox geändert, löst sie das Signal valueChanged() aus und gibt als Parameter den neuen Wert als Integer mit. Dieses Signal verbinden wir mit dem Slot des Sliders, dessen Wert dann auf den übergebenen gesetzt wird. Ändert die Spinbox ihren Wert, wird jener des Sliders angepasst.

QObject::connect( &spinBox, SIGNAL( valueChanged( int ) ), &slider, SLOT( setValue( int ) ) );

Als nächstes erstellen wir die gleiche Verbindung noch einmal, nur mit vertauschten Rollen. Ändert der Slider seinen Wert, wird jener der Spinbox angepasst.

QObject::connect( &slider, SIGNAL( valueChanged( int ) ), &spinBox, SLOT( setValue( int ) ) );

Der Aufruf von setValue() selbst löst übrigens kein Signal aus, weshalb dieses Vorgehen keine „Schleife“ verursacht.
Zu guter Letzt beendet der Button bei Betätigung das Programm.

QObject::connect( &button, SIGNAL( clicked() ), &app, SLOT( quit() ) );

Signale und Slots selbst implementieren

Um Signale und Slots selbst zu implementieren, müssen wir eine Klasse von QObject ableiten. Dabei werden für die betreffenden Methoden in der Klassendefinition die Qt-Spezifizierer signals und slots verwendet. Vor Slots kann noch ein Standard-Spezifizierer (public, private oder protected) stehen. Slots werden wie normale Methoden implementiert und können auch als solche verwendet werden. Zu beachten ist, dass auch private Slots mit anderen Objekt verbunden und von ihnen ausgelöst werden können. Direkt können sie aber trotzdem nur von befugten Klassen (Die eigene Klasse und friend-Klassen) aufgerufen werden. Das Objekt, welches das auslösende Signal ausgelöst hat, kann mit der Methode sender() als QObject abgefragt werden und danach in ein entsprechendes Objekt gecastet werden.
Im Gegensatz dazu haben Signale keinen Spezifizierer. Am Beginn der Klassendefinition muss auch noch das Makro Q_OBJECT (ohne Semikolon danach!) verwendet werden.
Wichtig: Signale werden niemals implementiert, sie werden lediglich deklariert und mittels emit ausgelöst. emit selbst hat keine Funktion, es dient nur der besseren Lesbarkeit.
Sowohl Signale als auch Slots können nach den Regeln von C++ überladen werden.
Als nächstes wollen wir einen Button implementieren, der anzeigt wie oft er bereits gedrückt wurde. Beim 10 Mal wird das Programm beendet.

// main.cpp
#include "CounterButton.h"
#include <QApplication>
 
int main( int argc, char *argv[] )
{
  QApplication app( argc, argv );
  CounterButton button;
 
  // Löst der Button das Signal aus, wird das Programm beendet.
  QObject::connect( &button, SIGNAL( tenTimesClicked() ), &app, SLOT( quit() ) );
 
  // Titel des Buttons festlegen und anzeigen
  button.setWindowTitle( "CounterButton" );
  button.show();
 
  return app.exec();
}
// CounterButton.h
#ifndef COUNTERBUTTON_H
#define COUNTERBUTTON_H
 
#include <QPushButton>
 
class CounterButton : public QPushButton   // von QPushButton ableiten -> indirekt von QObject abgeleitet
{
 
  Q_OBJECT                 // Kein Semikolon!
 
  public:
    CounterButton();       // Konstruktor der den Counter und Text auf 0 setzt
 
  private:
    unsigned int counter;  // Zählt wie oft Button gedrückt wurde
    QString pattern;       // Muster zum Setzen des Textes
    void adjustText();     // Passt den Text an den Counter an
 
  private slots:
    void incrementCounter();  // Erhöht den Counter und löst wenn nötig das Signal aus
 
  signals:
    void tenTimesClicked();   // Wird ausgelöst, wenn der Button 10 mal gedrückt wurde
 
};
 
#endif // COUNTERBUTTON_H
// CounterButton.cpp
#include "CounterButton.h"
 
CounterButton::CounterButton()
{
  counter = 0;
  // String-Muster, in das wir später die Werte einfügen
  pattern = "Button wurde %1 mal betaetigt.\nZum beenden muss er noch %2 mal betaetigt werden.";
  // Wird der Button betätigt, erhöhen wir den Counter.
  // Es ist hier kein QObject-Namespace nötig, da wir über QPushButton indirekt von QObject ableiten.
  connect( this, SIGNAL( clicked() ), this, SLOT( incrementCounter() ) );
  // Text des Buttons aktualisieren
  adjustText();
  // Größe des Buttons fixieren
  setFixedSize( 500, 100 );
}
 
 
void CounterButton::adjustText()
{
  // Richtige Werte in das Muster einsetzen und Text auf dem Button anzeigen.
  setText( pattern.arg( QString::number( counter ), QString::number( 10 - counter ) ) );
}
 
 
void CounterButton::incrementCounter()
{
  counter++;
  // Wenn der Button 10 Mal gedrückt wurde, wird das Signal ausgelöst.
  if (counter == 10)
    emit tenTimesClicked();
  // Text des Buttons aktualisieren
  adjustText();
}



Dieses Beispiel zeigt schön, wie man Signale und Slots selbst implementiert.

Was noch fehlt ist die Verwendung von Parametern, was nach diesem Beispiel relativ logisch erscheinen sollte. Deshalb fügen wir jetzt noch ein CounterLabel hinzu, das den gleichen Text wie der Button anzeigt, aber in fetter Schrift.

// main.cpp
#include "CounterButton.h"
#include "CounterLabel.h"
#include <QApplication>
 
int main( int argc, char *argv[] )
{
  QApplication app( argc, argv );
  CounterButton button;
  CounterLabel label;
 
  // Löst der Button das Signal aus, wird das Programm beendet.
  QObject::connect( &button, SIGNAL( tenTimesClicked() ), &app, SLOT( quit() ) );
  // Ändert der Button seinen Text, wird das Label angepasst.
  QObject::connect( &button, SIGNAL( textChanged( const QString& ) ),
                    &label, SLOT( setCounterText( const QString& ) ) );
 
  // Titel des Buttons festlegen und anzeigen
  button.setWindowTitle( "CounterButton" );
  button.show();
 
  // Titel des Labels festlegen und anzeigen
  label.setWindowTitle( "CounterLabel" );
  label.show();
 
  return app.exec();
}
// CounterButton.h
#ifndef COUNTERBUTTON_H
#define COUNTERBUTTON_H
 
#include <QPushButton>
 
class CounterButton : public QPushButton   // von QPushButton ableiten -> indirekt von QObject abgeleitet
{
 
  Q_OBJECT                 // Kein Semikolon!
 
  public:
    CounterButton();       // Konstruktor der den Counter und Text auf 0 setzt
 
  private:
    unsigned int counter;  // Zählt wie oft Button gedrückt wurde
    QString pattern;       // Muster zum Setzen des Textes
    void adjustText();     // Passt den Text an den Counter an
 
  private slots:
    void incrementCounter();  // Erhöht den Counter und löst wenn nötig das Signal aus
 
  signals:
    void tenTimesClicked();   // Wird ausgelöst, wenn der Button 10 mal gedrückt wurde
    void textChanged( const QString& text ); // Wird ausgelöst, wenn der Text des Buttons geändert wurde
 
};
 
#endif // COUNTERBUTTON_H
// CounterButton.cpp
#include "CounterButton.h"
 
CounterButton::CounterButton()
{
  counter = 0;
  // String-Muster, in das wir später die Werte einfügen
  pattern = "Button wurde %1 mal betaetigt.\nZum beenden muss er noch %2 mal betaetigt werden.";
  // Wird der Button betätigt, erhöhen wir den Counter.
  // Es ist hier kein QObject-Namespace nötig, da wir über QPushButton indirekt von QObject ableiten.
  connect( this, SIGNAL( clicked() ), this, SLOT( incrementCounter() ) );
  // Text des Buttons aktualisieren
  adjustText();
  // Größe des Buttons fixieren
  setFixedSize( 500, 100 );
}
 
 
void CounterButton::adjustText()
{
  // Richtige Werte in das Muster einsetzen und Text auf dem Button anzeigen.
  setText( pattern.arg( QString::number( counter ), QString::number( 10 - counter ) ) );
  // Signal auslösen, dass der Text geändert wurde.
  emit textChanged( text() );
}
 
 
void CounterButton::incrementCounter()
{
  counter++;
  // Wenn der Button 10 Mal gedrückt wurde, wird das Signal ausgelöst.
  if (counter == 10)
    emit tenTimesClicked();
  // Text des Buttons aktualisieren
  adjustText();
}
// CounterLabel.h
#ifndef COUNTERLABEL_H
#define COUNTERLABEL_H
 
#include <QLabel>
 
class CounterLabel : public QLabel
{
 
    Q_OBJECT
 
  public:
    CounterLabel();
 
  public slots:
    void setCounterText( const QString& text );
 
};
 
#endif // COUNTERLABEL_H
// CounterLabel.cpp
#include "CounterLabel.h"
 
CounterLabel::CounterLabel()
{
  // Text am Anfang in die Mitte setzen
  setAlignment( Qt::AlignHCenter | Qt::AlignVCenter );
  setText( "<b>Button wurde noch nicht gedrueckt</b>" );
  // Passende Größe für das Widget
  resize( 500, 100 );
}
 
 
void CounterLabel::setCounterText( const QString& text )
{
  // Text in fetter Schrift setzen
  setText( "<b>" + text + "</b>" );
}



Regeln für die Implementierung von Signalen und Slots

Zusammenfassend noch einmal die Regeln zur Erstellung von eigenen Signalen und Slots:

  • Die Klasse muss von QObject abgeleitet werden. QWidget ist bereits von QObject abgeleitet, also sollte man im GUI-Bereich keine Probleme bekommen.
  • Bei Mehrfachvererbung muss QObject bzw. die davon abgeleitete Klasse als erste Elternklasse genannt werden, ansonsten gibt der Qt-Meta-Object-Compiler eine Fehlermeldung aus.
  • Das Makro Q_OBJECT muss in der Klassendefinition (ohne Semikolon!) verwendet werden.
  • Für Slots wird das Qt-Schlüsselwort slots nach einem Spezifizierer verwendet. Sie werden wie gewöhnliche Methoden implementiert und können auch als solche aufgerufen werden.
  • Für Signale wird das Qt-Schlüsselwort signals ohne Spezifizierer verwendet. Sie werden niemals implementiert.
  • Signale werden durch einfachen Aufruf ausgelöst. Zur besseren Lesbarkeit wird ihnen das Qt-Schlüsselwort emit vorangestellt.
  • Signale und Slots können nicht in Template-Klassen implementiert werden. Der den Signal/Slot-Mechanismus ermöglichende MOC ist nämlich ein Präprozessor, aber Template-Klassen werden erst bei Bedarf vom Compiler erzeugt, der im Erstellungsvorgang nach dem MOC steht.