C++でQMLコンポーネントを作成する

Create a QML component by C++
QQuickItemを継承したC++クラスを作ることで、UI関連の制御も行えるQMLコンポーネントを実装することができます。また、このQQuickItemを継承したQQuickPaintedItemを継承すればQPainterによるオーナードローも実装できます。

QQuickPaintedItemを使ったプログラム例を紹介します。前回のQObject継承クラスと同様、Q_OBJECTを用意するまでは同じですが、コンストラクタでは親アイテムの情報がQMLから届いているので、それを親クラスにちゃんと転送するようにしておきましょう。
cont.cpp
#include <QQuickPaintedItem>

class CustomItem : public QQuickPaintedItem
{
    Q_OBJECT
public:
    CustomItem(QQuickItem *parent = nullptr);
}

CustomItem::CustomItem(QQuickItem *parent) : QQuickPaintedItem(parent)
{
}
QQuickItemのイベントをC++で受け取るには2つのパターンがあります。まずはQQuickItemで呼び出される関数(QtドキュメントのProtected Functionsに記載されている関数)をオーバーライドする方法です。これらの関数はオーバーライドするだけでイベントを処理することができます。
override.cpp
class CustomItem : public QQuickPaintedItem
{
    Q_OBJECT
protected:
    virtual void mousePressEvent(QMouseEvent *ev);
    virtual void paint(QPainter *p);
}

void CustomItem::mousePressEvent(QMouseEvent *ev)
{
    Q_UNUSED(ev);
}

void CustomItem::paint(QPainter *p)
{
    p->drawRect(QRectF(0,0,10,10));
}
もうひとつはQMLのシグナル(QtドキュメントのSignalsに記載されている関数)同士を紐付ける(接続する)方法で、プロパティの変更などはこれによって受け取ることになります。

シグナルは「public slots:」で宣言した関数をQObject::connectで対象のQObject派生クラスと紐付けます。引数は「送信元のオブジェクト」「送信元のシグナル関数」「送信先の(シグナルを受け取る)オブジェクト」「送信先のスロット関数」の順で指定します。SIGNALとSLOTマクロを使用するとより簡潔に記述できます。逆に紐付けを解除したい場合はQObject::disconnectを使用します。
connect.cpp
class CustomItem : public QQuickPaintedItem
{
    Q_OBJECT
public slots:
    void onSizeChanged();
};

CustomItem::CustomItem(QQuickItem *parent) : QQuickPaintedItem(parent)
{
    // widthChangedシグナルを自クラスのonSizeChangedに紐付ける
    connect(this, &QQuickItem::widthChanged, this, &CustomItem::onSizeChanged);
    // マクロを使ったheightChangedの接続方法
    connect(this, SIGNAL(heightChanged()), SLOT(onSizeChanged()));
}

void CustomItem::onSizeChanged()
{
    // 親アイテムの中央に移動
    setX((parentItem()->width() - width()) / 2.0);
    setY((parentItem()->height() - height()) / 2.0);
}
タイマーなどの派生クラスを作らないオブジェクトでもシグナルとスロットが重要になります。こちらはタイマー制御を行うためのクラス・QTimerを使った例です。
timer.cpp

class CustomItem : public QQuickPaintedItem
{
    Q_OBJECT
private:
    QTimer *timer;
}

CustomItem::CustomItem(QQuickItem *parent) : QQuickPaintedItem(parent)
{
    // 親に関連付けると、親が破棄されるとこのオブジェクトも破棄されるのでdelete処理は不要
    timer = new QTimer(this);
    // タイマーイベントごとにQQuickPaintedItem::updateを実行してアイテムを再描画させる
    connect(timer, SIGNAL(timeout()), this, SLOT(update()));
    // 15ms間隔でタイマーを開始
    timer->start(15);
    // 手動でタイマーを破棄する例
    // timer->deleteLater();
}
QtではC++と同様、新しいオブジェクトを作成するのに「new」を使いますが、これを「delete」で直接削除するとQMLから急にアクセスできなくなることによる不具合の原因となります。

