Help us understand the problem. What is going on with this article?

Q_GADGET を活用したデータモデルの実装について

はじめに

みなさんこんにちは。@task_jp です。好きなクラスは QSharedDataPointer、嫌いなクラスは QTextDocument です。

さて、今年も Qt Advent Calendar がはじまったわけですが、みなさん、今年も Qt で楽しくプログラミングしてますか?

私ごとですが、昨年の12月からフリーランスとしてフラフラしておりました が、ありがたいことに最近とても仕事が忙しくて、色々あって、Qt 関連のお仕事をする会社を作ってしまいました!

TODO: ウェブサイトを作ったらリンクをここに書く

それが理由かどうかはさておき、今年は一年中 Qt のモデル を書いていて、手抜きメンテナンス性の高いモデルの作り方について結構考えました。

例えば @Atsushi4 さんが QMLで使うモデルをC++で実装する という記事で書いているとおり、基本的には QAbstractItemModel というか QAbstractListModel の仮想関数を実装するだけなので、やることは決まっているのですが、 相当面倒くさい んですよね。

この記事ではモデルが提供する 内部のデータの表現 を上手に書くことで、モデルの実装がちょっと楽になるかもしれない方法を紹介したいと思います。

Q_GADGET とは

Q_GADGETqtbase/src/corelib/kernel/qobjectdefs.h の中で定義されているマクロで、ドキュメント に記載があるとおり、昔から QObject が提供していた メタオブジェクト という便利な機能を、軽量に扱えるように扱えるようにするために導入されたマクロです。

QObjectQt のオブジェクトモデル の根幹をなす重要なクラスですが、Qt Quick の発明とともに「メタオブジェクトの仕組みは便利で使いたいけれど、QObject は機能がモリモリ過ぎてツラい」という事情があり、この Q_GADGET が誕生したという経緯があります。

Q_GADGET を利用することで、既存のクラスに以下の機能を追加することが可能になります。

  • Q_ENUM を利用した enum の扱いの拡張
  • Q_PROPERTY を利用したプロパティの定義
  • Q_INVOKABLE を利用したメソッドの呼び出し
  • QMetaObject の機能 (staticMetaObject static 変数を利用)

ちなみに、QObject が提供する以下のような機能は 提供されません

  • シグナルの定義
  • スロットの定義

では、早速簡単な例を見てみましょう。

モデルの内部データクラスの定義

Q_GADGET を使った簡単な例

#ifdef MYELEMENT_H
#define MYELEMENT_H

#include <QtCore/QObject>

class MyElement {
    Q_GADGET
    Q_PROPERTY(QString name READ name WRITE setName)
    Q_PROPERTY(QString description READ description WRITE setDescription)
public:
    MyElement();
    ~MyElement() override;

    QString name() const;
    void setName(const QString &name);
    QString description() const;
    void setDescription(const QString &description);

private:
    class Private;
    QScopedPointer<Private> d;
};

#endif // MYELEMENT_H

馴染み深い QObject のサブクラスの定義に結構似ていますが、基底クラスは必要ありません。

クラスの実装は、ありきたりなものなので、ここでは省略します。以下の記事を参考にしてください。

Qt で真面目に値型のクラスを作るとこのようになるのですが、今回はモデルで使用する「内部データ」なので、外面は気にせず、もう少し都合のいいようにしてしまいましょう。

手抜き1

Qt 5.1 より、Q_PROPERTY の定義MEMBER が追加され、メンバ変数を直接指定できるようになりました。

これを活用することで、セッター/ゲッター関数の実装を省略することができます。(モデルの内部データのため、通知用のシグナルは元々利用していません)

class MyElement {
    Q_GADGET
    Q_PROPERTY(QString name MEMBER name)
    Q_PROPERTY(QString description MEMBER description)
public:
    MyElement();

    QString name;
    QString description;
};

だいぶすっきりしました。

プロパティの数だけ QStringQDateTime のメンバ変数が増えてしまいますが、Qt の値型のクラスはだいたいが Implicit Sharing なので細かいところは目をつぶりましょう。

手抜き2

(※要出典)はメンバ変数の初期化はコンストラクタで以下のように行っていましたが、

MyElement::MyElement()
    : name("新しいなんとか")
{}

最近は定義側に書いちゃうことが増えてきた気がしますね。

ということで、こうしちゃいましょう。

class MyElement {
    Q_GADGET
    Q_PROPERTY(QString name MEMBER name)
    Q_PROPERTY(QString description MEMBER description)
public:
    QString name = "新しいなんとか";
    QString description;
};

さらにすっきりしました。

手抜き3

ここは若干利用するケースに依存します。

