Kapitel 2. Philosophie des Objektmodells

Wer beim ersten Kontakt mit der Quantentheorie nicht schockiert ist, hat sie wahrscheinlich nicht verstanden.
— Max Born

Das Objektmodell von Qt basiert auf dem Konzept, dass alles aus Objekten aufgebaut ist. Die Klasse QObject ist faktisch die grundlegende Basisklasse. Die überwiegende Mehrheit der Qt-Klassen sind von ihr abgeleitet. Klassen, die Signale und Slots verwenden, müssen von dieser Klasse abgeleitet werden.

Mehrfachvererbung

Bei der Mehrfachvererbung ist es wichtig zu beachten, dass bei der Klassendefinition der Name der Klasse QObject (oder einer davon abgeleiteten Klasse) an erster Stelle stehen muss, damit MOC (Meta Object Compiler, Metaobjekt-Compiler) sie korrekt erkennen kann. Eine andere Reihenfolge führt zu einem Kompilierfehler. In Listing 2.1 ist die korrekte Reihenfolge für Mehrfachvererbung dargestellt.

Listing 2.1. Reihenfolge der Vererbung

class MyClass : public QObject, public AnotherClass {
...
};
Weiteres zur Mehrfachvererbung

Bei der Mehrfachvererbung ist außerdem zu beachten, dass nur eine der Basisklassen von QObject abgeleitet sein darf. Mit anderen Worten: Es ist nicht möglich, gleichzeitig von mehreren Klassen zu erben, die QObject als Basisklasse haben.

Die Klasse QObject bietet Unterstützung für:

  • Signale und Slots (signal/slot);
  • Timer;
  • den Mechanismus zur Organisation von Objekten in Hierarchien;
  • Ereignisse und deren Filtermechanismus;
  • die Organisation von Objekthierarchien;
  • Metaobjekt-Informationen;
  • Typumwandlungen;
  • Eigenschaften und Eigenschaftsbindungen (Property Bindings).

Signale und Slots sind Mittel, die einen effizienten Informationsaustausch über von Objekten erzeugte Ereignisse ermöglichen. Darüber werden wir später in diesem Kapitel ausführlich sprechen.

Der Timer ermöglicht es jeder von QObject abgeleiteten Klasse, kein zusätzliches Timer-Objekt erstellen zu müssen. Dadurch wird Entwicklungszeit gespart. Mehr über Timer erfahren Sie in Kapitel 37.

Der Mechanismus zur Organisation von Objekten in hierarchischen Strukturen ermöglicht eine erhebliche Reduzierung des Entwicklungsaufwands, da man sich nicht um die Speicherfreigabe der erstellten Objekte kümmern muss – Elternobjekte sind selbst für die Zerstörung ihrer Kindobjekte verantwortlich.

Der Ereignisfiltermechanismus ermöglicht das Abfangen von Ereignissen. Ein Ereignisfilter kann in jeder von QObject abgeleiteten Klasse installiert werden, wodurch die Reaktion von Objekten auf Ereignisse geändert werden kann, ohne den Quellcode der Klasse zu modifizieren (siehe Kapitel 15).

Metaobjekt-Informationen umfassen Informationen über die Klassenvererbung, mit denen festgestellt werden kann, ob Klassen direkte Nachfahren sind, sowie den Klassennamen ermittelt werden kann.

Für die Typumwandlung stellt Qt die Template-Funktion qobject_cast() bereit, die auf den vom Metaobjekt-Compiler MOC erzeugten Metainformationen basiert (siehe Kapitel 3) für Klassen, die von QObject abgeleitet sind.

Eigenschaften sind Felder, für die zwingend Lesemethoden existieren müssen. Mit ihrer Hilfe kann von außen auf Objektattribute zugegriffen werden – beispielsweise aus der Skriptsprache Qt Script (siehe Teil VII). Eigenschaften werden auch in der visuellen Entwicklungsumgebung Qt Designer (siehe Kapitel 44) intensiv genutzt. Der Mechanismus ist in Qt mithilfe von Präprozessor-Direktiven implementiert. Eine Eigenschaft wird mit dem Makro Q_PROPERTY definiert. Die allgemeine Definition einer Eigenschaft sieht wie folgt aus:

Q_PROPERTY(type name
           READ getFunction
           [WRITE setFunction]
           [RESET resetFunction]
           [NOTIFY notifySignal]
           [BINDABLE bindableFunction]
           [DESIGNABLE bool]
           [SCRIPTABLE bool]
           [STORED bool]
          )

Zuerst werden Typ und Name der Eigenschaft angegeben, dann der Name der Lesemethode (READ). Die Definition der übrigen Parameter ist optional. Der dritte Parameter gibt den Namen der Schreibmethode an (WRITE), der vierte den Namen der Reset-Methode (RESET), der fünfte (NOTIFY) das Signal, das bei Änderung der Eigenschaft ausgelöst wird, der sechste (BINDABLE) den Namen der Methode, die ein QBindable-Objekt zurückgibt, der siebte (DESIGNABLE) ist ein boolescher Wert, der angibt, ob die Eigenschaft im Eigenschaftsinspektor von Qt Designer erscheinen soll. Der achte Parameter (SCRIPTABLE) ist ebenfalls ein boolescher Wert, der steuert, ob die Eigenschaft für die Skriptsprache Qt Script zugänglich sein soll. Der letzte, neunte Parameter (STORED) steuert die Serialisierung, d.h. ob die Eigenschaft beim Speichern des Objekts persistiert werden soll.

Nachdem Sie nun das Konzept „Eigenschaft” kennengelernt haben (auch wenn wir diesen Mechanismus in nächster Zeit nicht benötigen werden), definieren wir als einfaches Beispiel eine Eigenschaft zur Steuerung des Nur-Lesen-Modus in einer Klasse (Listing 2.2).

Listing 2.2. Definition einer Eigenschaft zur Steuerung des Nur-Lesen-Modus

class MyClass : public QObject {
Q_OBJECT
Q_PROPERTY(bool readOnly READ isReadOnly WRITE setReadOnly NOTIFY readOnlyChanged)
 
private:
    bool m_bReadOnly;signals:
    void readOnlyChanged();
public:
    MyClass(QObject* pobj = nullptr) : QObject(pobj)
 , m_bReadOnly(false)
    {
    }
 
public:
    void setReadOnly(bool bReadOnly)
    {
        if (m_bReadOnly != bReadOnly) {
            m_bReadOnly = bReadOnly;
 emit readOnlyChanged();
 }
    }
 
    bool isReadOnly() const
    {
        return m_bReadOnly;
    }
}

Die in Listing 2.2 gezeigte Klasse MyClass erbt von der Klasse QObject. Wir definieren das Attribut m_bReadOnly, in dem die Zustandswerte gespeichert werden. Dieses Attribut wird im Konstruktor mit dem Wert false initialisiert. Zum Abrufen und Ändern des Attributwerts sind in der Klasse MyClass die Methoden isReadOnly() und setReadOnly() definiert. Diese Methoden werden im Makro Q_PROPERTY registriert. Die Methode isReadOnly() dient zum Abrufen des Werts und wird daher im Abschnitt READ angegeben, während die Methode setReadOnly() zum Ändern des Werts dient und daher im Abschnitt WRITE steht. Zusätzlich wurde das Signal readOnlyChanged() und der Parameter NOTIFY hinzugefügt, der auf dieses Signal verweist, was die Verwendung von Eigenschaftsbindungen ermöglicht.

Aus dem Programm können wir den Wert unserer Eigenschaft folgendermaßen ändern:

pobj->setProperty("readOnly", true);

Und so kann der aktuelle Wert abgerufen werden:

bool bReadOnly = pobj->property("readOnly").toBool();

