本页内容

可绑定属性

演示了如何通过使用可绑定属性来简化您的 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() ` 函数中,我们初始化 `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 的可绑定属性(Bindable Properties)如何帮助解决同样的问题。首先,让我们看看 `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 类来实现这一点。

示例项目 @ code.qt.io

© 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.