モデルの要素にアクセスするための変数は、public: スコープに置いていますが、割と規模の大きなモデルで、直接変数なんかでアクセスしない場合には public: を取っ払うことでプライベート変数にすることが可能です。

class MyElement {
    Q_GADGET

    Q_PROPERTY(QString name MEMBER name)
    QString name = "新しいなんとか";

    Q_PROPERTY(QString description MEMBER description)
    QString description;
};

Q_GADGET マクロが内部で スコープを定義していて その最後は private: になっています)

public なスコープにしておいたほうが便利な場合は、以下のように工夫することも可能です。

struct MyElement {
    QString name = "新しいなんとか";
    QString description;

    Q_GADGET
    Q_PROPERTY(QString name MEMBER name)
    Q_PROPERTY(QString description MEMBER description)
};

classstruct にして、パブリックな変数ということにしています。

どちらにせよ、かなり素敵ですね。

モデルからデータを扱う

例を簡単にするため、QAbstractListModel で最低限の実装に留めます。

class MyModel : public QAbstractListModel
{
    Q_OBJECT
public:
    MyModel(QObject *parent = nullptr);

    // これからあれこれ追加する

private:
    QList<MyElement> elements;
};

roleNames() の実装

QML から「名前」でアクセスするために実装します。

QHash<int, QByteArray> MyModel::roleNames() const
{
    QHash<int, QByteArray> ret;
    const auto mo = &MyElement::staticMetaObject;
    for (int i = 0; i < mo->propertyCount(); i++) {
        const auto mp = mo->property(i);
        ret.insert(Qt::UserRole + i, mp.name());
    }
    return ret;
}

突然見慣れない感じになりましたね!

Q_GADGET マクロが提供する static な、 staticMetaObject 変数が提供する QMetaObject の機能を利用して、MyElement がインターフェースとして持っている QMetaProperty 形式のプロパティの一覧を取得し、その「名前」をロール名として設定しています。

role の値は慣例どおり Qt::UserRole + n で採番しています。

data() の実装

QVariant MyElement::data(const QModelIndex &index, int role) const
{
    // シンプルにするためエラーチェックはすべて省略
    const auto mo = &MyElement::staticMetaObject;
    const auto mp = mo->property(role - Qt::UserRole);
    const auto row = index.row();
    const auto element = elements.at(row);
    return mp.readOnGadget(&element);
}

QMetaProperty::readOnGadget() というメソッドを利用して、MyElementプロパティの値 を取得し、返しています。

setData() の実装

data() とほとんど一緒なので省略します。 QMetaProperty::writeOnGadget() を使います。

その他の便利な使い方

モデルの読み込み

アプリケーションから Open や Import されるモデルのソースって JSON 形式だったり、XML 形式だったりすることが多いので、モデルの構築も自動化できたりすることが多いです。

void MyModel::loadData(const QString &file)
{
    // const auto mo = ...
    // QFile ...
    // QJsonDocument ...
    for (/* QJsonArray のループ */) {
        const auto QJsonObject jo = ...;
        MyElement element;
        for (int i = 0; i < mo->propertyCount(); i++) {
            const auto mp = metaObject->property(i);
            if (jo.contains(mp.name())) {
                mp.writeOnGadget(&element, jo.value(mp.name()));
            }
        }
        elements.append(element);
    }
}

モデルの保存

Save や Export 時の処理も読み込みの逆で、20行くらいで書けちゃいますね。

終わりに

Q_GADGET を利用した内部データ用のクラスを作って、QMetaObject/QMetaProperty の機能を使ってモデルを実装することで、内部データ用のクラスのメンバ変数に一切アクセスすることなく、データの読み書きやロード/セーブができるモデルが作れることが分かりました。

画期的で汎用的すぎて、今年関わったプロジェクトの C++ のモデルはほぼこんな感じの実装になってしまいました。

実は、ビュー側も C++ にせよ QML にせよ、同じような感じで書いていたので、もし機会があればその内容も紹介したいと思います。

というわけで、すでに Qt を使っていてお困りの方や、これから Qt を使おうとしている方、それ以外にも複雑な事情があって納得できるアドバイスを必要としている方がいましたら連絡いただければと思います!

明日は @hermit4 さんです。こうご期待!

task_jp
Qt が好きで遊んでいたら2014年、世界で史上2番目の Qt Champion というのになっちゃいました。 2019年にも自身2度目の Qt Champion を獲得。 現在求職中です。今までの経験を活かして活躍できそうなお仕事があれば是非紹介してください。 それ以外のお仕事の問い合わせは https://qt5.jp/qwork/ からお願いします。
https://qt5.jp/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away