Um alle Eigenschaften eines beliebigen Objekts und deren Werte auf einmal zu erfahren, kann man wie folgt vorgehen: Mit der Methode propertyCount() der Klasse QMetaObject die Anzahl der Eigenschaften ermitteln. Dann in einer Schleife für jeden Index das Eigenschaftsobjekt abrufen und daraus die Methode typeName() für den Eigenschaftstyp und die Methode name() für den Eigenschaftsnamen aufrufen. Der Eigenschaftswert ist im Typ QVariant gespeichert, einer universellen Klasse zur Speicherung beliebiger Typen (siehe Kapitel 4). Den Wert erhalten wir durch Aufruf der Methode property() am Objekt selbst. Diese Methode wurde bereits besprochen. Hier ein Beispiel, wie das Auslesen aller Eigenschaften eines Objekts implementiert werden kann:

const QMetaObject* pmo = pobj->metaObject();
for (int i = 0; i < pmo->propertyCount(); ++i) {
    const QMetaProperty mp = pmo->property(i);
    qDebug() << "Property#:" << i;
    qDebug() << "Type:" << mp.typeName();
    qDebug() << "Name:" << mp.name();
    qDebug() << "Value:" << pobj->property(mp.name());
}

Mechanismus von Signalen und Slots

Elemente der grafischen Benutzeroberfläche reagieren auf bestimmte Weise auf Benutzeraktionen und senden Nachrichten. Es gibt mehrere Varianten solcher Lösungen.

Das alte Konzept der Callback-Funktionen ist ein Mechanismus, bei dem einer Funktion ein Zeiger auf eine andere Funktion übergeben wird, die unter bestimmten Bedingungen aufgerufen wird. Dieser Mechanismus liegt dem X Window System zugrunde und basiert auf der Verwendung gewöhnlicher Funktionen, die als Reaktion auf Benutzeraktionen aufgerufen werden. Die Anwendung dieses Konzepts verkompliziert den Quellcode erheblich und macht ihn weniger verständlich. Außerdem fehlt hier die Möglichkeit, Typprüfungen für Rückgabewerte durchzuführen, da in allen Fällen ein Zeiger auf den leeren Typ void zurückgegeben wird. Um beispielsweise Code mit einer Schaltfläche zu verknüpfen, muss ein Zeiger auf die Schaltfläche an die Funktion übergeben werden. Wenn der Benutzer auf die Schaltfläche klickt, wird die Funktion aufgerufen. Die Bibliotheken selbst prüfen nicht, ob die an die Funktion übergebenen Argumente vom erforderlichen Typ sind, was oft die Ursache für Abstürze ist. Ein weiterer Nachteil von Callback-Funktionen besteht darin, dass GUI-Elemente eng mit den funktionalen Teilen des Programms verbunden sind, was die unabhängige Entwicklung von Klassen erheblich erschwert. Ein prominenter Vertreter dieses Konzepts ist die Motif-Bibliothek.

Es ist wichtig zu bedenken, dass Motif und Windows API für prozedurale Programmierung konzipiert sind und bei der Umsetzung objektorientierter Projekte sicherlich Schwierigkeiten auftreten werden.

Es gibt jedoch spezielle C++-Klassenbibliotheken, die die Programmierung für Windows erleichtern. Eine der ersten solchen Bibliotheken (die erstaunlicherweise bis heute von einer Reihe von Einzelentwicklern und Unternehmen verwendet wird) ist Microsoft Foundation Classes (MFC). Sie als objektorientiert zu bezeichnen ist nur mit großen Einschränkungen möglich, da sie von Personen erstellt wurde, die die elementarsten Prinzipien des objektorientierten Ansatzes nicht kannten. Eines der grundlegenden Gebote des objektorientierten Ansatzes ist die Kapselung, die es verbietet, Klassenattribute ungeschützt zu lassen (da dann Objekte Daten ohne Wissen des Eigentümerobjekts lesen und ändern können). Trotzdem ist diese Anforderung in vielen MFC-Klassen nicht erfüllt. Die MFC-Bibliothek selbst ist eine Schicht, die Zugang zu in C implementierten Windows-Funktionen bietet, was Entwickler dazu zwingt, von Zeit zu Zeit veraltete Strukturen zu verwenden, die nicht in den Rahmen des objektorientierten Konzepts passen. Interessanterweise verwendet Microsoft selbst für die Implementierung des bekannten Programms Microsoft Word überhaupt kein MFC.

Bei Verwendung von MFC werden zur Verknüpfung von Nachrichten und Verarbeitungsmethoden spezielle Makros verwendet – sogenannte Message Maps (Listing 2.3). Sie überladen den Quellcode des Programms stark und verringern dessen Lesbarkeit erheblich.

Listing 2.3. Programmfragment, implementiert mit MFC

class CPhotoStylerApp : public CWinApp {
public:
    CPhotoStylerApp();
public:
    virtual BOOL InitInstance();
 
    afx_msg void OnAppAbout();
    afx_msg void OnFileNew();
 
    DECLARE_MESSAGE_MAP()
};
BEGIN_MESSAGE_MAP(CPhotoStylerApp, CWinApp)
    ON_COMMAND(ID_APP_ABOUT, OnAppAbout)
    ON_COMMAND(ID_FILE_NEW, OnFileNew)
    ON_COMMAND(ID_FILE_NEW, CWinApp::OnFileNew)
    ON_COMMAND(ID_FILE_OPEN, CWinApp::OnFileOpen)
    ON_COMMAND(ID_FILE_PRINT_SETUP, CWinApp::OnFilePrintSetup)
END_MESSAGE_MAP()

Konstruktionen wie die in Listing 2.3 gezeigte sind für das menschliche Verständnis sehr unbequem und verursachen Verwirrung bei der Codeanalyse. Auch wenn viele von der Bequemlichkeit automatischer Codegenerierungswerkzeuge berichten, wurden diese nicht aus einem guten Grund geschaffen. Die mangelnde Durchdachtheit der Bibliothek selbst zwingt den Entwickler dazu, bei geringfügigen Änderungen den Code an mehreren Stellen gleichzeitig zu modifizieren. Um beispielsweise einem Dialog ein Textfeld hinzuzufügen, muss eine ganze Reihe von Operationen durchgeführt werden: Erstens muss in der Dialog-Klasse ein Attribut zum Speichern der im Textfeld eingegebenen Werte erstellt werden. Zweitens muss ein Ressourcenidentifikator für das Textfeld festgelegt werden. Drittens müssen in der Methode DoDataExchange() der Ressourcenidentifikator und das Attribut mithilfe der Methode DDX_Text() einander zugeordnet werden, damit ein Datenaustausch zwischen Textfeld und Attribut stattfinden kann. Viertens muss dieser Austausch durch Übergabe von booleschen Werten true oder false in der Methode UpdateData() gesteuert werden. Nur mit Hilfe automatischer Codeerzeugungswerkzeuge kann dieses Problem teilweise gelöst werden, wobei man im Austausch andere Nachteile erhält – beispielsweise zusätzliche Verunreinigung des Programmcodes mit unnötigen Informationen und mögliche Nichtübereinstimmung des generierten Codes mit den für das Projekt festgelegten Formatierungs- und Notationsanforderungen (falls keine ungarische Notation verwendet wird). Ich bin kein Gegner des begründeten Einsatzes solcher Werkzeuge, aber meiner Meinung nach sollten sie nicht als Mittel zur Beseitigung von Designmängeln der Bibliothek selbst verwendet werden.

In dieser Situation liegt ein Teil der Schuld bei der Sprache C++ selbst. C++ wurde nicht als Werkzeug für die Erstellung von Benutzeroberflächen konzipiert und bietet daher keine entsprechende Unterstützung, die die Programmierung in diesem Bereich komfortabler machen würde. Wenn beispielsweise die Ereignisübermittlung durch Sprachmittel selbst realisiert würde, wäre der Einsatz solcher Makros überflüssig. Bis heute ist nichts Derartiges gelungen, weshalb die Qt-Bibliothek wie ein Blitz aus heiterem Himmel kam. Im Gegensatz zu den meisten anderen Programmierbibliotheken erweitert Qt die Sprache C++ um zusätzliche Schlüsselwörter zur Erfüllung dieser Aufgabe.

