Propriétés liables
Cette page montre comment l'utilisation des propriétés liables peut simplifier votre code C++.
Dans cet exemple, nous allons présenter deux approches permettant d’exprimer les relations entre différents objets qui dépendent les uns des autres : l’approche basée sur les connexions signal/slot et celle basée sur les propriétés liables. Pour cela, nous allons nous appuyer sur un modèle de service d’abonnement afin de calculer le coût de l’abonnement.

Modélisation d'un système d'abonnement avec l'approche signal/slot
Considérons d'abord l'implémentation habituelle antérieure à Qt 6. Pour modéliser le service d'abonnement, on utilise la classe ` Subscription ` :
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 d’abonnement. Elle conserve également un pointeur vers une instance de la classe ` User `.
Le prix de l’abonnement est calculé en fonction de sa durée :
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 notifier ce changement aux auditeurs :
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 : ces deux méthodes doivent vérifier si les données ont effectivement changé avant d’émettre les signaux. La méthode ` setDuration() ` doit également recalculer le prix lorsque la durée a changé.
L’ Subscription n’est valide que si l’utilisateur a renseigné un pays et un âge valides ; la validité est donc 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 des 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 de ` user ` et ` subscription ` lorsque les éléments de l’interface utilisateur changent. C’est très simple, nous allons donc passer cette partie.
Ensuite, nous nous connectons à Subscription::priceChanged() pour mettre à jour le prix dans l’interface utilisateur lorsque celui-ci 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 signal-slot afin de suivre correctement les modifications apportées à la fois à
useret àsubscription. Si l’une des dépendances du prix change, nous devons penser à émettre les signaux de notification correspondants, recalculer le prix et le mettre à jour dans l’interface utilisateur. - Si d’autres dépendances sont ajoutées à l’avenir pour le calcul du prix, nous devrons ajouter davantage de connexions signal-slot et nous assurer que toutes les dépendances sont correctement mises à jour dès qu’une d’entre elles change. La complexité globale augmentera et le code deviendra plus difficile à maintenir.
- Les classes `
Subscription` et `User` dépendent du système de méta-objets pour pouvoir utiliser le mécanisme signal/slot.
Peut-on faire mieux ?
Modélisation d’un système d’abonnement avec les propriétés liables
Voyons maintenant comment les propriétés liables (Bindable Properties) de Qt peuvent aider à résoudre ce même problème. Commençons par examiner 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 remarquer est que les champs de données sont désormais encapsulés dans des classes ` QProperty `, que les signaux du notificateur (et, par conséquent, la dépendance vis-à-vis du système de méta-objets) ont disparu, et que de nouvelles méthodes renvoyant une instance de ` QBindable ` pour chaque ` QProperty ` ont été ajoutées à la place. Les méthodes ` calculatePrice() ` et ` updateValidty() ` ont également été supprimées. Nous verrons ci-dessous 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 désormais gérées 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 arrière-plan, les propriétés liables suivent les changements de dépendance et mettent à jour la valeur de la propriété dès qu’un changement est détecté. Ainsi, si, par exemple, le pays ou l’âge de l’utilisateur est modifié, le prix et la durée de validité de l’abonnement seront automatiquement mis à jour.
Une autre différence réside dans le fait que les accesseurs sont désormais très simples :
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 plus nécessaire de vérifier au sein des setters si la valeur de la propriété a réellement changé, car « QProperty » s’en charge déjà. Les propriétés dépendantes ne seront informées du changement que si la valeur a effectivement changé.
Le code permettant de mettre à jour les informations relatives au 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 modifications via ` bindablePrice() ` et ` bindableIsValid() `, puis nous mettons à jour l’affichage du prix en conséquence dès que 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 qu’il doit advenir de leurs liaisons lors de la copie.
Comme vous pouvez le constater, le code est devenu beaucoup plus simple et les problèmes mentionnés ci-dessus sont résolus :
- Le code standard pour les connexions signal-slot a été supprimé, les dépendances sont désormais gérées automatiquement.
- Le code est plus facile à maintenir. L’ajout de nouvelles dépendances à l’avenir ne nécessitera que l’ajout des propriétés liables correspondantes et la définition des expressions de liaison reflétant les relations entre elles.
- Les classes `
Subscription` et `User` ne dépendent plus du système de méta-objets. Bien sûr, vous pouvez toujours les exposer au système de méta-objets et ajouter des ` Q_PROPERTY` si nécessaire, afin de bénéficier des avantages des propriétés liables à la fois dans le code `C++` et `QML`. Vous pouvez utiliser la classe ` QObjectBindableProperty ` à cette fin.
© 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.