Auf dieser Seite

Bindbare Eigenschaften

Hier wird gezeigt, wie die Verwendung von bindbaren Eigenschaften Ihren C++-Code vereinfachen kann.

In diesem Beispiel zeigen wir zwei Ansätze zur Darstellung der Beziehungen zwischen verschiedenen, voneinander abhängigen Objekten: den auf Signal/Slot-Verbindungen basierenden und den auf bindbaren Eigenschaften basierenden Ansatz. Zu diesem Zweck betrachten wir ein Abonnement-Servicemodell zur Berechnung der Abonnementkosten.

Anmeldeformular mit Angaben zu Alter, Abonnementdauer, Länderflaggen und Preis pro Monat

Modellierung eines Abonnementsystems mit dem Signal-Slot-Ansatz

Betrachten wir zunächst die übliche Implementierung vor Qt 6. Zur Modellierung des Abonnementdienstes wird die Klasse „ Subscription “ verwendet:

class Subscription : public QObject
{
    Q_OBJECT
public:
    enum Duration { Monthly = 1, Quarterly = 3, Yearly = 12 };

    Subscription(User *user);

    void calculatePrice();
    int price() const { return m_price; }

    Duration duration() const { return m_duration; }
    void setDuration(Duration newDuration);

    bool isValid() const { return m_isValid; }
    void updateValidity();

signals:
    void priceChanged();
    void durationChanged();
    void isValidChanged();

private:
    double calculateDiscount() const;
    int basePrice() const;

    QPointer<User> m_user;
    Duration m_duration = Monthly;
    int m_price = 0;
    bool m_isValid = false;
};

Sie speichert die Informationen zum Abonnement und stellt entsprechende Getter, Setter sowie Notifier-Signale bereit, um die Listener über Änderungen der Abonnementinformationen zu informieren. Außerdem verwaltet sie einen Zeiger auf eine Instanz der Klasse ` User `.

Der Preis des Abonnements wird auf der Grundlage der Laufzeit des Abonnements berechnet:

double Subscription::calculateDiscount() const
{
    switch (m_duration) {
    case Monthly:
        return 1;
    case Quarterly:
        return 0.9;
    case Yearly:
        return 0.6;
    }
    Q_ASSERT(false);
    return -1;
}

sowie nach dem Standort des Nutzers:

int Subscription::basePrice() const
{
    if (m_user->country() == User::Country::AnyTerritory)
        return 0;

    return (m_user->country() == User::Country::Norway) ? 100 : 80;
}

Wenn sich der Preis ändert, wird das Signal „ priceChanged() “ ausgelöst, um die Abonnenten über die Änderung zu informieren:

void Subscription::calculatePrice()
{
    const auto oldPrice = m_price;

    m_price = qRound(calculateDiscount() * int(m_duration) * basePrice());
    if (m_price != oldPrice)
        emit priceChanged();
}

Ebenso wird das Signal „ durationChanged() “ ausgelöst, wenn sich die Laufzeit des Abonnements ändert.

void Subscription::setDuration(Duration newDuration)
{
    if (newDuration != m_duration) {
        m_duration = newDuration;
        calculatePrice();
        emit durationChanged();
    }
}

Hinweis: Beide Methoden müssen prüfen, ob sich die Daten tatsächlich geändert haben, und erst dann die Signale auslösen. „ setDuration() “ muss zudem den Preis neu berechnen, wenn sich die Laufzeit geändert hat.

„ Subscription “ ist nur gültig, wenn der Benutzer ein gültiges Land und ein gültiges Alter angegeben hat; daher wird die Gültigkeit wie folgt aktualisiert:

void Subscription::updateValidity()
{
    bool isValid = m_isValid;
    m_isValid = m_user->country() != User::Country::AnyTerritory && m_user->age() > 12;

    if (m_isValid != isValid)
        emit isValidChanged();
}

Die Klasse „ User “ ist einfach aufgebaut: Sie speichert Land und Alter des Benutzers und stellt die entsprechenden Getter, Setter und Notifier-Signale bereit:

class User : public QObject
{
    Q_OBJECT

public:
    using Country = QLocale::Territory;

public:
    Country country() const { return m_country; }
    void setCountry(Country country);

    int age() const { return m_age; }
    void setAge(int age);

signals:
    void countryChanged();
    void ageChanged();

private:
    Country m_country { QLocale::AnyTerritory };
    int m_age { 0 };
};

void User::setCountry(Country country)
{
    if (m_country != country) {
        m_country = country;
        emit countryChanged();
    }
}