Das Problem der C++-Erweiterung wird in Qt mit Hilfe des speziellen Präprozessors MOC (Meta Object Compiler, Metaobjekt-Compiler) gelöst. Er analysiert Klassen auf das Vorhandensein des speziellen Makros Q_OBJECT in ihrer Definition und fügt alle erforderlichen zusätzlichen Informationen in eine separate Datei ein. Dies geschieht automatisch, ohne direktes Eingreifen des Entwicklers. Eine solche automatische Codegenerierung widerspricht nicht dem üblichen C++-Programmierprozess – schließlich erzeugt auch der Standardpräprozessor vor der Kompilierung des Programms Zwischencode mit ausgeführten Präprozessorbefehlen. In ähnlicher Weise arbeitet auch MOC, indem er alle notwendigen Zusatzinformationen in eine separate Datei schreibt, deren Inhalt keine Aufmerksamkeit des Entwicklers erfordert. Das Makro Q_OBJECT muss direkt in der Zeile nach dem Schlüsselwort class mit der Klassennamensdefinition stehen. Es ist sehr wichtig zu beachten, dass nach dem Makro kein Semikolon stehen darf. Das Einfügen des Makros in die Klassendefinition ist sinnvoll, wenn die erstellte Klasse den Signal-Slot-Mechanismus verwendet oder Eigenschaftsinformationen benötigt.

Hinweis

Es wird empfohlen, Klassen mit dem Makro Q_OBJECT in separaten Header-Dateien (.h) zu definieren und nicht innerhalb von Implementierungsdateien (.cpp oder main.cpp). Dies hängt mit den Besonderheiten der Arbeitsweise des Metaobjekt-Compilers (MOC) zusammen, der zusätzlichen Code für solche Klassen generiert. Die Definition von Klassen mit Q_OBJECT innerhalb von Implementierungsdateien kann bei der Kompilierung zu Fehlern wie „unresolved external symbol” führen.

Der Signal-Slot-Mechanismus ersetzt vollständig das alte Callback-Funktionsmodell, ist sehr flexibel und vollständig objektorientiert. Signale und Slots sind ein Eckpfeiler-Konzept der Qt-Programmierung, das es ermöglicht, nicht miteinander verbundene Objekte zu verbinden. Jede von QObject abgeleitete Klasse kann Signale senden und empfangen. Diese Eigenschaft passt ideal in das Konzept der Objektorientierung und widerspricht nicht der menschlichen Wahrnehmung. Stellen Sie sich folgende Situation vor: Ihr Telefon klingelt, und Sie reagieren darauf, indem Sie den Hörer abnehmen. In der Sprache der Signale und Slots könnte diese Situation wie folgt beschrieben werden: Das Objekt „Telefon” hat das Signal „Klingeln” gesendet, auf das das Objekt „Mensch” mit dem Slot „Hörer abnehmen” reagiert hat.

Die Verwendung des Signal-Slot-Mechanismus bietet dem Programmierer folgende Vorteile:

  • Jede von QObject abgeleitete Klasse kann eine beliebige Anzahl von Signalen und Slots haben.
  • Nachrichten, die über Signale gesendet werden, können mehrere Argumente beliebigen Typs haben.
  • Ein Signal kann mit einer beliebigen Anzahl von Slots verbunden werden. Das gesendete Signal wird an alle verbundenen Slots weitergeleitet.
  • Ein Slot kann Nachrichten von vielen Signalen empfangen, die zu verschiedenen Objekten gehören. Dazu genügt es, mehrere Verbindungen mit der Methode connect() zu erstellen, wobei der Ziel-Slot dieselbe Methode ist. Die Klasse QSignalMapper wird in besonderen Fällen verwendet, wenn verschiedene Daten von verschiedenen Signalquellen an einen Slot übergeben werden müssen, ohne zusätzliche Unterklassen zu erstellen.
  • Die Verbindung von Signalen und Slots kann an beliebiger Stelle in der Anwendung erfolgen.
  • Signale und Slots sind Mechanismen, die die Kommunikation zwischen Objekten gewährleisten. Darüber hinaus kann diese Verbindung zwischen Objekten in verschiedenen Threads hergestellt werden (siehe Kapitel 38).
  • Bei der Zerstörung eines Objekts werden automatisch alle Signal-Slot-Verbindungen getrennt. Dies garantiert, dass keine Signale an nicht mehr existierende Objekte gesendet werden.

Es müssen auch die Nachteile erwähnt werden, die mit der Verwendung von Signalen und Slots verbunden sind:

  • Signale und Slots sind kein Teil der Sprache C++, daher muss vor der Programmkompilierung ein zusätzlicher Präprozessor ausgeführt werden.
  • Das Senden von Signalen ist etwas langsamer als ein normaler Funktionsaufruf, wie er beim Callback-Mechanismus verwendet wird.
  • Es besteht die Notwendigkeit, von der Klasse QObject zu erben.
  • Während der Kompilierung werden bei Verwendung der traditionellen Syntax keinerlei Prüfungen durchgeführt: Ob ein Signal oder Slot in den entsprechenden Klassen vorhanden ist oder nicht, ob Signal und Slot miteinander kompatibel sind und ob sie miteinander verbunden werden können. Ein Fehler wird erst dann erkannt, wenn die Anwendung im Debugger oder in der Konsole gestartet wird. All diese Informationen werden in der Konsole ausgegeben, daher muss man, um sie unter Windows zu sehen, in der Projektdatei (siehe Kapitel 3) im Abschnitt CONFIG die Option „console“ hinzufügen (für macOS und Linux sind keine zusätzlichen Änderungen an der Projektdatei erforderlich).
Alternative Form der Signal-Slot-Verbindungen

Die Verwendung der modernen Syntax für Signal-Slot-Verbindungen beseitigt diesen Nachteil, da sie es ermöglicht, Fehler bei der Verbindung von Signalen und Slots bereits während der Kompilierung zu erkennen. In Qt6 ist diese Syntax die bevorzugte Variante. Mehr dazu finden Sie im Abschnitt „Verbindung von Objekten“.

Signale

Signale (signals) umgeben uns überall im Alltag: der Weckerklingel, die Geste des Verkehrspolizisten, und auch im nicht alltäglichen Leben – ein Beispiel könnte ein indianisches Signalfeuer sein usw. In der Qt-Programmierung versteht man unter dem Begriff „Signal” Methoden, die Nachrichten versenden können. Der Grund für das Auftreten eines Signals kann eine Benachrichtigung über eine Zustandsänderung eines Steuerelements sein – zum Beispiel über das Verschieben eines Schiebereglers. Auf solche Änderungen kann ein verbundenes Objekt, das solche Signale überwacht, entsprechend reagieren, was aber nicht zwingend erforderlich ist. Dies ist ein sehr wichtiger Punkt – er besagt, dass die zu verbindenden Objekte absolut unabhängig sein und getrennt voneinander implementiert werden können. Dieser Ansatz ermöglicht es dem signalsendenden Objekt, sich nicht darum zu kümmern, was anschließend mit diesen Signalen geschieht. Das signalsendende Objekt kann sogar nicht wissen, dass seine Signale von anderen Objekten empfangen und verarbeitet werden. Dank dieser Trennung kann ein großes Projekt in Komponenten aufgeteilt werden, die von verschiedenen Programmierern separat entwickelt und dann mithilfe von Signalen und Slots verbunden werden. Dies macht den Code sehr flexibel und leicht erweiterbar – wenn eine der Komponenten veraltet oder anders implementiert werden muss, ändern sich alle anderen Komponenten, die mit dieser Komponente kommunizieren, und das gesamte Projekt nicht. Die neue Komponente wird nach der Entwicklung an die Stelle der alten gesetzt und mithilfe derselben Signale und Slots mit dem Hauptprogramm verbunden. Dies macht die Qt-Bibliothek besonders attraktiv für die Implementierung komponentenorientierter Anwendungen. Vergessen Sie jedoch nicht, dass eine große Anzahl von Verbindungen zu stark gekoppelten Systemen führt, in denen selbst geringfügige Änderungen unvorhersehbare Folgen haben können.

