Atsushi4のブログ

へたれQtプログラマの備忘録的ななにか

QWidgetでもプロパティバインディングがしたい!

はじめに

この記事はQt Advent Calendar 2014 - Qiitaの19日目のエントリーです。

タイトルのとおり、QWidgetベースのアプリケーションでプロパティバインディングできたらいいな、という事で、やってみた内容を書いていきます。
ちなみに元々の動機は、なんかメンテナンス性を上げるためにMVVMで作れ!とかなんとかそんな圧力とかなんとかそんな感じです。
あと、QMetaType周りってちょっと便利そうだけどあんまり使い方が分からないので使ってみたいとかそんな欲求もありました。


早速実際にやってみた

とにかくやってみました。まぁ何とか動くところまではいった、というところでしょうか。

SignalとSlotのQMetaMethodを探してなんとかする

とりあえずSignalとSlotをQMetaObjectのMethodから探して何とかしようと思いました。

static QString findMethod(const QMetaObject *meta, QByteArray propertyName)
{
    for (int i = 0; i < meta->methodCount(); ++i) {
        auto method = meta->method(i);
        if (method.name().toLower() == ("set" + propertyName).toLower())
        {
            return method.methodSignature();
        }
    }
    return QString();
}

void MainWindow::bind(QObject *from, const char *read,
    QObject *to, const char *write) const
{
    auto meta1 = from->metaObject();
    auto meta2 = to->metaObject();

    // Set current value
    to->setProperty(write, from->property(read));

    auto prop1 = meta1->property(meta1->indexOfProperty(read));
    if (!prop1.hasNotifySignal()) return;

    auto prop2 = meta2->property(meta2->indexOfProperty(write));
    QString signature = findMethod(meta2, prop2.name());
    if (signature.isEmpty()) return;

    connect(from, prop1.notifySignal(),
        to, meta2->method(meta2->indexOfMethod(signature.toLatin1())));
}

とりあえずこれで一方のプロパティの変更を他方のプロパティに反映することができるようになりました。ただし以下の制限があります。

  • 二つのプロパティの型が同一であること
  • from側の変更通知シグナルがあること
  • to側にset[プロパティ名]という名前のset用Slotがあること

Qtのクラスは大体3番目の条件を満たしているので、ViewModelのプロパティの型を合わせてnotifySignalを定義すればそれなりに使えそうな気がします。

一旦別のクラスで受ける

上の方法ではちょっと制限がきついので、もう少し考えてみました。

class MainWindow : public QMainWindow
{
    Q_OBJECT
public:
    explicit MainWindow(QWidget *parent = 0);
    ~MainWindow();
public slots:
    void checkChanged();
private:
    void bind(QObject *from, const char *read, QObject *to, const char *write);
private:
    struct BindMap
    {
        QObject *from;
        QByteArray read;
        QObject *to;
        QByteArray write;
    };
private:
    Ui::MainWindow *ui;
    QMultiMap<QObject*,BindMap> objects;
};

void MainWindow::bind(QObject *from, const char *read,
    QObject *to, const char *write)
{
    objects.insertMulti(from, {from, read, to, write});
    to->setProperty(write, from->property(read));

    auto meta1 = from->metaObject();
    auto prop1 = meta1->property(meta1->indexOfProperty(read));
    if (!prop1.hasNotifySignal()) return;

    auto meta = this->metaObject();
    connect(from, prop1.notifySignal(),
            this, meta->method(meta->indexOfMethod("checkChanged()")));
}

これならfrom側にnotifySignalがあってto側のpropertyがwritableならbindできます。型も同一でなくてもQVariantベースで変換できれば変換してくれます。シグナルが関数ポインタで取れればラムダでちょちょいっと書けそうなのですが、それらしきAPIは見つかりませんでした。

まぁあとはちゃんと専用クラスに分離して、途中でdeleteされるクラスの始末とかエラー処理とか入れればそれなりに使える・・・かも?


おわりに

QMetaObjectとか使うとconnectよりも柔軟にpropertyの接続とかできるかもしれません。大量のコントロールのobjectNameをproperty名に合わせておいて全部まとめてset、getするとか(そもそもそれは設計が悪いとか言わない)。