void User::setAge(int age)
{
    if (m_age != age) {
        m_age = age;
        emit ageChanged();
    }
}

In der Funktion „ main() “ initialisieren wir Instanzen von „ User “ und „ Subscription “:

    User user;
    Subscription subscription(&user);

Außerdem nehmen wir die entsprechenden Signal-Slot-Verbindungen vor, um die Daten von „ user “ und „ subscription “ zu aktualisieren, wenn sich UI-Elemente ändern. Das ist unkompliziert, daher überspringen wir diesen Teil.

Als Nächstes stellen wir eine Verbindung zu Subscription::priceChanged() her, um den Preis in der Benutzeroberfläche zu aktualisieren, sobald sich der Preis ändert.

    QObject::connect(&subscription, &Subscription::priceChanged, priceDisplay, [&] {
        QLocale lc{QLocale::AnyLanguage, user.country()};
        priceDisplay->setText(lc.toCurrencyString(subscription.price() / subscription.duration()));
    });

Außerdem verbinden wir uns mit „ Subscription::isValidChanged() “, um die Preisanzeige zu deaktivieren, falls das Abonnement nicht gültig ist.

    QObject::connect(&subscription, &Subscription::isValidChanged, priceDisplay, [&] {
        priceDisplay->setEnabled(subscription.isValid());
    });

Da der Abonnementpreis und die Gültigkeit auch vom Land und vom Alter des Nutzers abhängen, müssen wir außerdem eine Verbindung zu den Signalen „ User::countryChanged() “ und „ User::ageChanged() “ herstellen und „ subscription “ entsprechend aktualisieren.

    QObject::connect(&user, &User::countryChanged, &subscription, [&] {
        subscription.calculatePrice();
        subscription.updateValidity();
    });

    QObject::connect(&user, &User::ageChanged, &subscription, [&] {
        subscription.updateValidity();
    });

Das funktioniert zwar, aber es gibt einige Probleme:

  • Es gibt viel Boilerplate-Code für die Signal-Slot-Verbindungen, um Änderungen sowohl bei user als auch bei subscription korrekt nachzuverfolgen. Wenn sich eine der Abhängigkeiten des Preises ändert, müssen wir daran denken, die entsprechenden Notifier-Signale auszulösen, den Preis neu zu berechnen und ihn in der Benutzeroberfläche zu aktualisieren.
  • Sollten in Zukunft weitere Abhängigkeiten für die Preisberechnung hinzukommen, müssen wir weitere Signal-Slot-Verbindungen hinzufügen und sicherstellen, dass alle Abhängigkeiten bei jeder Änderung ordnungsgemäß aktualisiert werden. Die Gesamtkomplexität wird zunehmen, und der Code wird schwieriger zu warten sein.
  • Die Klassen „ Subscription “ und „ User “ sind auf das Metaobjektsystem angewiesen, um den Signal-Slot-Mechanismus nutzen zu können.

Gibt es eine bessere Lösung?

Modellierung eines Abonnementsystems mit bindbaren Eigenschaften

Sehen wir uns nun an, wie die Qt-Bindable-Properties helfen können, dasselbe Problem zu lösen. Werfen wir zunächst einen Blick auf die Klasse ` BindableSubscription `, die der Klasse ` Subscription ` ähnelt, jedoch mithilfe von Bindable-Properties implementiert ist:

class BindableSubscription
{
public:
    enum Duration { Monthly = 1, Quarterly = 3, Yearly = 12 };

    BindableSubscription(BindableUser *user);
    BindableSubscription(const BindableSubscription &) = delete;

    int price() const { return m_price; }
    QBindable<int> bindablePrice() { return &m_price; }

    Duration duration() const { return m_duration; }
    void setDuration(Duration newDuration);
    QBindable<Duration> bindableDuration() { return &m_duration; }

    bool isValid() const { return m_isValid; }
    QBindable<bool> bindableIsValid() { return &m_isValid; }

private:
    double calculateDiscount() const;
    int basePrice() const;

    BindableUser *m_user;
    QProperty<Duration> m_duration { Monthly };
    QProperty<int> m_price { 0 };
    QProperty<bool> m_isValid { false };
};

Der erste Unterschied, der uns auffällt, ist, dass die Datenfelder nun in „ QProperty “-Klassen eingeschlossen sind und die Notifier-Signale (und damit auch die Abhängigkeit vom Metaobjektsystem) weggefallen sind; stattdessen wurden neue Methoden hinzugefügt, die für jeden „ QProperty “ ein „ QBindable “ zurückgeben. Die Methoden „ calculatePrice() “ und „ updateValidty() “ wurden ebenfalls entfernt. Wir werden weiter unten sehen, warum sie nicht mehr benötigt werden.