Signale werden in der Klasse wie gewöhnliche Methoden definiert, nur ohne Implementierung. Aus Sicht des Programmierers sind sie lediglich Methodenprototypen, die in der Header-Datei der Klassendefinition enthalten sind. Die gesamte weitere Arbeit an der Code-Implementierung für diese Methoden übernimmt MOC. Signalmethoden dürfen keine Werte zurückgeben, daher muss vor dem Methodennamen immer der Rückgabeparameter void stehen.

Ein Signal muss nicht unbedingt mit einem Slot verbunden werden. Wenn keine Verbindung hergestellt wurde, wird es einfach nicht verarbeitet. Diese Trennung von sendenden und empfangenden Objekten schließt die Möglichkeit aus, dass einer der verbundenen Slots das signalsendende Objekt irgendwie behindern könnte.

Die Bibliothek stellt eine große Anzahl bereits fertiger Signale für vorhandene Steuerelemente bereit. In der Regel reichen diese Signale zur Lösung der gestellten Aufgaben aus, aber manchmal entsteht die Notwendigkeit, neue Signale in eigenen Klassen zu implementieren. Ein Beispiel für die Definition eines Signals ist in Listing 2.4 dargestellt.

Listing 2.4. Definition eines Signals

class MySignal {
Q_OBJECT
...
signals:
    void doIt();
...
};

Beachten Sie die Signalmethode doIt(). Sie hat keine Implementierung – diese Arbeit übernimmt MOC, der ungefähr folgende Implementierung bereitstellt:

void MySignal::doIt()
{
    QMetaObject::activate(this, &staticMetaObject, 0, 0);
}

Signale werden mit dem speziellen Abschnitt signals: deklariert und haben standardmäßig geschützten (protected) Zugriff. Das bedeutet, dass Signale nur innerhalb der Klasse selbst oder ihrer Nachfolger mit dem Schlüsselwort emit aufgerufen werden können. Dieser Mechanismus verhindert den Aufruf eines Signals von außerhalb des Objekts, was den architektonischen Prinzipien von Qt entspricht und die korrekte Logik der Signal-Slot-Funktionsweise gewährleistet. Für Tests oder Sonderfälle wird empfohlen, zusätzliche Methoden oder Friend-Klassen zu verwenden, anstatt Signale öffentlich zu machen.

Ein Signal kann mit dem Schlüsselwort emit gesendet werden. Da Signale die Rolle von aufrufenden Methoden spielen, führt die Konstruktion emit doIt() zu einem normalen Aufruf der Methode doIt(). Signale können von den Klassen gesendet werden, die sie enthalten. Im Listing 2.4 kann das Signal doIt() beispielsweise nur von Objekten der Klasse MySignal und von keiner anderen gesendet werden. Um ein Signal programmatisch von einem Objekt dieser Klasse senden zu können, sollte eine Methode sendSignal() hinzugefügt werden, deren Aufruf das Objekt der Klasse MySignal veranlasst, das Signal doIt() zu senden, wie in Listing 2.5 gezeigt.

Listing 2.5. Implementierung eines Signals

class MySignal {
Q_OBJECT
public:
    void sendSignal()
    {
        emit doIt();
    }
signals:
    void doIt();
};

Signale können auch Informationen senden, die in Parametern übergeben werden. Wenn beispielsweise die Notwendigkeit besteht, einen Textstring in einem Signal zu übertragen, kann dies wie in Listing 2.6 gezeigt implementiert werden.

Listing 2.6. Implementierung eines Signals mit Parameter

class MySignal : public QObject {
Q_OBJECT
public:
    void sendSignal()
    {
        emit sendString("Information");
    }
signals:
    void sendString(const QString&);
};

Slots

Slots (slots) sind Methoden, die mit Signalen verbunden werden. Im Wesentlichen sind sie gewöhnliche Methoden. Der größte Unterschied zwischen Slots und normalen Methoden besteht in der Fähigkeit der Slots, Signale zu empfangen. Wie normale Methoden werden Slots in der Klasse als public, private oder protected definiert. Wenn es erforderlich ist, dass ein Slot nur mit Signalen von fremden Objekten verbunden werden kann, aber nicht als normale Methode von außen aufgerufen werden kann, muss dieser Slot als protected oder private deklariert werden. In allen anderen Fällen deklarieren Sie sie als public. In den Deklarationen muss vor jeder Gruppe von Slots entsprechend stehen: private slots:, protected slots: oder public slots:. Slots können auch virtuell sein.

Verbindung eines Signals mit einem virtuellen Slot

Die Verbindung eines Signals mit einem virtuellen Slot ist etwa zehnmal langsamer als mit einem nicht-virtuellen. Daher sollten Slots nicht virtuell gemacht werden, wenn es keine besondere Notwendigkeit gibt.

Allerdings gibt es kleine Einschränkungen, die normale Methoden von Slots unterscheiden. In Slots können keine Standardparameter verwendet werden (z.B.: slotMethod(int n = 8)) und Slots können nicht als static definiert werden.

Die Klassen der Bibliothek enthalten eine Reihe bereits implementierter Slots. Aber die Definition von Slots für eigene Klassen ist ein häufiger Vorgang. Die Implementierung eines Slots ist in Listing 2.7 gezeigt.

Listing 2.7. Implementierung eines Slots

class MySlot : public QObject {
 Q_OBJECT
public:
 MySlot();
public slots:
 void slot()
 {
   qDebug() << "I’m a slot";
 }
};

Innerhalb eines Slots kann durch Aufruf der Methode sender() herausgefunden werden, von welchem Objekt das Signal gesendet wurde. Die Methode gibt einen Zeiger auf ein Objekt vom Typ QObject zurück. In diesem Fall wird beispielsweise der Name des signalsendenden Objekts auf der Konsole ausgegeben:

void slot()
{
    qDebug() << sender()->objectName();
}

Verbindung von Objekten

Die Verbindung von Objekten erfolgt mit der statischen Methode connect(), die in der Klasse QObject definiert ist. Qt6 empfiehlt die Verwendung der neuen Verbindungssyntax, die auf Zeigern auf Memberfunktionen basiert, aber aus Gründen der Abwärtskompatibilität wird auch die alte Syntax mit den Makros SIGNAL/SLOT beibehalten.

Neue Verbindungssyntax

Die neue Verbindungssyntax sieht wie folgt aus:

QObject::connect(sender, &SenderClass::signalMethod, 
                 receiver, &ReceiverClass::slotMethod);

Der Vorteil dieser Syntax besteht darin, dass die Typprüfung und die Überprüfung der Existenz der Methoden zur Kompilierzeit erfolgt, nicht zur Laufzeit. Dies ermöglicht es, Fehler viel früher zu erkennen. Hier ein Beispiel:

auto* pcmd = new QPushButton("Click me");
QObject::connect(pcmd, &QPushButton::clicked, 
                 this, &MyClass::handleButtonClick);

Sie können auch Lambda-Ausdrücke als Slot verwenden:

QObject::connect(pcmd, &QPushButton::clicked, 
                 [=]() { qDebug() << "Button clicked!"; });

Vorteile der modernen connect()-Syntax:

  • Typprüfung und Überprüfung der Methodenexistenz zur Kompilierzeit, nicht zur Laufzeit.
  • Automatische Codevervollständigung in der IDE (Methoden können aus einer Dropdown-Liste ausgewählt werden).
  • Bessere Performance.
  • Möglichkeit zur Verwendung von Lambda-Ausdrücken.

