En esta página

Propiedades enlazables

Demuestra cómo el uso de propiedades enlazables puede simplificar tu código C++.

En este ejemplo demostraremos dos enfoques para expresar las relaciones entre diferentes objetos que dependen unos de otros: basado en la conexión señal/ranura y basado en propiedades bindable. Para ello consideraremos un modelo de servicio de suscripción para calcular el coste de la suscripción.

Modelado del sistema de suscripción con el enfoque señal/ranura

Consideremos primero la implementación pre-Qt 6 habitual. Para modelar el servicio de suscripción se utiliza la clase 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;
};

Almacena la información sobre la suscripción y proporciona los correspondientes getters, setters y señales notificadoras para informar a los oyentes sobre los cambios en la información de la suscripción. También mantiene un puntero a una instancia de la clase User.

El precio de la suscripción se calcula en función de la duración de la misma:

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

Y la ubicación del usuario:

int Subscription::basePrice() const
{
    if (m_user->country() == User::Country::AnyTerritory)
        return 0;

    return (m_user->country() == User::Country::Norway) ? 100 : 80;
}

Cuando cambia el precio, se emite la señal priceChanged() para notificar el cambio a los oyentes:

void Subscription::calculatePrice()
{
    const auto oldPrice = m_price;

    m_price = qRound(calculateDiscount() * int(m_duration) * basePrice());
    if (m_price != oldPrice)
        emit priceChanged();
}

Del mismo modo, cuando la duración de la suscripción cambia, se emite la señal durationChanged().

void Subscription::setDuration(Duration newDuration)
{
    if (newDuration != m_duration) {
        m_duration = newDuration;
        calculatePrice();
        emit durationChanged();
    }
}

Nota: Ambos métodos necesitan comprobar si los datos han cambiado realmente y sólo entonces emitir las señales. setDuration() también necesita recalcular el precio cuando la duración ha cambiado.

El Subscription no es válido a menos que el usuario tenga un país y una edad válidos, por lo que la validez se actualiza de la siguiente manera:

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 clase User es sencilla: almacena el país y la edad del usuario y proporciona los correspondientes getters, setters y señales notificadoras:

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

En la función main() inicializamos instancias de User y Subscription:

    User user;
    Subscription subscription(&user);

Y hacemos las conexiones señal-ranura adecuadas para actualizar los datos de user y subscription cuando cambian los elementos de la interfaz de usuario. Esto es sencillo, así que nos saltaremos esta parte.

A continuación, nos conectamos a Subscription::priceChanged() para actualizar el precio en la interfaz de usuario cuando el precio cambia.

    QObject::connect(&subscription, &Subscription::priceChanged, priceDisplay, [&] {
        QLocale lc{QLocale::AnyLanguage, user.country()};
        priceDisplay->setText(lc.toCurrencyString(subscription.price() / subscription.duration()));
    });

También nos conectamos a Subscription::isValidChanged() para desactivar la visualización del precio si la suscripción no es válida.

    QObject::connect(&subscription, &Subscription::isValidChanged, priceDisplay, [&] {
        priceDisplay->setEnabled(subscription.isValid());
    });

Como el precio y la validez de la suscripción también dependen del país y la edad del usuario, también tenemos que conectarnos a las señales User::countryChanged() y User::ageChanged() y actualizar subscription en consecuencia.

    QObject::connect(&user, &User::countryChanged, &subscription, [&] {
        subscription.calculatePrice();
        subscription.updateValidity();
    });

    QObject::connect(&user, &User::ageChanged, &subscription, [&] {
        subscription.updateValidity();
    });

Esto funciona, pero hay algunos problemas:

  • Hay un montón de código repetitivo para las conexiones señal-ranura con el fin de realizar un seguimiento adecuado de los cambios tanto en user como en subscription. Si alguna de las dependencias del precio cambia, tenemos que acordarnos de emitir las señales notificadoras correspondientes, recalcular el precio y actualizarlo en la interfaz de usuario.
  • Si en el futuro se añaden más dependencias para el cálculo del precio, tendremos que añadir más conexiones de ranura de señal y asegurarnos de que todas las dependencias se actualizan correctamente cada vez que cambia alguna de ellas. La complejidad general crecerá y el código será más difícil de mantener.
  • Las clases Subscription y User dependen del sistema de metaobjetos para poder utilizar el mecanismo señal/ranura.

¿Podemos hacerlo mejor?

Modelado del sistema de suscripción con propiedades bindbables

Veamos ahora cómo las Qt Bindable Properties pueden ayudar a resolver el mismo problema. Primero, echemos un vistazo a la clase BindableSubscription, que es similar a la clase Subscription, pero está implementada usando propiedades bindable:

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 primera diferencia que podemos notar, es que los campos de datos están ahora envueltos dentro de las clases QProperty, y las señales del notificador (y como consecuencia la dependencia del sistema de metaobjetos) han desaparecido, y en su lugar se han añadido nuevos métodos que devuelven un QBindable para cada QProperty. También se eliminan los métodos calculatePrice() y updateValidty(). Más adelante veremos por qué ya no son necesarios.

La clase BindableUser difiere de la clase User de forma similar:

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 segunda diferencia está en la implementación de estas clases. En primer lugar, las dependencias entre subscription y user se controlan ahora mediante expresiones vinculables:

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

Entre bastidores, las propiedades vinculables rastrean los cambios de dependencia y actualizan el valor de la propiedad cada vez que se detecta un cambio. Así, por ejemplo, si se cambia el país o la edad del usuario, el precio y la validez de la suscripción se actualizarán automáticamente.

Otra diferencia es que ahora los setters son triviales:

void BindableSubscription::setDuration(Duration newDuration)
{
    m_duration = newDuration;
}

void BindableUser::setCountry(Country country)
{
    m_country = country;
}

void BindableUser::setAge(int age)
{
    m_age = age;
}

No hay necesidad de comprobar dentro de los setters si el valor de la propiedad ha cambiado realmente, QProperty ya lo hace. Las propiedades dependientes serán notificadas del cambio sólo si el valor ha cambiado realmente.

El código para actualizar la información sobre el precio en la interfaz de usuario también se ha simplificado:

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

Nos suscribimos a los cambios a través de bindablePrice() y bindableIsValid() y actualizamos la visualización del precio en consecuencia cuando cualquiera de estas propiedades cambia el valor. Las suscripciones se mantendrán activas mientras lo estén los manejadores correspondientes.

Nótese también que los constructores de copia de BindableSubscription y BindableUser están deshabilitados, ya que no está definido qué debe ocurrir con sus enlaces al copiar.

Como puedes ver, el código se ha simplificado mucho, y los problemas mencionados anteriormente están resueltos:

  • El código repetitivo para las conexiones de ranura de señal se elimina, las dependencias ahora se rastrean automáticamente.
  • El código es más fácil de mantener. Para añadir más dependencias en el futuro sólo será necesario añadir las propiedades vinculables correspondientes y establecer las expresiones de vinculación que reflejen las relaciones entre ellas.
  • Las clases Subscription y User ya no dependen del sistema de metaobjetos. Por supuesto, aún puedes exponerlas al sistema de metaobjetos y añadir Q_PROPERTYs si lo necesitas, y tener las ventajas de las propiedades enlazables tanto en el código de C++ como en el de QML. Puede utilizar la clase QObjectBindableProperty para ello.

Proyecto de ejemplo @ 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.