QML コーディングにおける Q_PROPERTY 不足問題の解決策

Qt でクライアント向けのアノテーションクラスを実装する際、C++ の主要データを QML に公開するために Q_PROPERTY を多用すると、十数個しか使えないという制限に遭遇しました。

class ImageShow : public QQuickPaintedItem
{
    Q_OBJECT
    Q_PROPERTY(QJsonObject minio_source READ minio_source WRITE setMinio_source NOTIFY minio_sourceChanged)
    Q_PROPERTY(QVariant source READ source WRITE setSource NOTIFY sourceChanged)
    Q_PROPERTY(zy::BaseShape::ShapeType base_shape READ base_shape WRITE setBase_shape NOTIFY base_shapeChanged)
    Q_PROPERTY(int edit READ edit WRITE setEdit NOTIFY editChanged)
    // 以下、コメントアウトされたプロパティ多数…
    // Q_PROPERTY(bool canScaled …)
    // Q_PROPERTY(bool canMoved …)
    Q_PROPERTY(QRectF clipRoi READ clipRoi WRITE setClipRoi NOTIFY clipRoiChanged)
    // … 複数行省略
};

そのため、QML の Itemanchors{} を利用して多くのプロパティを定義できるよう、マクロで簡易的な処理を実装し、後々の機能追加に備えることにしました。

毎回同じようなコードを書くのを避けるため、以下のマクロを定義します。

#ifndef DEMOHEADER_H
#define DEMOHEADER_H

// Q_PROPERTY も含む完全版マクロ
#define QML_PROPERTY(type, name, setter, initial, handler) \
    Q_PROPERTY(type name READ name WRITE setter NOTIFY name##Changed) \
    public: \
        type name() const { return m_##name; } \
        void setter(type val) { \
            if (m_##name != val) { \
                m_##name = val; \
                handler(val); \
                emit name##Changed(m_##name); \
            } \
        } \
    Q_SIGNALS: \
        void name##Changed(type val); \
    private: \
        type m_##name = initial;

// 簡易版(Q_PROPERTY 行を別途記述したい場合)
#define QML_PROPERTY_SIMPLE(type, name, setter, initial, handler) \
    public: \
        type name() const { return m_##name; } \
        void setter(type val) { \
            if (m_##name != val) { \
                m_##name = val; \
                handler(val); \
                emit name##Changed(m_##name); \
            } \
        } \
    Q_SIGNALS: \
        void name##Changed(type val); \
    private: \
        type m_##name = initial;

#endif // DEMOHEADER_H

次に、拡張データを保持するクラス RoiData でこれらのマクロを活用します。

#ifndef ROIDATA_H
#define ROIDATA_H

#include "demoheader.h"
#include <QObject>
#include <QVariant>

class RoiData : public QObject
{
    Q_OBJECT
    // 手動で Q_PROPERTY を定義
    Q_PROPERTY(bool canScale READ canScale WRITE setCanScale NOTIFY canScaleChanged)
    Q_PROPERTY(bool canMove READ canMove WRITE setCanMove NOTIFY canMoveChanged)
    Q_PROPERTY(QVariantMap handles READ handles WRITE setHandles NOTIFY handlesChanged)
    Q_PROPERTY(int fillMode READ fillMode WRITE setFillMode NOTIFY fillModeChanged)
    Q_PROPERTY(int statusFlags READ statusFlags WRITE setStatusFlags NOTIFY statusFlagsChanged)

public:
    enum LabelStatus { Annotation = 0, Training, Testing };
    Q_ENUM(LabelStatus)
    enum FillMode { DefaultFit = 0, Stretch, CenteredPreserve };
    Q_ENUM(FillMode)

    // マクロでプロパティ自動生成(Q_PROPERTY も含む)
    QML_PROPERTY(int, statusFlags, setStatusFlags, 1, reshaped)
    QML_PROPERTY(bool, canScale, setCanScale, false, reshaped)
    QML_PROPERTY(bool, canMove, setCanMove, true, reshaped)
    QML_PROPERTY(bool, canHover, setCanHover, false, reshaped)
    QML_PROPERTY(QVariantMap, handles, setHandles, {}, reshaped)
    QML_PROPERTY(FillMode, fillMode, setFillMode, DefaultFit, reshaped)
    QML_PROPERTY(LabelStatus, label, setLabel, Annotation, reshaped)

    explicit RoiData(QObject *parent = nullptr);

signals:
    void reshaped(QVariant data); // 再描画トリガー
};

Q_DECLARE_METATYPE(RoiData::LabelStatus)

#endif // ROIDATA_H

そして、RoiData を子プロパティとして持つメインクラス MainItem を以下に定義します。

#ifndef MAINITEM_H
#define MAINITEM_H

#include <QObject>
#include "roidata.h"
#include <QVariant>
#include <QDebug>

class MainItem : public QObject
{
    Q_OBJECT
    // 手動で Q_PROPERTY 追加(マクロを使わず、子オブジェクトは専用に定義)
    Q_PROPERTY(RoiData* roi READ roi WRITE setRoi NOTIFY roiChanged)

public:
    explicit MainItem(QObject *parent = nullptr) : QObject(parent)
    {
        m_roi = new RoiData(this);
        qDebug() << m_roi->statusFlags() << m_roi->handles()
                << m_roi->canHover() << m_roi->canMove()
                << m_roi->canScale();
    }

    RoiData* roi() const { return m_roi; }
    void setRoi(RoiData* val) {
        if (m_roi != val) {
            m_roi = val;
            emit roiChanged();
        }
    }

signals:
    void roiChanged();

private:
    RoiData* m_roi = nullptr;
};

#endif // MAINITEM_H

続いて、main.cpp にて QML 側に型を登録します。

#define URI "cpp"
qmlRegisterType<MainItem>(URI, 1, 0, "MainItem");
qmlRegisterType<RoiData>(URI, 1, 0, "RoiData");

最終的に main.qml から下記のように使えます。子プロパティを {} で初期化できるので、可読性が向上します。

import QtQuick 2.12
import QtQuick.Window 2.12
import cpp 1.0

Window {
    visible: true
    width: 640
    height: 480
    title: qsTr("Hello World")

    MainItem {
        id: item
        roi {
            statusFlags: 12
            canScale: true
        }
    }

    Component.onCompleted: {
        item.roi.statusFlags = 2
        item.roi.label = RoiData.Training
        item.roi.handles = { type: "12312", data: { index: 1, value: "dasfads" } }
        item.roi.fillMode = RoiData.CenteredPreserve
        console.log(JSON.stringify(item.roi))
    }
}

実行結果(略)。後日データを拡張する際は、RoiData クラスに新しいプロパティを QML_PROPERTY マクロで追加するだけで済みます。

タグ: Qt QML Q_PROPERTY マクロ C++

5月15日 17:50 投稿