Signale und Slots

Signale und Slots gehören zu den wichtigsten Konzepten des Qt-Frameworks und dienen der Verständigung von Qt-Objekten untereinander. Wird ein Signal ausgelöst werden alle damit verbunden Slots ausgeführt. In anderen Bibliotheken wie z.B. Gtk+ werden stattdessen Callback-Funktionen verwendet. Das Signal/Slot-Konzept verhält sich ähnlich, ist jedoch klassenbasiert.

Signale mit Slots verbinden

Signale und Slots können durch die statische Methode QObject::connect() verbunden werden. Die Methode ist für verschiedene Parameter überladen, in dieser Erklärung wird sie mit folgender Signatur verwendet:

QMetaObject::Connection QObject::connect( const QObject *sender,
                                          PointerToMemberFunction signal,
                                          const QObject *receiver,
                                          PointerToMemberFunction method,
                                          Qt::ConnectionType type = Qt::AutoConnection)

sender: Zeiger auf das Objekt, das das Signal auslöst
signal: Zeiger auf das auslösende Signal
receiver: Zeiger auf das Objekt, dessen Slot ausgeführt werden soll
method: Zeiger auf den Slot, der das Signal verarbeitet
type: Der Default-Wert passt im Normalfall und sollte deshalb nicht überschrieben werden. Genaueres dazu findet sich im Kapitel Threads.
Rückgabewert: Ein Objekt, das die Verbindung beschreibt

:!: Hinweis: In Version 4 des Qt-Frameworks wurde ein Makro-basierter Signal/Slot-Mechanismus angeboten. Diese Implementierung des Konzepts wird technisch von Qt zwar noch immer unterstützt, aufgrund der hohen Fehleranfälligkeit allerdings nicht empfohlen.

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, &QPushButton::clicked, &app, &QApplication::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, static_cast<void (QSpinBox::*)(int)>(&QSpinBox::valueChanged), &slider, &QSlider::setValue );
  QObject::connect( &slider, &QSlider::valueChanged, &spinBox, &QSpinBox::setValue );
  QObject::connect( &button, &QPushButton::clicked, &app, &QApplication::quit );
 
  // Widgets anzeigen
  spinBox.show();
  slider.show();
  button.show();
 
  return app.exec();
}

Bis auf die drei 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. Da das Signal valueChanged() für den Typ QSpinBox überladen ist, muss in diesem Fall ein Cast verwendet werden. 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, static_cast<void (QSpinBox::*)(int)>(&QSpinBox::valueChanged), &slider, &QSlider::setValue );

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, &QSlider::valueChanged, &spinBox, &QSpinBox::setValue );

Da es vom Signal valueChanged in der Klasse QSlider nur eine einzige Variante gibt, ist hier kein Cast notwendig. 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, &QPushButton::clicked, &app, &QApplication::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, &CounterButton::tenTimesClicked, &app, &QApplication::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, &CounterButton::clicked, this, &CounterButton::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, &CounterButton::tenTimesClicked, &app, QApplication::quit );
  // Ändert der Button seinen Text, wird das Label angepasst.
  QObject::connect( &button, &CounterButton::textChanged,
                    &label, &CounterLabel::setCounterText );
 
  // 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, &CounterButton::clicked, this, &CounterButton::incrementCounter );
  // Text des Buttons aktualisieren
  adjustText();
  // Größe des Buttons fixieren
  setFixedSize( 600, 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( 600, 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.