可绑定属性

演示如何使用可绑定属性简化 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;
};

该类存储订阅信息,并提供相应的获取器、设置器和通知器信号,用于通知监听者订阅信息的更改。它还保存一个指向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 类很简单:它存储用户的国家和年龄,并提供相应的获取器、设置器和通知器信号:

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);

并进行适当的信号槽连接,以便在用户界面元素发生变化时更新usersubscription 数据。这部分简单明了,因此我们将跳过。

接下来,我们连接Subscription::priceChanged() ,以便在价格发生变化时更新用户界面中的价格。

    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 的变化,信号槽连接需要大量模板代码。如果价格的任何依赖关系发生变化,我们需要记住发出相应的通知信号,重新计算价格,并在用户界面中进行更新。
  • 如果将来增加更多的价格计算依赖项,我们就需要增加更多的信号槽连接,并确保在任何依赖项发生变化时都能正确更新所有依赖项。整体复杂性将会增加,代码也会变得更难维护。
  • 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 已经做到了这一点。只有当属性值实际发生变化时,从属属性才会收到变化通知。

用户界面中更新价格信息的代码也得到了简化:

    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_PROPERTYs,同时在C++QML 代码中使用可绑定属性的优势。为此,您可以使用QObjectBindableProperty 类。

示例项目 @ 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.