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 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.
Im folgenden Beispiel wird das Programm beendet, wenn der Button betätigt wird:
#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.
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.
#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 );
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.
#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(); }
#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
#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.
#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(); }
#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
#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(); }
#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
#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>" ); }
Zusammenfassend noch einmal die Regeln zur Erstellung von eigenen Signalen und Slots:
QObject
abgeleitet werden. QWidget
ist bereits von QObject
abgeleitet, also sollte man im GUI-Bereich keine Probleme bekommen.QObject
bzw. die davon abgeleitete Klasse als erste Elternklasse genannt werden, ansonsten gibt der Qt-Meta-Object-Compiler eine Fehlermeldung aus.Q_OBJECT
muss in der Klassendefinition (ohne Semikolon!) verwendet werden.slots
nach einem Spezifizierer verwendet. Sie werden wie gewöhnliche Methoden implementiert und können auch als solche aufgerufen werden.signals
ohne Spezifizierer verwendet. Sie werden niemals implementiert.emit
vorangestellt.