Die Klasse „ BindableUser “ unterscheidet sich in ähnlicher Weise von der Klasse „ User “:

class BindableUser
{
public:
    using Country = QLocale::Territory;

public:
    BindableUser() = default;
    BindableUser(const BindableUser &) = delete;

    Country country() const { return m_country; }
    void setCountry(Country country);
    QBindable<Country> bindableCountry() { return &m_country; }

    int age() const { return m_age; }
    void setAge(int age);
    QBindable<int> bindableAge() { return &m_age; }

private:
    QProperty<Country> m_country { QLocale::AnyTerritory };
    QProperty<int> m_age { 0 };
};

Der zweite Unterschied liegt in der Implementierung dieser Klassen. Zunächst einmal werden die Abhängigkeiten zwischen „ subscription “ und „ user “ nun über Bindungsausdrücke verfolgt:

BindableSubscription::BindableSubscription(BindableUser *user) : m_user(user)
{
    Q_ASSERT(user);

    m_price.setBinding(
            [this] { return qRound(calculateDiscount() * int(m_duration) * basePrice()); });

    m_isValid.setBinding([this] {
        return m_user->country() != BindableUser::Country::AnyCountry && m_user->age() > 12;
    });
}

Im Hintergrund verfolgen die bindbaren Eigenschaften die Änderungen der Abhängigkeiten und aktualisieren den Wert der Eigenschaft, sobald eine Änderung erkannt wird. Wenn sich also beispielsweise das Land oder das Alter des Benutzers ändert, werden der Preis und die Gültigkeitsdauer des Abonnements automatisch aktualisiert.

Ein weiterer Unterschied besteht darin, dass die Setter nun sehr einfach sind:

void BindableSubscription::setDuration(Duration newDuration)
{
    m_duration = newDuration;
}

void BindableUser::setCountry(Country country)
{
    m_country = country;
}

void BindableUser::setAge(int age)
{
    m_age = age;
}

Es ist nicht notwendig, innerhalb der Setter zu prüfen, ob sich der Wert der Eigenschaft tatsächlich geändert hat – „ QProperty “ übernimmt dies bereits. Die abhängigen Eigenschaften werden nur dann über die Änderung benachrichtigt, wenn sich der Wert tatsächlich geändert hat.

Auch der Code zur Aktualisierung der Preisangaben in der Benutzeroberfläche wurde vereinfacht:

    auto priceChangeHandler = subscription.bindablePrice().subscribe([&] {
        QLocale lc{QLocale::AnyLanguage, user.country()};
        priceDisplay->setText(lc.toCurrencyString(subscription.price() / subscription.duration()));
    });

    auto priceValidHandler = subscription.bindableIsValid().subscribe([&] {
        priceDisplay->setEnabled(subscription.isValid());
    });

Wir abonnieren Änderungen über „ bindablePrice() “ und „ bindableIsValid() “ und aktualisieren die Preisanzeige entsprechend, sobald sich der Wert einer dieser Eigenschaften ändert. Die Abonnements bleiben so lange bestehen, wie die entsprechenden Handler aktiv sind.

Beachten Sie außerdem, dass die Kopierkonstruktoren sowohl von „ BindableSubscription “ als auch von „ BindableUser “ deaktiviert sind, da nicht definiert ist, was beim Kopieren mit ihren Bindungen geschehen soll.

Wie Sie sehen können, ist der Code nun wesentlich einfacher geworden, und die oben genannten Probleme sind gelöst:

  • Der Boilerplate-Code für die Signal-Slot-Verbindungen wurde entfernt, die Abhängigkeiten werden nun automatisch nachverfolgt.
  • Der Code ist leichter zu warten. Das Hinzufügen weiterer Abhängigkeiten in der Zukunft erfordert lediglich das Hinzufügen der entsprechenden bindbaren Eigenschaften und das Festlegen der Bindungsausdrücke, die die Beziehungen zueinander widerspiegeln.
  • Die Klassen „ Subscription “ und „ User “ sind nicht mehr vom Metaobjektsystem abhängig. Natürlich können Sie sie weiterhin für das Metaobjektsystem freigeben und bei Bedarf „ Q_PROPERTYs“ hinzufügen, um die Vorteile bindbarer Eigenschaften sowohl im „ C++ “- als auch im „ QML “-Code zu nutzen. Dazu können Sie die Klasse „ QObjectBindableProperty “ verwenden.

Beispielprojekt @ code.qt.io

© 2026 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.