Signale & Slots
Einführung
In der GUI-Programmierung wollen wir oft, dass ein anderes Widget benachrichtigt wird, wenn wir ein Widget ändern. Allgemeiner ausgedrückt: Wir wollen, dass Objekte jeder Art miteinander kommunizieren können. Wenn zum Beispiel ein Benutzer auf eine Schaltfläche Close klickt, soll wahrscheinlich die Funktion close() des Fensters aufgerufen werden.
Andere Toolkits erreichen diese Art der Kommunikation mit Hilfe von Callbacks. Ein Callback ist ein Zeiger auf eine Funktion. Wenn Sie also möchten, dass eine Verarbeitungsfunktion Sie über ein Ereignis benachrichtigt, übergeben Sie einen Zeiger auf eine andere Funktion (den Callback) an die Verarbeitungsfunktion. Die Verarbeitungsfunktion ruft dann gegebenenfalls den Rückruf auf. Es gibt zwar erfolgreiche Frameworks, die diese Methode verwenden, aber Callbacks können unintuitiv sein und Probleme bei der Gewährleistung der Typkorrektheit von Callback-Argumenten verursachen.
Signale und Slots
In Qt haben wir eine Alternative zur Callback-Technik: Wir verwenden Signale und Slots. Ein Signal wird ausgesendet, wenn ein bestimmtes Ereignis eintritt. Qt's Widgets haben viele vordefinierte Signale, aber wir können jederzeit Widgets subklassifizieren, um ihnen unsere eigenen Signale hinzuzufügen. Ein Slot ist eine Funktion, die als Reaktion auf ein bestimmtes Signal aufgerufen wird. Qt Widgets haben viele vordefinierte Slots, aber es ist gängige Praxis, Widgets zu subklassifizieren und eigene Slots hinzuzufügen, um die Signale zu behandeln, an denen man interessiert ist.
Der Mechanismus für Signale und Slots ist typsicher: Die Signatur eines Signals muss mit der Signatur des empfangenden Slots übereinstimmen. (Tatsächlich kann ein Slot eine kürzere Signatur haben als das Signal, das er empfängt, da er zusätzliche Argumente ignorieren kann.) Da die Signaturen kompatibel sind, kann der Compiler bei der Verwendung der auf Funktionszeigern basierenden Syntax helfen, Typinkongruenzen zu erkennen. Die stringbasierte SIGNAL- und SLOT-Syntax erkennt Typinkongruenzen zur Laufzeit. Signale und Slots sind lose gekoppelt: Eine Klasse, die ein Signal aussendet, weiß nicht, welche Slots das Signal empfangen und kümmert sich auch nicht darum. Qt's Signale und Slots Mechanismus stellt sicher, dass wenn Sie ein Signal mit einem Slot verbinden, der Slot mit den Parametern des Signals zur richtigen Zeit aufgerufen wird. Signale und Slots können eine beliebige Anzahl von Argumenten eines beliebigen Typs annehmen. Sie sind vollständig typsicher.
Alle Klassen, die von QObject oder einer ihrer Unterklassen (z. B. QWidget) erben, können Signale und Slots enthalten. Signale werden von Objekten ausgesendet, wenn sie ihren Zustand auf eine Weise ändern, die für andere Objekte interessant sein könnte. Das ist alles, was das Objekt tut, um zu kommunizieren. Es weiß nicht und kümmert sich nicht darum, ob irgendetwas die von ihm ausgesandten Signale empfängt. Dies ist eine echte Informationskapselung und gewährleistet, dass das Objekt als Softwarekomponente verwendet werden kann.
Slots können für den Empfang von Signalen verwendet werden, aber sie sind auch normale Mitgliedsfunktionen. Genauso wie ein Objekt nicht weiß, ob irgendetwas seine Signale empfängt, weiß ein Slot nicht, ob irgendwelche Signale mit ihm verbunden sind. Dadurch wird sichergestellt, dass mit Qt wirklich unabhängige Komponenten erstellt werden können.
Sie können beliebig viele Signale mit einem einzigen Slot verbinden, und ein Signal kann mit so vielen Slots verbunden werden, wie Sie benötigen. Es ist sogar möglich, ein Signal direkt mit einem anderen Signal zu verbinden. (Dadurch wird das zweite Signal sofort ausgegeben, wenn das erste Signal ausgegeben wird).
Zusammen bilden Signale und Slots einen leistungsfähigen Mechanismus zur Programmierung von Komponenten.
Signale
Signale werden von einem Objekt ausgegeben, wenn sich sein interner Zustand in einer Weise geändert hat, die für den Client oder Eigentümer des Objekts interessant sein könnte. Signale sind Funktionen mit öffentlichem Zugriff und können von überall aus gesendet werden, aber wir empfehlen, sie nur von der Klasse, die das Signal definiert, und ihren Unterklassen auszusenden.
Wenn ein Signal ausgegeben wird, werden die damit verbundenen Slots normalerweise sofort ausgeführt, genau wie bei einem normalen Funktionsaufruf. In diesem Fall ist der Mechanismus der Signale und Slots völlig unabhängig von jeder GUI-Ereignisschleife. Die Ausführung des Codes, der auf die emit
Anweisung folgt, erfolgt, sobald alle Slots zurückgekehrt sind. Die Situation ist etwas anders, wenn queued connections verwendet wird; in einem solchen Fall wird der Code nach dem Schlüsselwort emit
sofort fortgesetzt, und die Slots werden später ausgeführt.
Wenn mehrere Slots mit einem Signal verbunden sind, werden die Slots nacheinander ausgeführt, in der Reihenfolge, in der sie verbunden wurden, wenn das Signal ausgegeben wird.
Signale werden automatisch vom moc generiert und müssen nicht in der Datei .cpp
implementiert werden.
Ein Hinweis zu den Argumenten: Unsere Erfahrung zeigt, dass Signale und Slots besser wiederverwendbar sind, wenn sie keine speziellen Typen verwenden. Wenn QScrollBar::valueChanged() einen speziellen Typ wie das hypothetische QScrollBar::Range verwenden würde, könnte es nur mit Slots verbunden werden, die speziell für QScrollBar entwickelt wurden. Es wäre unmöglich, verschiedene Eingabe-Widgets miteinander zu verbinden.
Steckplätze
Ein Slot wird aufgerufen, wenn ein mit ihm verbundenes Signal ausgesendet wird. Slots sind normale C++-Funktionen und können ganz normal aufgerufen werden; ihre einzige Besonderheit ist, dass Signale mit ihnen verbunden werden können.
Da Slots normale Mitgliedsfunktionen sind, folgen sie den normalen C++-Regeln, wenn sie direkt aufgerufen werden. Als Slots können sie jedoch von jeder Komponente, unabhängig von ihrer Zugriffsebene, über eine Signal-Slot-Verbindung aufgerufen werden. Das bedeutet, dass ein Signal, das von einer Instanz einer beliebigen Klasse ausgegeben wird, dazu führen kann, dass ein privater Slot in einer Instanz einer nicht verwandten Klasse aufgerufen wird.
Sie können Slots auch als virtuell definieren, was sich in der Praxis als sehr nützlich erwiesen hat.
Im Vergleich zu Callbacks sind Signale und Slots etwas langsamer, weil sie mehr Flexibilität bieten, obwohl der Unterschied für reale Anwendungen unbedeutend ist. Im Allgemeinen ist das Aussenden eines Signals, das mit einigen Slots verbunden ist, etwa zehnmal langsamer als der direkte Aufruf der Empfänger mit nicht-virtuellen Funktionsaufrufen. Dies ist der Overhead, der erforderlich ist, um das Verbindungsobjekt zu finden, um sicher über alle Verbindungen zu iterieren (d.h. zu überprüfen, dass nachfolgende Empfänger während der Aussendung nicht zerstört wurden) und um alle Parameter in einer generischen Weise zu marshallieren. Zehn nicht-virtuelle Funktionsaufrufe mögen zwar nach viel klingen, aber es ist viel weniger Overhead als z. B. jede new
oder delete
Operation. Sobald Sie eine String-, Vektor- oder Listen-Operation durchführen, die hinter der Bühne new
oder delete
erfordert, ist der Overhead für Signale und Slots nur noch für einen sehr kleinen Teil der gesamten Funktionsaufrufkosten verantwortlich. Das Gleiche gilt, wenn Sie einen Systemaufruf in einem Slot ausführen oder indirekt mehr als zehn Funktionen aufrufen. Die Einfachheit und Flexibilität des Signals- und Slot-Mechanismus ist den Overhead, den Ihre Benutzer nicht einmal bemerken werden, allemal wert.
Beachten Sie, dass andere Bibliotheken, die Variablen mit den Namen signals
oder slots
definieren, Compiler-Warnungen und Fehler verursachen können, wenn sie zusammen mit einer Qt-basierten Anwendung kompiliert werden. Um dieses Problem zu lösen, entfernen Sie das entsprechende Präprozessorsymbol #undef
.
Ein kleines Beispiel
Eine minimale C++-Klassendeklaration könnte lauten:
class Counter { public: Counter() { m_value = 0; } int value() const { return m_value; } void setValue(int value); private: int m_value; };
Eine kleine QObject-basierte Klasse könnte lauten:
#include <QObject> class Counter : public QObject { Q_OBJECT // Note. The Q_OBJECT macro starts a private section. // To declare public members, use the 'public:' access modifier. public: Counter() { m_value = 0; } int value() const { return m_value; } public slots: void setValue(int value); signals: void valueChanged(int newValue); private: int m_value; };
Die auf QObject basierende Version hat den gleichen internen Zustand und bietet öffentliche Methoden, um auf den Zustand zuzugreifen, aber zusätzlich hat sie Unterstützung für Komponentenprogrammierung mit Signalen und Slots. Diese Klasse kann der Außenwelt mitteilen, dass sich ihr Zustand geändert hat, indem sie ein Signal aussendet, valueChanged()
, und sie hat einen Slot, an den andere Objekte Signale senden können.
Alle Klassen, die Signale oder Slots enthalten, müssen Q_OBJECT am Anfang ihrer Deklaration erwähnen. Außerdem müssen sie sich (direkt oder indirekt) von QObject ableiten.
Slots werden von den Anwendungsprogrammierern implementiert. Hier ist eine mögliche Implementierung des Slots Counter::setValue()
:
void Counter::setValue(int value) { if (value != m_value) { m_value = value; emit valueChanged(value); } }
Die Zeile emit
sendet das Signal valueChanged()
vom Objekt aus, mit dem neuen Wert als Argument.
Im folgenden Codeschnipsel erstellen wir zwei Counter
Objekte und verbinden das valueChanged()
Signal des ersten Objekts mit dem setValue()
Slot des zweiten Objekts mittels QObject::connect():
Counter a, b; QObject::connect(&a, &Counter::valueChanged, &b, &Counter::setValue); a.setValue(12); // a.value() == 12, b.value() == 12 b.setValue(48); // a.value() == 12, b.value() == 48
Der Aufruf von a.setValue(12)
bewirkt, dass a
ein Signal valueChanged(12)
aussendet, das b
in seinem Slot setValue()
empfängt, d.h. b.setValue(12)
wird aufgerufen. Dann sendet b
das gleiche Signal valueChanged()
, aber da kein Slot mit dem Signal valueChanged()
von b
verbunden ist, wird das Signal ignoriert.
Beachten Sie, dass die Funktion setValue()
nur dann den Wert setzt und das Signal ausgibt, wenn value != m_value
aufgerufen wird. Dies verhindert Endlosschleifen bei zyklischen Verbindungen (z. B. wenn b.valueChanged()
mit a.setValue()
verbunden wäre).
Standardmäßig wird für jede Verbindung, die Sie herstellen, ein Signal ausgegeben; für doppelte Verbindungen werden zwei Signale ausgegeben. Sie können alle diese Verbindungen mit einem einzigen disconnect()-Aufruf unterbrechen. Wenn Sie Qt::UniqueConnection type übergeben, wird die Verbindung nur hergestellt, wenn es sich nicht um ein Duplikat handelt. Wenn es bereits ein Duplikat gibt (genau dasselbe Signal an genau denselben Slot auf denselben Objekten), schlägt die Verbindung fehl und connect gibt false
zurück.
Dieses Beispiel veranschaulicht, dass Objekte zusammenarbeiten können, ohne dass sie irgendwelche Informationen übereinander wissen müssen. Um dies zu ermöglichen, müssen die Objekte nur miteinander verbunden werden, was mit einigen einfachen QObject::connect()-Funktionsaufrufen oder mit der automatischen Verbindungsfunktion von uic erreicht werden kann.
Ein reales Beispiel
Im Folgenden sehen Sie ein Beispiel für den Kopf einer einfachen Widget-Klasse ohne Mitgliedsfunktionen. Der Zweck ist zu zeigen, wie Sie Signale und Slots in Ihren eigenen Anwendungen verwenden können.
#ifndef LCDNUMBER_H #define LCDNUMBER_H #include <QFrame> class LcdNumber : public QFrame { Q_OBJECT
LcdNumber
erbt QObject, das den größten Teil des Signal-Slot-Wissens über QFrame und QWidget besitzt. Es ist dem eingebauten Widget QLCDNumber etwas ähnlich.
Das Makro Q_OBJECT wird vom Präprozessor erweitert, um mehrere Mitgliedsfunktionen zu deklarieren, die von moc
implementiert werden. Wenn Sie Compiler-Fehler in der Art von "undefinierte Referenz auf vtable für LcdNumber
" erhalten, haben Sie wahrscheinlich vergessen, das moc auszuführen oder die moc-Ausgabe in den Link-Befehl aufzunehmen.
public: LcdNumber(QWidget *parent = nullptr); signals: void overflow();
Nach dem Klassenkonstruktor und den Mitgliedern von public
deklarieren wir die Klasse signals
. Die Klasse LcdNumber
gibt ein Signal aus, overflow()
, wenn sie aufgefordert wird, einen unmöglichen Wert anzuzeigen.
Wenn Sie sich nicht um einen Überlauf kümmern oder wissen, dass ein Überlauf nicht auftreten kann, können Sie das Signal overflow()
ignorieren, d. h. es nicht mit einem Steckplatz verbinden.
Wenn Sie andererseits zwei verschiedene Fehlerfunktionen aufrufen wollen, wenn die Zahl überläuft, verbinden Sie das Signal einfach mit zwei verschiedenen Slots. Qt wird beide aufrufen (in der Reihenfolge, in der sie verbunden wurden).
public slots: void display(int num); void display(double num); void display(const QString &str); void setHexMode(); void setDecMode(); void setOctMode(); void setBinMode(); void setSmallDecimalPoint(bool point); }; #endif
Ein Slot ist eine Empfangsfunktion, die verwendet wird, um Informationen über Zustandsänderungen in anderen Widgets zu erhalten. LcdNumber
verwendet sie, wie der obige Code zeigt, um die angezeigte Zahl zu setzen. Da display()
Teil der Schnittstelle der Klasse zum Rest des Programms ist, ist der Slot öffentlich.
Mehrere der Beispielprogramme verbinden das Signal valueChanged() eines QScrollBar mit dem Slot display()
, so dass die LCD-Zahl kontinuierlich den Wert des Rollbalkens anzeigt.
Beachten Sie, dass display()
überladen ist; Qt wählt die passende Version, wenn Sie ein Signal mit dem Slot verbinden. Mit Callbacks müssten Sie fünf verschiedene Namen finden und die Typen selbst im Auge behalten.
Signale und Slots mit Standardargumenten
Die Signaturen von Signalen und Slots können Argumente enthalten, und die Argumente können Standardwerte haben. Betrachten Sie QObject::destroyed():
void destroyed(QObject* = nullptr);
Wenn ein QObject gelöscht wird, sendet es das Signal QObject::destroyed(). Wir wollen dieses Signal abfangen, wo auch immer wir einen Verweis auf das gelöschte QObject haben könnten, damit wir es bereinigen können. Eine geeignete Slot-Signatur könnte sein:
void objectDestroyed(QObject* obj = nullptr);
Um das Signal mit dem Slot zu verbinden, verwenden wir QObject::connect(). Es gibt mehrere Möglichkeiten, Signale und Slots zu verbinden. Die erste ist die Verwendung von Funktionszeigern:
connect(sender, &QObject::destroyed, this, &MyObject::objectDestroyed);
Die Verwendung von QObject::connect() mit Funktionszeigern hat mehrere Vorteile. Erstens kann der Compiler damit prüfen, ob die Argumente des Signals mit den Argumenten des Slots kompatibel sind. Außerdem können die Argumente bei Bedarf implizit vom Compiler umgewandelt werden.
Sie können auch eine Verbindung zu Funktoren oder C++11-Lambdas herstellen:
connect(sender, &QObject::destroyed, this, [=](){ this->m_objects.remove(sender); });
In beiden Fällen stellen wir this als Kontext im Aufruf von connect() zur Verfügung. Das Kontextobjekt liefert Informationen darüber, in welchem Thread der Empfänger ausgeführt werden soll. Dies ist wichtig, da durch die Bereitstellung des Kontexts sichergestellt wird, dass der Empfänger im Kontext-Thread ausgeführt wird.
Die Verbindung des Lambdas wird unterbrochen, wenn der Sender oder der Kontext zerstört wird. Sie sollten darauf achten, dass alle im Funktor verwendeten Objekte noch aktiv sind, wenn das Signal gesendet wird.
Die andere Möglichkeit, ein Signal mit einem Slot zu verbinden, ist die Verwendung von QObject::connect() und den Makros SIGNAL
und SLOT
. Die Regel, ob Argumente in die SIGNAL()
und SLOT()
Makros aufgenommen werden sollen oder nicht, wenn die Argumente Standardwerte haben, ist, dass die an das SIGNAL()
Makro übergebene Signatur nicht weniger Argumente haben darf als die an das SLOT()
Makro übergebene Signatur.
Alle diese Makros würden funktionieren:
connect(sender, SIGNAL(destroyed(QObject*)), this, SLOT(objectDestroyed(Qbject*))); connect(sender, SIGNAL(destroyed(QObject*)), this, SLOT(objectDestroyed())); connect(sender, SIGNAL(destroyed()), this, SLOT(objectDestroyed()));
Aber das hier wird nicht funktionieren:
connect(sender, SIGNAL(destroyed()), this, SLOT(objectDestroyed(QObject*)));
...weil der Slot eine QObject erwartet, die das Signal nicht senden wird. Diese Verbindung wird einen Laufzeitfehler melden.
Beachten Sie, dass Signal- und Slot-Argumente vom Compiler nicht überprüft werden, wenn Sie diese QObject::connect()-Überladung verwenden.
Erweiterte Verwendung von Signalen und Slots
Für Fälle, in denen Sie Informationen über den Absender des Signals benötigen, bietet Qt die Funktion QObject::sender(), die einen Zeiger auf das Objekt zurückgibt, das das Signal gesendet hat.
Lambda-Ausdrücke sind ein bequemer Weg, um eigene Argumente an einen Slot zu übergeben:
connect(action, &QAction::triggered, engine, [=]() { engine->processAction(action->text()); });
Verwendung von Qt mit Signalen und Slots von Drittanbietern
Es ist möglich, Qt mit einem Signal/Slot-Mechanismus eines Drittanbieters zu verwenden. Sie können sogar beide Mechanismen im selben Projekt verwenden. Um das zu tun, schreiben Sie das Folgende in Ihre CMake-Projektdatei:
target_compile_definitions(my_app PRIVATE QT_NO_KEYWORDS)
In einer qmake-Projektdatei (.pro) müssen Sie folgendes schreiben:
CONFIG += no_keywords
Damit wird Qt angewiesen, die moc-Schlüsselwörter signals
, slots
und emit
nicht zu definieren, da diese Namen von einer Bibliothek eines Drittanbieters, z.B. Boost, verwendet werden. Um dann Qt-Signale und -Slots mit dem no_keywords
Flag weiter zu verwenden, ersetzen Sie einfach alle Verwendungen der Qt-Moc-Schlüsselwörter in Ihren Quellen durch die entsprechenden Qt-Makros Q_SIGNALS (oder Q_SIGNAL), Q_SLOTS (oder Q_SLOT) und Q_EMIT.
Signale und Slots in Qt-basierten Bibliotheken
Die öffentliche API von Qt-basierten Bibliotheken sollte die Schlüsselwörter Q_SIGNALS
und Q_SLOTS
anstelle von signals
und slots
verwenden. Ansonsten ist es schwierig, eine solche Bibliothek in einem Projekt zu verwenden, das QT_NO_KEYWORDS
definiert.
Um diese Einschränkung zu erzwingen, kann der Ersteller der Bibliothek die Präprozessor-Definition QT_NO_SIGNALS_SLOTS_KEYWORDS
beim Erstellen der Bibliothek setzen.
Diese Definition schließt Signale und Slots aus, ohne zu beeinflussen, ob andere Qt-spezifische Schlüsselwörter in der Bibliotheksimplementierung verwendet werden können.
Siehe auch QLCDNumber, QObject::connect(), Meta-Objektsystem und Qt's Property System.
© 2025 The Qt Company Ltd. Documentation contributions included herein are the copyrights of their respective owners. The documentation provided herein is licensed under the terms of the GNU Free Documentation License version 1.3 as published by the Free Software Foundation. Qt and respective logos are trademarks of The Qt Company Ltd. in Finland and/or other countries worldwide. All other trademarks are property of their respective owners.