可绑定属性

演示如何使用可绑定属性简化您的 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() 函数中,我们初始化 UserSubscription 的实例

    User user;
    Subscription subscription(&user);

进行适当的信号-槽连接,以便在 UI 元素更改时更新 usersubscription 数据。这部分很简单,所以我们将跳过这部分。

接下来,我们将连接到 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();
    });

这行得通,但有几个问题

  • 有许多样板代码用于信号/槽连接,以便正确跟踪 usersubscription 的更改。如果任何价格更改的依赖项,我们需要记住发出相应的通知器信号,重新计算价格,并在 UI 中更新它。
  • 如果在将来添加更多的价格计算依赖项,我们需要添加更多的信号-槽连接,并确保在任何一个依赖项更改时都正确更新所有依赖项。整体复杂性将增加,代码将更难维护。
  • SubscriptionUser 类依赖于元对象系统,以便能够使用信号/槽机制。

我们能做得更好吗?

使用可绑定属性建模订阅系统

现在让我们看看如何使用 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 };
};

第二个区别在于这些类的实现方式。首先,subscriptionuser 之间的依赖关系现在通过绑定表达式进行跟踪

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() 订阅更改,并在这些属性中的任何一个改变值时相应更新价格显示。只要相应处理程序存在,订阅就会保持活动状态。

请注意,BindableSubscriptionBindableUser 的复制构造函数都被禁用,因为在复制时对它们的绑定会发生什么还没有定义。

如您所见,代码变得更加简单,并且上述问题得到了解决

  • 去掉了信号-槽连接的模板代码,依赖关系现在被自动跟踪。
  • 代码更容易维护。在将来添加更多的依赖关系只需要添加相应的可绑定属性,并设置反映它们之间关系的绑定表达式。
  • SubscriptionUser 类不再依赖于元对象系统。当然,如果您需要,您仍然可以将它们公开给元对象系统并添加 Q_PROPERTY,并使用在 C++QML 代码中的可绑定属性的优势。您可以使用 QObjectBindableProperty 类来实现这一点。

示例项目 @ code.qt.io

© 2024 Qt 公司有限公司。本文档中的文档贡献均为各自所有者的版权。提供的文档在自由软件基金会发布的GNU自由文档许可协议版本1.3下许可。Qt及其相关标志是芬兰和/或其他国家/地区的Qt公司注册商标。所有其他商标均为其各自所有者的财产。