Mögliche Nachteile:

  • Weniger explizite Angabe der Methodensignaturen im Code, was die Lesbarkeit verringern kann.
  • Schwierigkeiten bei der Arbeit mit überladenen Methoden (erfordert die Verwendung von qOverload()).
  • Eine gewisse Ausführlichkeit bei der Verwendung von Methodenzeigern.

Klassische Verbindungssyntax

Die klassische Verbindungssyntax mit den Makros SIGNAL und SLOT sieht so aus:

QObject::connect(const QObject*     sender,
                 const char*        signal,
                 const QObject*     receiver,
                 const char*        slot,
                 Qt::ConnectionType type = Qt::AutoConnection
                );

Ihr werden fünf Parameter übergeben:

  • sender – Zeiger auf das signalsendende Objekt.
  • signal – das Signal, mit dem die Verbindung hergestellt wird. Der Prototyp (Name und Argumente) der Signalmethode muss in das spezielle Makro SIGNAL(method()) eingeschlossen werden.
  • receiver – Zeiger auf das Objekt, das einen Slot zur Signalverarbeitung hat.
  • slot – der Slot, der beim Empfang des Signals aufgerufen wird. Der Prototyp des Slots muss in das spezielle Makro SLOT(method()) eingeschlossen werden.
  • type – steuert den Verarbeitungsmodus. Es gibt mehrere mögliche Werte:

1. Qt::DirectConnection – das Signal wird sofort durch Aufruf der entsprechenden Slot-Methode verarbeitet.

2. Qt::QueuedConnection – das Signal wird in ein Ereignis umgewandelt (siehe Kapitel 14) und in die allgemeine Warteschlange zur Verarbeitung eingereiht.

3. Qt::BlockingQueuedConnection – wie Qt::QueuedConnection, blockiert aber den Sender, bis der Slot aufgerufen wird.

4. Qt::UniqueConnection – kann mit den vorherigen Typen durch bitweises ODER kombiniert werden, verhindert doppelte Verbindungen.

5. Qt::AutoConnection – dies ist der automatische Modus, der wie folgt funktioniert: Wenn sich das signalsendende Objekt im selben Thread wie das empfangende Objekt befindet, wird der Modus Qt::DirectConnection eingestellt, andernfalls der Modus Qt::QueuedConnection. Dieser Modus (Qt::AutoConnection) ist in der Methode connect() standardmäßig definiert. Sie werden die Modi wahrscheinlich selten „manuell” ändern müssen, aber es ist nützlich zu wissen, dass diese Möglichkeit besteht.

Das folgende Beispiel zeigt, wie die Objektverbindung in einem Programm mit der klassischen Syntax der Methode connect() durchgeführt werden kann:

void main()
{
    ...
    QObject::connect(pSender, SIGNAL(signalMethod()),
                     pReceiver, SLOT(slotMethod())
                    );
    ...
}
Hinweis

Obwohl die alte Syntax mit SIGNAL und SLOT in Qt6 noch unterstützt wird, wird empfohlen, die neue Syntax zu verwenden, die zur Kompilierzeit Typkontrolle bietet und eine bessere Performance hat.

Wenn der Aufruf aus einer von QObject abgeleiteten Klasse erfolgt, kann QObject:: weggelassen werden:

MyClass::MyClass() : QObject()
{
    ...
    connect(pSender, &SenderClass::signalMethod,
            pReceiver, &ReceiverClass::slotMethod
 );
    ...
}

Falls der Slot in der Klasse enthalten ist, aus der die Verbindung hergestellt wird, kann die verkürzte Form der Methode connect verwendet werden, wobei this als Empfänger angegeben wird:

MyClass::MyClass() : QObject()
{
    connect(pSender, &SenderClass::signalMethod, this, &MyClass::slot);
}
void MyClass::slot()
{
   qDebug() << "I’m a slot";
}

Die Methode connect() gibt nach dem Aufruf ein Objekt der Klasse Connection zurück, mit dem festgestellt werden kann, ob die Verbindung erfolgreich war oder nicht. Diese Tatsache können Sie beispielsweise nutzen, um das Programm im Fehlerfall durch Aufruf des Makros Q_ASSERT() mit einem Fehler zu beenden.

Die Klasse Connection enthält einen impliziten Konvertierungsoperator zum Typ bool, daher verwenden wir im Beispiel eine Variable bOk dieses Typs. Ein solcher Code könnte wie folgt aussehen:

bool bOk = true;
bOk &= connect(pcmd1, &QPushButton::clicked, pobjReceiver1, &Receiver::slotButton1Clicked);
bOk &= connect(pcmd2, &QPushButton::clicked, pobjReceiver2, &Receiver::slotButton2Clicked); 
Q_ASSERT(bOk);

Manchmal entstehen Situationen, in denen ein Objekt ein Signal nicht verarbeitet, sondern es einfach weiterleitet. Dafür ist es nicht erforderlich, einen Slot zu definieren, der als Reaktion auf den Empfang eines Signals (mittels emit) sein eigenes Signal sendet. Man kann einfach Signale miteinander verbinden. Das zu sendende Signal muss in der Klassendefinition enthalten sein:

MyClass::MyClass() : QObject()
{
    connect(pSender, SIGNAL(signalMethod()), SIGNAL(mySignal()));
}

Das Senden von Signalen kann für einige Zeit durch Aufruf der Methode blockSignals() mit dem Parameter true blockiert werden. Das Objekt wird „schweigen”, bis die Blockierung durch dieselbe Methode blockSignals() mit dem Parameter false aufgehoben wird. Mit der Methode signalsBlocked() kann der aktuelle Zustand der Signalblockierung abgefragt werden.

Direkte Verbindung eines Signals mit einer Lambda-Funktion

In Qt6 können Sie ein Signal direkt mit einer Lambda-Funktion verbinden, was die Signalverarbeitung vereinfacht, ohne einen separaten Slot erstellen zu müssen. Die folgende Verbindung blendet nach dem Klicken auf die Schaltfläche pcmd das Widget pwgt aus:
connect(pcmd, &QPushButton::clicked, [=](){pwgt->hide();});

Die in Abb. 2.1 gezeigten Programmfenster demonstrieren den Signal-Slot-Mechanismus in Aktion. Für dieses Beispiel wird eine Anwendung erstellt (Listings 2.8–2.10), in deren erstem Fenster (Abb. 2.1, rechts) sich eine Schaltfläche befindet und im zweiten (Abb. 2.1, links) ein Label-Widget. Beim Klicken im rechten Fenster auf die Schaltfläche ADD (Hinzufügen) wird der im linken Fenster angezeigte Wert um eins erhöht. Sobald der Wert fünf erreicht, wird die Anwendung beendet.

Qt6 Signal-Slot-Beispiel: Zählerprogramm mit Schaltfläche ADD und Label-Anzeige
Abb. 2.1. Zählerprogramm: Demonstration der Funktionsweise des Signal-Slot-Mechanismus

Listing 2.8. Hauptprogramm der Anwendung (Datei main.cpp)

#include <QApplication>
#include <QLabel>
#include <QPushButton>
#include "Counter.h"
 
int main (int argc, char** argv)
{
    QApplication app(argc, argv);
 
    QLabel      lbl("0");
    QPushButton cmd("ADD");
    Counter     counter;
 
    lbl.show();
    cmd.show();
 
    QObject::connect(&cmd, &QPushButton::clicked,
 &counter, &Counter::slotInc
                    );
 
    QObject::connect(&counter, &Counter::counterChanged,
                     &lbl, qOverload<int>(&QLabel::setNum)
                    );
 
    QObject::connect(&counter, &Counter::goodbye,
 &app, &QApplication::quit
                    );
 
    return app.exec();
}