Qtの場合、親オブジェクトを指定して作成したオブジェクトは、親オブジェクトが破棄されたときにQtが適切に処理してくれます。Qtによって破棄されると「destroyed」シグナルが発生するので、QMLが関与する最終処理はデストラクタではなく、こちらに記述するのが望ましいでしょう。なお、手動でQtオブジェクトを削除したいときは「deleteLater()」関数を呼び出して、破棄処理をQtに任せます。

以上を踏まえた最終的なプログラム例がこちらとなります。プロジェクトを実行すると、4つの四角形が順番に明滅し、また、この領域をクリックすると四角形が赤色に切り替わります。
customitem.h
#ifndef CUSTOMITEM_H
#define CUSTOMITEM_H

#include <QQuickPaintedItem>

class CustomItem : public QQuickPaintedItem
{
    Q_OBJECT
    Q_PROPERTY(QColor color MEMBER color)

private:
    QTimer *timer;
    QColor color;
    int frame = 0, cframe = 0;

public:
    CustomItem(QQuickItem *parent = nullptr);

protected:
    virtual void mousePressEvent(QMouseEvent *ev);
    virtual void paint(QPainter *p);

public slots:
    void onSizeChanged();
};

#endif
customitem.cpp
#include "customitem.h"
#include <QTimer>
#include <QPainter>

CustomItem::CustomItem(QQuickItem *parent) : QQuickPaintedItem(parent)
{
    connect(this, &QQuickItem::widthChanged, this, &CustomItem::onSizeChanged);
    connect(this, SIGNAL(heightChanged()), SLOT(onSizeChanged()));

    // 左ボタンの入力を受け付け、mousePressイベントを発生させる
    setAcceptedMouseButtons(Qt::LeftButton);

    timer = new QTimer(this);
    connect(timer, SIGNAL(timeout()), this, SLOT(update()));
    timer->start(15);
}

void CustomItem::mousePressEvent(QMouseEvent *ev)
{
    Q_UNUSED(ev);
    color = Qt::GlobalColor::red;
    // rgb形式の例
    //color = QColor(255, 0, 0);
}

void CustomItem::paint(QPainter *p)
{
    frame += 2;
    qreal w = width(), h = height();

    qreal r = qMin(w - 1, h - 1) / 2.0;
    qreal s = r / 20.0;
    const qreal ps = 1.0;
    qreal sz = r - 2 * ps - s;

    // アンチエイリアスを有効にして描画
    p->setRenderHints(QPainter::Antialiasing | QPainter::TextAntialiasing);

    // colorの色を持つペンを作成
    QPen pen(color);
    pen.setWidthF(ps);
    p->setPen(pen);

    QPointF cp = QPointF(w / 2, h / 2);

    const int all = 120;
    const qreal start = 30, end = 90;
    QRectF drc;
    for(int i = 0; i < 4; i++){
        qreal pos = (frame - i * 30) % all;
        qreal opq = (pos - end) / (start - end);
        if(opq < 0 || opq > 1) opq = 0;
        switch(i){
        case 3: drc = QRectF(cp.x() - s - sz, cp.y() + s, sz, sz); break;
        case 2: drc = QRectF(cp.x() + s, cp.y() + s, sz, sz); break;
        case 1: drc = QRectF(cp.x() + s, cp.y() - s - sz, sz, sz); break;
        case 0: drc = QRectF(cp.x() -s - sz, cp.y() - s - sz, sz, sz); break;
        }

        color.setAlpha(static_cast<int>(opq * 255.0));
        p->setBrush(QBrush(color));

        p->drawRect(drc);
    }

    color.setAlpha(255);
}

void CustomItem::onSizeChanged()
{
    setX((parentItem()->width() - width()) / 2.0);
    setY((parentItem()->height() - height()) / 2.0);
}
2018/11/06