可绑定属性
演示如何使用可绑定属性简化您的 C++ 代码。
在此示例中,我们将演示两种表示不同对象之间相互关系的方法:基于信号/槽连接和基于可绑定属性的方法。为此,我们将考虑一个订阅服务模型来计算订阅费用。
使用信号/槽方法建模订阅系统
让我们首先考虑 Qt 6 之前的典型实现。为了建模订阅服务,我们使用 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; };
它存储订阅的信息,并提供相应的getter、setter和通知信号以通知监听器订阅信息的变化。它还保持一个指向 User
类实例的指针。
订阅费根据订阅的时长计算
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; }
以及用户的位置
int Subscription::basePrice() const { if (m_user->country() == User::Country::AnyTerritory) return 0; return (m_user->country() == User::Country::Norway) ? 100 : 80; }
当价格变化时,会发出 priceChanged()
信号,以通知监听器价格变化
void Subscription::calculatePrice() { const auto oldPrice = m_price; m_price = qRound(calculateDiscount() * int(m_duration) * basePrice()); if (m_price != oldPrice) emit priceChanged(); }
同样,当订阅时长变化时,会发出 durationChanged()
信号。
void Subscription::setDuration(Duration newDuration) { if (newDuration != m_duration) { m_duration = newDuration; calculatePrice(); emit durationChanged(); } }
注意:两种方法都需要检查数据是否确实已更改,然后才发出信号。《setDuration()`》还需要在时长更改时重新计算价格。
Subscription
除非用户有有效的国籍和年龄,否则是无效的,因此,有效性以以下方式更新
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(); }
User
类很简单:它存储用户的国家和年龄信息,并提供相应的getter、setter和通知信号
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(); } }
在 main()
函数中,我们初始化 User
和 Subscription
的实例
User user;
Subscription subscription(&user);
进行适当的信号-槽连接,以便在 UI 元素更改时更新 user
和 subscription
数据。这部分很简单,所以我们将跳过这部分。
接下来,我们将连接到 Subscription::priceChanged()
,以便在价格更改时更新 UI 中的价格。
QObject::connect(&subscription, &Subscription::priceChanged, priceDisplay, [&] { QLocale lc{QLocale::AnyLanguage, user.country()}; priceDisplay->setText(lc.toCurrencyString(subscription.price() / subscription.duration())); });
我们还连接到 Subscription::isValidChanged()
,以便在订阅无效时禁用价格显示。
QObject::connect(&subscription, &Subscription::isValidChanged, priceDisplay, [&] { priceDisplay->setEnabled(subscription.isValid()); });
因为订阅价格和有效性也取决于用户的国家和年龄,所以我们也需要连接到 User::countryChanged()
和 User::ageChanged()
信号,并相应地更新 subscription
。
QObject::connect(&user, &User::countryChanged, &subscription, [&] { subscription.calculatePrice(); subscription.updateValidity(); }); QObject::connect(&user, &User::ageChanged, &subscription, [&] { subscription.updateValidity(); });
这行得通,但有几个问题
- 有许多样板代码用于信号/槽连接,以便正确跟踪
user
和subscription
的更改。如果任何价格更改的依赖项,我们需要记住发出相应的通知器信号,重新计算价格,并在 UI 中更新它。 - 如果在将来添加更多的价格计算依赖项,我们需要添加更多的信号-槽连接,并确保在任何一个依赖项更改时都正确更新所有依赖项。整体复杂性将增加,代码将更难维护。
Subscription
和User
类依赖于元对象系统,以便能够使用信号/槽机制。
我们能做得更好吗?
使用可绑定属性建模订阅系统
现在让我们看看如何使用 Qt 可绑定属性 来解决相同的问题。首先,让我们看一下 BindableSubscription
类,它与 Subscription
类类似,但使用可绑定属性实现
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 }; };
我们可以注意到的第一个区别是,数据字段现在被封装在 QProperty 类中,通知信号(以及因此从元对象系统的依赖关系)已经消失,并添加了新的方法来返回每个 QProperty 的一个 QBindable。同时,去掉了 calculatePrice()
和 updateValidty()
方法。我们将在下面看到为什么它们不再需要。
BindableUser
类与 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 }; };
第二个区别在于这些类的实现方式。首先,subscription
与 user
之间的依赖关系现在通过绑定表达式进行跟踪
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; }); }
在幕后,可绑定属性会跟踪依赖关系的变化,并在检测到变化时更新属性值。因此,如果用户的国籍或年龄发生变化,订阅的价格和有效期将自动更新。
另一个区别是设置器现在变得非常简单
void BindableSubscription::setDuration(Duration newDuration) { m_duration = newDuration; } void BindableUser::setCountry(Country country) { m_country = country; } void BindableUser::setAge(int age) { m_age = age; }
在设置器中不需要检查属性值是否实际上已经改变,因为 QProperty 已经做到了。相关属性仅在值实际改变时才会通知更改。
更新 UI 中价格信息的代码也得到了简化
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()); });
我们通过 bindablePrice()
和 bindableIsValid()
订阅更改,并在这些属性中的任何一个改变值时相应更新价格显示。只要相应处理程序存在,订阅就会保持活动状态。
请注意,BindableSubscription
和 BindableUser
的复制构造函数都被禁用,因为在复制时对它们的绑定会发生什么还没有定义。
如您所见,代码变得更加简单,并且上述问题得到了解决
- 去掉了信号-槽连接的模板代码,依赖关系现在被自动跟踪。
- 代码更容易维护。在将来添加更多的依赖关系只需要添加相应的可绑定属性,并设置反映它们之间关系的绑定表达式。
Subscription
和User
类不再依赖于元对象系统。当然,如果您需要,您仍然可以将它们公开给元对象系统并添加 Q_PROPERTY,并使用在C++
和QML
代码中的可绑定属性的优势。您可以使用 QObjectBindableProperty 类来实现这一点。
© 2024 Qt 公司有限公司。本文档中的文档贡献均为各自所有者的版权。提供的文档在自由软件基金会发布的GNU自由文档许可协议版本1.3下许可。Qt及其相关标志是芬兰和/或其他国家/地区的Qt公司注册商标。所有其他商标均为其各自所有者的财产。