Im Hauptprogramm der Anwendung (Listing 2.8) wird ein Label-Objekt lbl, eine klickbare Schaltfläche cmd und ein Zählerobjekt counter erstellt (dessen Beschreibung in den Listings 2.9–2.10 zu finden ist). Dann wird das Signal clicked() mit dem Slot slotInc() verbunden. Bei jedem Klick auf die Schaltfläche wird die Methode slotInc() aufgerufen, die den Zählerwert um 1 erhöht. Sie muss in der Lage sein, über solche Änderungen zu informieren, damit das Label-Element immer nur den aktuellen Wert anzeigt. Dazu wird das Signal counterChanged(int), das den aktuellen Zählerwert im Parameter übergibt, mit dem Slot setNum(int) verbunden, der diesen Wert empfangen kann.

Hinweis

In Qt6 muss für überladene Methoden (wie QLabel::setNum) die Funktion qOverload() verwendet werden, um explizit anzugeben, welche Version der Methode verwendet werden soll. Im obigen Beispiel haben wir qOverload() für die Methode setNum() verwendet, die einen int akzeptiert.

Achten Sie auf die Typübereinstimmung zwischen Signalen und Slots!

Bei der Verbindung von Signalen mit Slots, die Werte übergeben, ist es wichtig, auf die Übereinstimmung ihrer Typen zu achten. Beispielsweise sollte ein Signal, das einen int-Wert im Parameter übergibt, nicht mit einem Slot verbunden werden, der einen QString erwartet:
connect(pobj1, &SenderClass::sig, pobj2, &ReceiverClass::sltInt); // RICHTIG!
connect(pobj1, &SenderClass::sig, pobj2, &ReceiverClass::sltString); //
FALSCH!

Im Slot können vom Signal übergebene Werte ignoriert werden

Es ist auch wichtig zu verstehen, dass im Slot die vom Signal übergebenen Werte ignoriert werden können. So können Sie beispielsweise ein Signal, das einen int-Wert sendet, mit einem Slot verbinden, der keine Parameter akzeptiert. Aber es ist nicht möglich, umgekehrt zu verfahren und ein Signal, das keine Werte sendet, mit einem Slot zu verbinden, der Parameter erwartet:
connect(pobj1, &SenderClass::sigInt, pobj2, &ReceiverClass::slt); // MÖGLICH!
connect(pobj1, &SenderClass::sig, pobj2, &ReceiverClass::sltInt); //
NICHT MÖGLICH!

Geben Sie nicht zusammen mit dem Typ einen Variablennamen an!

Beachten Sie, dass als Argument nur der Typ int ohne Angabe eines Variablennamens verwendet wird. Wenn Sie zusammen mit dem Typ einen Variablennamen angeben, funktioniert die Verbindung nicht. Dabei kann die Fehlersuche viel Zeit in Anspruch nehmen:
connect(pobj1, SIGNAL(sig(int n)), pobj2, SLOT(slt(int n))); // FEHLER!!

Schließlich wird das Signal goodbye(), das das Ende der Zählerarbeit symbolisiert, mit dem Slot quit() des Anwendungsobjekts verbunden, der – nach dem fünften Klicken auf die Schaltfläche ADD – das Beenden der Anwendung durchführt. Unsere Anwendung besteht aus zwei Fenstern, und nach dem Schließen des letzten Fensters wird ihre Arbeit automatisch beendet.

Listing 2.9. Zähler (Datei Counter.h)

#pragma once
 
#include <QObject>
 
// ======================================================================
class Counter : public QObject {
    Q_OBJECT
private:
    int m_nValue;
 
public:
    Counter();
 
public slots:
    void slotInc();
 
signals:
    void goodbye       (   );
    void counterChanged(int);
};

Wie aus Listing 2.9 ersichtlich, enthält die Definition der Zählerklasse zwei Signale: goodbye(), das das Ende der Zählerarbeit meldet, und counterChanged(int), das den aktuellen Zählerwert überträgt, sowie den Slot slotInc(), der den Zählerwert um eins erhöht.

Listing 2.10. Zähler (Datei counter.cpp)

#include "Counter.h"
 
// ----------------------------------------------------------------------
Counter::Counter() : QObject()
                   , m_nValue(0)
{
}
// ----------------------------------------------------------------------
void Counter::slotInc()
{
    emit counterChanged(++m_nValue);
 
    if (m_nValue == 5) {
        emit goodbye();
    }
}

In Listing 2.10 sendet die Slot-Methode slotInc() zwei Signale: counterChanged() und goodbye(). Das Signal goodbye() wird gesendet, wenn der Wert des Attributs m_nValue gleich 5 ist.

Ein Slot ohne Parameter kann mit einem Signal verbunden werden, das Parameter hat. Dies ist praktisch, wenn Signale mehr Informationen liefern, als für das empfangende Objekt erforderlich sind. In diesem Fall können die Parameter im Slot weggelassen werden:

MyClass::MyClass() : QObject()
{
    connect(pSender, &SenderClass::signalMethod, this, &MyClass::mySignal);
}

Wenn Sie nicht sicher sind, ob der Signalparameter in Zukunft benötigt wird, ist es besser, den Slot mit Parameter zu definieren und ihn innerhalb des Slots zu ignorieren. Dann müssen Sie später, wenn die Notwendigkeit entsteht, den Slot-Prototyp nicht ändern.

Trennen von Objekten

Wenn es die Möglichkeit gibt, Objekte zu verbinden, muss es auch die Möglichkeit geben, sie zu trennen. In Qt werden bei der Zerstörung eines Objekts alle damit verbundenen Verbindungen automatisch zerstört, aber in seltenen Fällen kann es notwendig sein, diese Verbindungen „manuell” zu zerstören. Dafür existiert die statische Methode disconnect(), deren Parameter analog zu den Parametern der statischen Methode connect() sind. Für die moderne Verbindungssyntax:

QObject::disconnect(sender, &SenderClass::signalMethod,
                    receiver, &ReceiverClass::slotMethod);

Für die klassische Syntax mit SIGNAL() und SLOT():

QObject::disconnect(sender, SIGNAL(signalMethod()),
                    receiver, SLOT(slotMethod()));

Das folgende Beispiel zeigt, wie das Trennen von Objekten in einem Programm durchgeführt werden kann:

void main()
{
    ...
    QObject::disconnect(pSender, SIGNAL(signalMethod()),
                        pReceiver, SLOT(slotMethod())
                       );
    ...
}

Es gibt auch verkürzte Varianten der Methode disconnect():

  • Alle Verbindungen mit dem angegebenen Signal trennen:
sender->disconnect(SIGNAL(signalMethod()));
  • Alle Verbindungen mit dem angegebenen Empfängerobjekt trennen:
sender->disconnect(receiver);
  • Alle Verbindungen trennen:
sender->disconnect();

Neuabbildung von Signalen

Wenn es notwendig ist, die Anzahl der Slot-Methoden in einer Klasse zu reduzieren und auf verschiedene Signale in einem Slot zu reagieren, sollte die Klasse QSignalMapper verwendet werden. Mit ihrer Hilfe können Signale neu abgebildet werden, sodass Werte vom Typ int, QString oder QWidget an den Slot gesendet werden. Betrachten wir diesen Mechanismus am Beispiel der Verwendung von QString-Werten. Nehmen wir an, wir haben in unserem Programm zwei Schaltflächen: Beim Klicken auf die erste Schaltfläche müssen wir die Nachricht „Button1 Action” anzeigen, und beim Klicken auf die zweite – „Button2 Action”. Wir könnten natürlich in der Klasse zwei verschiedene Slots implementieren, die mit den clicked()-Signalen jeder der beiden Schaltflächen verbunden wären und jeweils ihre eigene Nachricht ausgeben würden. Aber wir gehen anders vor und verwenden dazu die Klasse QSignalMapper (Listing 2.11).

Listing 2.11. Beispiel für die Neuabbildung von Signalen (klassischer Ansatz)

