====== 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.