はじめに
みなさんこんにちは。@task_jp です。好きなクラスは QSharedDataPointer、嫌いなクラスは QTextDocument です。
さて、今年も Qt Advent Calendar がはじまったわけですが、みなさん、今年も Qt で楽しくプログラミングしてますか?
私ごとですが、昨年の12月からフリーランスとしてフラフラしておりました が、ありがたいことに最近とても仕事が忙しくて、色々あって、Qt 関連のお仕事をする会社を作ってしまいました!
合同会社シグナルスロット
https://signal-slot.co.jp/
それが理由かどうかはさておき、今年は一年中 Qt のモデル を書いていて、手抜きメンテナンス性の高いモデルの作り方について結構考えました。
例えば @Atsushi4 さんが QMLで使うモデルをC++で実装する という記事で書いているとおり、基本的には QAbstractItemModel というか QAbstractListModel の仮想関数を実装するだけなので、やることは決まっているのですが、 相当面倒くさい んですよね。
この記事ではモデルが提供する 内部のデータの表現 を上手に書くことで、モデルの実装がちょっと楽になるかもしれない方法を紹介したいと思います。
Q_GADGET とは
Q_GADGET
は qtbase/src/corelib/kernel/qobjectdefs.h の中で定義されているマクロで、ドキュメント に記載があるとおり、昔から QObject
が提供していた メタオブジェクト という便利な機能を、軽量に扱えるように扱えるようにするために導入されたマクロです。
QObject は Qt のオブジェクトモデル の根幹をなす重要なクラスですが、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;
};
だいぶすっきりしました。
プロパティの数だけ QString
や QDateTime
のメンバ変数が増えてしまいますが、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)
};
class
を struct
にして、パブリックな変数ということにしています。
どちらにせよ、かなり素敵ですね。
モデルからデータを扱う
例を簡単にするため、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 = mo->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 さんです。こうご期待!