MyClass::MyClass(QWidget* pwgt)
{
...
    auto* psigMapper = new QSignalMapper(this);
 connect(psigMapper, SIGNAL(mapped(const QString&)),
            this,       SLOT(slotShowAction(const QString&))
           );
 
    auto* pcmd1 = new QPushButton(“Button1”);
 connect(pcmd1, SIGNAL(clicked()), psigMapper, SLOT(map()));
    psigMapper->setMapping(pcmd1, “Button1 Action”);
 
    auto* pcmd2 = new QPushButton(“Button2”);
 connect(pcmd2, SIGNAL(clicked()), psigMapper, SLOT(map()));
    psigMapper->setMapping(pcmd2, “Button2 Action”);
...
}
void MyClass::slotShowAction(const QString& str)
{
    qDebug() << str;
}

In Listing 2.11 erstellen wir ein Objekt der Klasse QSignalMapper und verbinden sein Signal mapped() mit dem einzigen Slot slotShowAction(), der QString-Objekte empfängt. Die Klasse QSignalMapper stellt den Slot map() bereit, mit dem jedes Objekt verbunden werden muss, dessen Signal neu abgebildet werden soll. Mit der Methode QSignalMapper::setMapping() legen wir den konkreten Wert fest, der beim Empfang des Signals an den Slot gesendet werden soll – in unserem Fall des Signals clicked().

Das war’s… Es bleibt nur anzumerken, dass Signalneuabbildungen nicht übermäßig verwendet werden sollten, da ein übermäßiger Gebrauch dieser Konstruktionen die Lesbarkeit des Programmcodes erheblich verringern kann.

In Qt6 ist die Klasse QSignalMapper durch die Möglichkeit der Verwendung von Lambda-Ausdrücken praktisch veraltet. Dennoch kann dieser Mechanismus nützlich sein, um die Kompatibilität mit älterem Code zu gewährleisten. Wie das Beispiel aus Listing 2.11 mit Lambda-Ausdrücken aussehen könnte (moderner Ansatz in Qt6), zeigt Listing 2.12.

Listing 2.12. Beispiel für die Neuabbildung von Signalen (moderner Ansatz)

MyClass::MyClass(QWidget* pwgt)
{
    …
    auto* pcmd1 = new QPushButton("Button1");
    connect(pcmd1, &QPushButton::clicked, [=]() {
        slotShowAction("Button1 Action");
    });
 
    auto* pcmd2 = new QPushButton("Button2");
    connect(pcmd2, &QPushButton::clicked, [=]() {
        slotShowAction("Button2 Action");
    });
    …
}
 
void MyClass::slotShowAction(const QString& str)
{
    qDebug() << str;
}

Organisation von Objekthierarchien

Die Organisation von Objekten in Hierarchien befreit den Entwickler von der Notwendigkeit, sich selbst um die Speicherfreigabe der erstellten Objekte zu kümmern.

Der Konstruktor der Klasse QObject sieht wie folgt aus:

QObject(QObject* pobj = nullptr);

In seinem Parameter wird ein Zeiger auf ein anderes Objekt der Klasse QObject oder einer davon abgeleiteten Klasse übergeben. Dank dieses Parameters besteht die Möglichkeit, Objekthierarchien zu erstellen. Er stellt einen Zeiger auf das Elternobjekt dar. Wenn im ersten Parameter ein Wert gleich Null übergeben wird oder nichts übergeben wird, bedeutet dies, dass das erstellte Objekt kein Elternobjekt hat und es ein Objekt der obersten Ebene sein wird, das sich an der Spitze der Objekthierarchie befindet. Das Elternobjekt wird im Konstruktor bei der Objekterstellung festgelegt, kann aber später jederzeit während der Programmausführung mit der Methode setParent() auf ein anderes geändert werden.

Erstellte Objekte haben standardmäßig keinen Namen. Mit der Methode setObjectName() kann einem Objekt ein Name zugewiesen werden. Der Objektname hat keine besondere Bedeutung, kann aber beim Debuggen des Programms nützlich sein. Um den Objektnamen herauszufinden, kann die Methode objectName() aufgerufen werden, wie in Listing 2.13 gezeigt.

Listing 2.13. Beispiel für die Erstellung einer Objekthierarchie

auto* pobj1 = new QObject;
auto* pobj2 = new QObject(pobj1);
auto* pobj4 = new QObject(pobj2);
auto* pobj3 = new QObject(pobj1);
pobj2->setObjectName("the first child of pobj1");
pobj3->setObjectName("the second child of pobj1");
pobj4->setObjectName("the first child of pobj2");

In der ersten Zeile von Listing 2.13 wird ein Objekt der obersten Ebene erstellt (ein Objekt ohne Elternobjekt). Bei der Erstellung des Objekts pobj2 wird in seinem Konstruktor ein Zeiger auf das Objekt pobj1 als Elternobjekt übergeben. Das Objekt pobj3 hat als Elternobjekt pobj1, und das Objekt pobj4 hat als Elternobjekt pobj2. Die resultierende Objekthierarchie ist in Abb. 2.2 dargestellt.

Schema einer Qt-Objekthierarchie mit QObject-Eltern- und Kindobjekten
Abb. 2.2. Schema der resultierenden Objekthierarchie

Bei der Zerstörung des erstellten Objekts (beim Aufruf seines Destruktors) werden alle damit verbundenen Kindobjekte automatisch zerstört. Diese Eigenschaft der rekursiven Objektzerstörung vereinfacht die Programmierung erheblich, da man sich nicht um die Freigabe von Speicherressourcen kümmern muss. Deshalb ist es notwendig, Objekte, und insbesondere Objekte, die nicht auf der obersten Ebene liegen, dynamisch mit dem Operator new zu erstellen, da sonst das Löschen eines Objekts zu einem Laufzeitfehler führt.

Alle Objekte müssen dynamisch im Speicher erstellt werden!

Einer der häufigsten Fehler von C++-Entwicklern bei der Programmierung mit der Qt-Bibliothek ist die eigenständige Kontrolle des Speicherallokations-/Freigabeprozesses für Objekte und die nicht-dynamische Erstellung von Steuerelementen. Bei der Programmierung mit Qt ist es wichtig zu beachten, dass Objekte mit einem Elternobjekt (parent) dynamisch im Speicher mit dem Operator new erstellt werden müssen. Eine Ausnahme können Objekte der obersten Ebene (ohne Elternobjekte) oder temporäre Objekte in der Funktion main() bilden, die während der gesamten Laufzeit des Programms existieren. Beispielsweise werden in Listing 2.8 die Objekte QLabel, QPushButton und Counter auf dem Stack erstellt, da sie keine Elternobjekte haben und während der gesamten Programmlaufzeit existieren.

Zur Ermittlung von Informationen über die Objekthierarchie existieren zwei Methoden: parent() und children(). Mit der Methode parent() kann das Elternobjekt bestimmt werden. Gemäß Abb. 2.2 gibt der Aufruf pobj2->parent() einen Zeiger auf das Objekt obj1 zurück. Für Objekte der obersten Ebene gibt diese Methode den Wert nullptr zurück. Um alle Elternobjektnamen eines beliebigen Objekts auf der Konsole auszugeben, kann wie in Listing 2.14 gezeigt vorgegangen werden (wir tun dies für das Objekt pobj4 aus Listing 2.13).

Listing 2.14. Ausgabe der Namen der Elternobjekte

for (QObject* pobj = pobj4; pobj; pobj = pobj->parent()) {
    qDebug() << pobj->objectName();
}

Und auf dem Bildschirm sehen Sie:

the first child of pobj2
the first child of pobj1

Die Methode children() gibt einen konstanten Zeiger auf die Liste der Kindobjekte zurück. Für das vorherige Beispiel (siehe Listing 2.13 und Abb. 2.2) gibt die Methode pobj1->children() einen Zeiger auf die Liste QObjectList zurück, die zwei Elemente enthält: die Zeiger pobj2 und pobj3.

