このページでは

バインド可能なプロパティ

バインド可能なプロパティを使用することで、C++ コードをいかに簡潔にできるかを示します。

この例では、互いに依存する異なるオブジェクト間の関係を表現するための 2 つのアプローチ、すなわちシグナル/スロット接続ベースとバインダブルプロパティベースについて解説します。この目的のために、サブスクリプションの料金を計算するサブスクリプションサービスモデルを例に挙げます。

年齢、利用期間、国旗、月額料金が記載された申込フォーム

シグナル/スロット方式によるサブスクリプションシステムのモデリング

まず、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 };
};

2つ目の違いは、これらのクラスの実装にあります。まず、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 の両方のコードでbindableプロパティの利点を享受できます。その際には、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.