Propriétés liantes
Démontre comment l'utilisation de propriétés liables peut simplifier votre code C++.
Dans cet exemple, nous allons démontrer deux approches pour exprimer les relations entre différents objets dépendant les uns des autres : la connexion basée sur le signal/la fente et la propriété liante. À cette fin, nous considérerons un modèle de service d'abonnement pour calculer le coût de l'abonnement.

Modélisation d'un système d'abonnement à l'aide de l'approche signal/emplacement
Considérons tout d'abord l'implémentation pré-Qt 6 habituelle. La classe Subscription est utilisée pour modéliser le service d'abonnement :
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; };
Elle stocke les informations relatives à l'abonnement et fournit les getters, setters et signaux de notification correspondants pour informer les auditeurs des modifications apportées aux informations relatives à l'abonnement. Elle conserve également un pointeur sur une instance de la classe User.
Le prix de l'abonnement est calculé en fonction de la durée de l'abonnement :
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; }
et de la localisation de l'utilisateur :
int Subscription::basePrice() const { if (m_user->country() == User::Country::AnyTerritory) return 0; return (m_user->country() == User::Country::Norway) ? 100 : 80; }
Lorsque le prix change, le signal priceChanged() est émis pour informer les auditeurs du changement :
void Subscription::calculatePrice() { const auto oldPrice = m_price; m_price = qRound(calculateDiscount() * int(m_duration) * basePrice()); if (m_price != oldPrice) emit priceChanged(); }
De même, lorsque la durée de l'abonnement change, le signal durationChanged() est émis.
void Subscription::setDuration(Duration newDuration) { if (newDuration != m_duration) { m_duration = newDuration; calculatePrice(); emit durationChanged(); } }
Remarque : les deux méthodes doivent vérifier si les données sont effectivement modifiées et n'émettre les signaux qu'ensuite. setDuration() doit également recalculer le prix lorsque la durée a changé.
Le site Subscription n'est valide que si l'utilisateur a un pays et un âge valides, de sorte que la validité est mise à jour de la manière suivante :
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(); }
La classe User est simple : elle stocke le pays et l'âge de l'utilisateur et fournit les getters, setters et signaux de notification correspondants :
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(); } }
Dans la fonction main(), nous initialisons les instances de User et Subscription:
User user;
Subscription subscription(&user);Et nous effectuons les connexions signal-slot appropriées pour mettre à jour les données user et subscription lorsque les éléments de l'interface utilisateur changent. C'est très simple, nous allons donc sauter cette partie.
Ensuite, nous nous connectons à Subscription::priceChanged() pour mettre à jour le prix dans l'interface utilisateur lorsque le prix change.
QObject::connect(&subscription, &Subscription::priceChanged, priceDisplay, [&] { QLocale lc{QLocale::AnyLanguage, user.country()}; priceDisplay->setText(lc.toCurrencyString(subscription.price() / subscription.duration())); });
Nous nous connectons également à Subscription::isValidChanged() pour désactiver l'affichage du prix si l'abonnement n'est pas valide.
QObject::connect(&subscription, &Subscription::isValidChanged, priceDisplay, [&] { priceDisplay->setEnabled(subscription.isValid()); });
Comme le prix et la validité de l'abonnement dépendent également du pays et de l'âge de l'utilisateur, nous devons également nous connecter aux signaux User::countryChanged() et User::ageChanged() et mettre à jour subscription en conséquence.
QObject::connect(&user, &User::countryChanged, &subscription, [&] { subscription.calculatePrice(); subscription.updateValidity(); }); QObject::connect(&user, &User::ageChanged, &subscription, [&] { subscription.updateValidity(); });
Cela fonctionne, mais il y a quelques problèmes :
- Il y a beaucoup de code standard pour les connexions entre les signaux afin de suivre correctement les modifications apportées à
useretsubscription. Si l'une des dépendances du prix change, nous devons nous rappeler d'émettre les signaux de notification correspondants, de recalculer le prix et de le mettre à jour dans l'interface utilisateur. - Si d'autres dépendances pour le calcul du prix sont ajoutées à l'avenir, nous devrons ajouter d'autres connexions de fente de signal et nous assurer que toutes les dépendances sont correctement mises à jour chaque fois que l'une d'entre elles change. La complexité globale augmentera et le code deviendra plus difficile à maintenir.
- Les classes
SubscriptionetUserdépendent du système de métaobjets pour pouvoir utiliser le mécanisme de signal/fente.
Peut-on faire mieux ?
Modélisation du système d'abonnement avec des propriétés liables
Voyons maintenant comment les propriétés liables de Qt peuvent aider à résoudre le même problème. Tout d'abord, jetons un coup d'œil à la classe BindableSubscription, qui est similaire à la classe Subscription, mais qui est implémentée à l'aide de propriétés liables :
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 }; };
La première différence que l'on peut noter est que les champs de données sont maintenant enveloppés dans des classes QProperty, et que les signaux de notification (et par conséquent la dépendance du système de métaobjets) ont disparu, et que de nouvelles méthodes renvoyant un QBindable pour chaque QProperty ont été ajoutées à la place. Les méthodes calculatePrice() et updateValidty() sont également supprimées. Nous verrons plus loin pourquoi elles ne sont plus nécessaires.
La classe BindableUser diffère de la classe User de la même manière :
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 }; };
La deuxième différence réside dans l'implémentation de ces classes. Tout d'abord, les dépendances entre subscription et user sont maintenant suivies via des expressions de liaison :
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; }); }
En coulisses, les propriétés liantes suivent les changements de dépendance et mettent à jour la valeur de la propriété chaque fois qu'un changement est détecté. Ainsi, si, par exemple, le pays ou l'âge de l'utilisateur est modifié, le prix et la validité de l'abonnement seront automatiquement mis à jour.
Une autre différence réside dans le fait que les paramètres sont désormais triviaux :
void BindableSubscription::setDuration(Duration newDuration) { m_duration = newDuration; } void BindableUser::setCountry(Country country) { m_country = country; } void BindableUser::setAge(int age) { m_age = age; }
Il n'est pas nécessaire de vérifier à l'intérieur des setters si la valeur de la propriété a réellement changé, QProperty le fait déjà. Les propriétés dépendantes ne seront informées du changement que si la valeur a effectivement changé.
Le code de mise à jour des informations sur le prix dans l'interface utilisateur est également simplifié :
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()); });
Nous nous abonnons aux changements via bindablePrice() et bindableIsValid() et mettons à jour l'affichage du prix en conséquence lorsque l'une de ces propriétés change de valeur. Les abonnements resteront actifs tant que les gestionnaires correspondants seront actifs.
Notez également que les constructeurs de copie de BindableSubscription et BindableUser sont désactivés, car il n'est pas défini ce qui doit se passer avec leurs bindings lors de la copie.
Comme vous pouvez le voir, le code est devenu beaucoup plus simple, et les problèmes mentionnés ci-dessus sont résolus :
- Le code de base pour les connexions de fentes de signaux est supprimé, les dépendances sont maintenant suivies automatiquement.
- Le code est plus facile à maintenir. Pour ajouter d'autres dépendances à l'avenir, il suffira d'ajouter les propriétés liables correspondantes et de définir les expressions de liaison qui reflètent les relations entre elles.
- Les classes
SubscriptionetUserne dépendent plus du système de métaobjets. Bien entendu, vous pouvez toujours les exposer au système de métaobjets et ajouter les classes Q_PROPERTYsi nécessaire, et bénéficier des avantages des propriétés liantes à la fois dans le codeC++etQML. Vous pouvez utiliser la classe QObjectBindableProperty pour cela.
© 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.