Die Suche nach einem bestimmten Kindobjekt kann mit der Methode findChild() durchgeführt werden. Im Parameter dieser Methode muss der Name des gesuchten Objekts übergeben werden. Beispielsweise gibt der folgende Aufruf einen Zeiger auf das Objekt pobj4 zurück:

QObject* pobj = pobj1->findChild<QObject*>("the first child of pobj2");

Für eine erweiterte Suche existiert die Methode findChildren(), die eine Liste von Objektzeigern zurückgibt. Alle Parameter der Methode sind optional – es kann entweder ein Namensstring oder ein regulärer Ausdruck übergeben werden (siehe Kapitel 4), und ein Aufruf der Methode ohne Parameter führt dazu, dass sie eine Liste von Zeigern auf alle Kindobjekte zurückgibt. Die Suche erfolgt rekursiv. Der folgende Aufruf gibt eine Liste von Zeigern auf alle Objekte zurück, deren Name mit den Buchstaben „th” beginnt; in unserem Fall sind es drei:

QList<QObject*> plist = pobj1->findChildren<QObject*>(QRegExp("th*"));

Um alle Kindobjekte eines bestimmten Typs zurückzugeben, unabhängig von ihrem Namen, muss einfach kein Argument angegeben werden:

QList<QObject*> plist = pobj1->findChildren<QObject*>();

In unserem konkreten Fall gibt diese Funktion Zeiger auf die Objekte pobj2, pobj3 und pobj4 zurück.

Für das Debugging des Programms ist die Methode dumpObjectInfo() nützlich, die folgende Informationen über das Objekt anzeigt:

  • Objektname.
  • Klasse, von der das Objekt erstellt wurde.
  • Signal-Slot-Verbindungen.

All diese Informationen werden an den Standardausgabestrom stdout gesendet.

Beim Debugging kann auch die Methode dumpObjectTree() verwendet werden, die zur Anzeige der Kindobjekte in Form einer Hierarchie dient. Beispielsweise zeigt der Aufruf von dumpObjectTree() für unser Objekt pobj1 aus Listing 2.13:

QObject::
    QObject::the first child of pobj1
        QObject::the first child of pobj2
    QObject::the second child of pobj1

Metaobjekt-Informationen

Jedes Objekt, das von der Klasse QObject oder einer davon abgeleiteten Klasse erstellt wurde, verfügt über eine Datenstruktur, die Metaobjekt-Informationen genannt wird (Klasse QMetaObject). In ihr werden Informationen über Signale, Slots (einschließlich Zeiger auf sie), über die Klasse selbst und über die Vererbung gespeichert. Der Zugriff auf diese Informationen kann über die Methode QObject::metaObject() erfolgen. Um beispielsweise den Namen der Klasse eines Objekts herauszufinden, von der es erstellt wurde, kann wie folgt vorgegangen werden:

qDebug() << pobj1->metaObject()->className();

Und um den Klassennamen mit einem bekannten zu vergleichen, kann so vorgegangen werden:

if (pobj1->metaObject()->className() == "MyClass") {
    // Einige Aktionen ausführen
}

Zur Ermittlung von Informationen über die Klassenvererbung existiert die Methode inherits(const char*), die direkt in der Klasse QObject definiert ist und den Wert true zurückgibt, wenn die Objektklasse von der in dieser Methode angegebenen Klasse geerbt wurde oder von dieser Klasse erstellt wurde, andernfalls gibt die Methode den Wert false zurück. Beispiel:

if(pobj->inherits("QWidget")) {
    auto* pwgt = static_cast<QWidget*>(pobj);
 // Einige Aktionen mit pwgt ausführen
}

Die Metaobjekt-Informationen werden auch von der Typumwandlungsoperation qobject_cast verwendet. So kann mit der Methode inherits() das Beispiel geändert werden:

auto* pwgt = qobject_cast<QWidget*>(pobj);
if(pwgt) {
    // Einige Aktionen mit pwgt ausführen
}

Zu den Metaobjekt-Informationen gehört auch die Methode tr(), die für die Internationalisierung von Programmen bestimmt ist. Die Internationalisierung ist ausführlicher in Kapitel 30 beschrieben.

Zusammenfassung

In diesem Kapitel haben wir das Wesen von Signalen und Slots kennengelernt. Dies ist eine würdige Alternative zu den bisher verwendeten Mitteln zur Implementierung von Verbindungen zwischen Komponenten der grafischen Benutzeroberfläche, die es ermöglicht, die Lesbarkeit des Programmcodes erheblich zu verbessern.

Signale und Slots können miteinander verbunden werden, wobei ein Signal mit einer großen Anzahl von Slots verbunden werden kann. Ein Slot kann seinerseits auch mit vielen Signalen verbunden werden. Falls ein Slot nichts anderes tut, als das empfangene Signal weiterzuleiten, kann man ganz auf ihn verzichten und einfach Signale miteinander verbinden. Signalmethoden müssen in der Klassendefinition mit dem speziellen Wort signals gekennzeichnet werden, und Slots mit dem Wort slots. Dabei sind Slots gewöhnliche Methoden der Sprache C++, und in ihrer Definition können die Modifikatoren public, protected, private vorkommen. Die Code-Implementierung für Signale übernimmt MOC. Das Senden eines Signals erfolgt mit dem Schlüsselwort emit. Die Klasse, die Signale und Slots enthält, muss von der Klasse QObject oder einer davon abgeleiteten Klasse geerbt werden. Signal-Slot-Verbindungen können jederzeit mit der Methode disconnect() entfernt (getrennt) werden, aber dies ist äußerst selten erforderlich, da bei der Zerstörung eines Objekts automatisch alle seine Verbindungen zerstört werden. Die Verbindung von Objekten erfolgt mit der statischen Methode QObject::connect().

QObject ist eine Klasse, die im Wesentlichen die Grundklasse bei der Programmierung mit Qt ist. Der Konstruktor der Klasse QObject hat einen Parameter – einen Zeiger auf das Elternobjekt:

QObject(QObject* pobj = nullptr);

Zur Zuweisung eines Namens an ein Objekt wird die separate Methode setObjectName() verwendet. Objekteigenschaften sind wichtig, da sie es ermöglichen, während der Programmausführung Informationen über die Klasse und das Objekt zu erhalten. Objekte mit einem Elternobjekt (parent) müssen dynamisch im Speicher mit dem Operator new erstellt werden. Eine Ausnahme können Objekte der obersten Ebene (ohne Elternobjekte) oder temporäre Objekte in der Funktion main() bilden, die während der gesamten Laufzeit des Programms existieren.

Da das Konzept von Signalen und Slots sowie die Vererbungsinformationen nicht mit den Mitteln der Sprache C++ selbst realisiert werden konnten, wurde ein spezieller Präprozessor namens MOC (Metaobjekt-Compiler) geschaffen, dessen Aufgabe es ist, für Header-Dateien zusätzliche CPP-Dateien zu erstellen, die kompiliert und deren Objektcode dem ausführbaren Programmcode hinzugefügt werden muss. Damit MOC Klassen erkennen kann, die eine solche Verarbeitung benötigen, muss eine solche Klasse das Makro Q_OBJECT enthalten.

In Qt6 wird die moderne Syntax für die Verbindung von Signalen und Slots unter Verwendung von Methodenzeigern und Lambda-Ausdrücken bereitgestellt, die die bevorzugte Methode zur Erstellung von Verbindungen darstellt.

* * *

Erschließen Sie das volle Potenzial dieses Kapitels! Erhalten Sie die dafür vorbereiteten Zusatzmaterialien. Stellen Sie eine Frage, teilen Sie Ihre Erfahrungen oder schließen Sie sich einfach der Diskussion über den folgenden Link an: https://qt-book.com/02-69-de/

Leave a Reply

Your email address will not be published. Required fields are marked *