Bindbare Eigenschaften

Zeigt, wie die Verwendung von bindbaren Eigenschaften Ihren C++-Code vereinfachen kann.

In diesem Beispiel werden zwei Ansätze für die Darstellung der Beziehungen zwischen verschiedenen Objekten, die voneinander abhängig sind, demonstriert: signal/slot connection-based und bindable property-based. Zu diesem Zweck werden wir ein Abonnementdienstmodell betrachten, um die Kosten für das Abonnement zu berechnen.

Modellierung eines Abonnementsystems mit 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 über das Abonnement und stellt entsprechende Getter, Setter und Notifier-Signale zur Verfügung, um die Zuhörer über Änderungen der Abonnementinformationen zu informieren. Außerdem hält sie einen Zeiger auf eine Instanz der Klasse User.

Der Preis des Abonnements wird auf der Grundlage der Dauer 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;
}

Und dem Standort des Benutzers:

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() ausgegeben, um die Zuhörer ü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() gesendet, wenn sich die Dauer 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 aussenden. setDuration() muss auch den Preis neu berechnen, wenn sich die Dauer geändert hat.

Subscription ist nur dann gültig, wenn der Benutzer ein gültiges Land und ein gültiges Alter 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: Sie speichert das Land und das 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);

Und stellen die richtigen Signal-Slot-Verbindungen her, um die Daten von user und subscription zu aktualisieren, wenn sich UI-Elemente ändern. Das ist ganz einfach, also überspringen wir diesen Teil.

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

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

Wir stellen auch eine Verbindung zu Subscription::isValidChanged() her, um die Preisanzeige zu deaktivieren, wenn das Abonnement ungültig ist.

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

Da der Preis und die Gültigkeit des Abonnements auch vom Land und Alter des Benutzers abhängen, müssen wir auch 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, aber es gibt einige Probleme:

  • Für die Signal-Slot-Verbindungen gibt es eine Menge Boilerplate-Code, um Änderungen an user und subscription korrekt zu verfolgen. Wenn sich eine der Abhängigkeiten des Preises ändert, müssen wir daran denken, die entsprechenden Melde-Signale auszusenden, den Preis neu zu berechnen und ihn in der Benutzeroberfläche zu aktualisieren.
  • Wenn 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 ordnungsgemäß aktualisiert werden, wenn sich eine von ihnen ändert. Die Gesamtkomplexität wird zunehmen, und der Code wird schwieriger zu pflegen sein.
  • Die Klassen Subscription und User sind auf das Metaobjektsystem angewiesen, um den Signal/Slot-Mechanismus nutzen zu können.

Können wir das besser machen?

Modellierung des Abonnementsystems mit bindefähigen Eigenschaften

Schauen wir uns nun an, wie die Qt Bindable Properties helfen können, das gleiche Problem zu lösen. Werfen wir zunächst einen Blick auf die Klasse BindableSubscription, die der Klasse Subscription ähnelt, aber mit bindbaren Eigenschaften 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, den wir feststellen können, ist, dass die Datenfelder nun in QProperty -Klassen verpackt sind, und dass die Notifier-Signale (und folglich die Abhängigkeit vom Metaobjektsystem) verschwunden sind, und stattdessen neue Methoden hinzugefügt wurden, die eine QBindable für jede QProperty zurückgeben. Die Methoden calculatePrice() und updateValidty() werden ebenfalls entfernt. Wir werden weiter unten sehen, warum sie nicht mehr benötigt werden.

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

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 jetzt ü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;
    });
}

Hinter den Kulissen verfolgen die bindbaren Eigenschaften die Änderungen der Abhängigkeiten und aktualisieren den Wert der Eigenschaft, sobald eine Änderung festgestellt wird. Wenn also zum Beispiel das Land oder das Alter des Benutzers geändert wird, werden der Preis und die Gültigkeit des Abonnements automatisch aktualisiert.

Ein weiterer Unterschied ist, dass die Setter jetzt trivial 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 mehr nötig, innerhalb der Setter zu prüfen, ob sich der Wert der Eigenschaft tatsächlich geändert hat, QProperty macht das bereits. Die abhängigen Eigenschaften werden nur dann über die Änderung benachrichtigt, wenn sich der Wert tatsächlich geändert hat.

Der Code für die Aktualisierung der Preisinformationen in der Benutzeroberfläche wurde ebenfalls 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 die Änderungen über bindablePrice() und bindableIsValid() und aktualisieren die Preisanzeige entsprechend, wenn eine dieser Eigenschaften den Wert ändert. Die Abonnements bleiben so lange bestehen, wie die entsprechenden Handler aktiv sind.

Beachten Sie auch, dass die Kopierkonstruktoren von BindableSubscription und BindableUser deaktiviert sind, da nicht definiert ist, was mit ihren Bindungen beim Kopieren geschehen soll.

Wie Sie sehen können, ist der Code viel 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 verfolgt.
  • Der Code ist leichter zu pflegen. Wenn in Zukunft weitere Abhängigkeiten hinzugefügt werden, müssen nur noch die entsprechenden bindbaren Eigenschaften hinzugefügt und die Bindungsausdrücke gesetzt werden, die die Beziehungen untereinander widerspiegeln.
  • Die Klassen Subscription und User sind nicht mehr vom Meta-Objektsystem abhängig. Natürlich können Sie sie immer noch dem Metaobjektsystem aussetzen und Q_PROPERTYhinzufügen, wenn Sie die Vorteile von bindbaren Eigenschaften sowohl im C++ als auch im QML Code nutzen wollen. Sie können dafür die Klasse QObjectBindableProperty verwenden.

Beispielprojekt @ code.qt.io

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