====== 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. [[gui:gtk:start|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 [[frameworks:qt:process:threading|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: #include #include 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(); } {{:frameworks:qt:basic:firstconnect.png|}} 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. #include #include #include #include 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(&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(); } {{:frameworks:qt:basic:sync.png|}} 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(&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. #include "CounterButton.h" #include 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 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(); } {{:frameworks:qt:basic:counterbutton.png|}} 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 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 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 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( "Button wurde noch nicht gedrueckt" ); // Passende Größe für das Widget resize( 600, 100 ); } void CounterLabel::setCounterText( const QString& text ) { // Text in fetter Schrift setzen setText( "" + text + "" ); } {{:frameworks:qt:basic:counterlabel.png|}} ==== 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.