More than 1 year has passed since last update.

[2016/08/17 19:51 - 少し手直し]
Qtをさわり始めてQMLとC++のバインディングで引っかかったのでまとめてみました。間違いやもっと良い方法があればご指摘下さい。

以下では、QtCreatorでQtQuickアプリケーションのプロジェクトを作成した際に自動で生成される下記のソースコードをベースにします。(ここではui.qmlは使用しません)

main.cpp
#include <QGuiApplication>
#include <QQmlApplicationEngine>

int main(int argc, char *argv[])
{
    QGuiApplication app(argc, argv);

    QQmlApplicationEngine engine;
    engine.load(QUrl(QStringLiteral("qrc:/main.qml")));

    return app.exec();
}
main.qml
import QtQuick 2.5
import QtQuick.Window 2.2

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

    MouseArea {
        anchors.fill: parent
        onClicked: {
            Qt.quit();
        }
    }

    Text {
        text: qsTr("Hello World")
        anchors.centerIn: parent
    }
}

C++からQMLオブジェクトへアクセス

いろいろな方法があるようですが、ここではobjectNameでアクセスする方法を取り上げます。

main.cppではroot要素へのアクセスを経て、子要素であるTextへQML側で定義するobjectNameでアクセスする。

main.cpp
#include <QGuiApplication>
#include <QQmlApplicationEngine>

int main(int argc, char *argv[])
{
    QGuiApplication app(argc, argv);

    QQmlApplicationEngine engine;
    engine.load(QUrl(QStringLiteral("qrc:/main.qml")));

    /* added */
    //(1) Get an access to root object and qml object by objectName "textObject" defined in main.qml
    QObject *rootObject = engine.rootObjects().first();
    QObject *qmlObject = rootObject->findChild<QObject*>("textObject");
    // (2) set an object property
    qmlObject->setProperty("text", "Text from C++");
    //----------

    return app.exec();
}

QML側ではアクセスしたい子要素にobjectNameを定義する。

main.qml
import QtQuick 2.5
import QtQuick.Window 2.2

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

    MouseArea {
        anchors.fill: parent
        onClicked: {
            Qt.quit();
        }
    }

    Text {
        /*added*/
        objectName: textObject
        // ----------
        text: qsTr("Hello World")
        anchors.centerIn: parent
    }
}

結果
cpp2qml.png
(見にくかったので色をつけてフォントを大きくしています)

なお、オフィシャルではC++からQMLオブジェクトへのアクセスはC++とQMLの独立性の観点から推奨していないようです。

Warning: While it is possible to use C++ to access and manipulate QML objects deep into the object tree, we recommend that you do not take this approach outside of application testing and prototyping. One strength of QML and C++ integration is the ability to implement the QML user interface separately from the C++ logic and dataset backend, and this strategy breaks if the C++ side reaches deep into the QML components to manipulate them directly. This would make it difficult to, for example, swap a QML view component for another view, if the new component was missing a required objectName. It is better for the C++ implementation to know as little as possible about the QML user interface implementation and the composition of the QML object tree.

QMLからC++のクラスへアクセス

ContextPropertyにQObjectインスタンスを埋め込んで、QMLからメソッドをコールする例を挙げてみます。(オフィシャルドキュメントではQDateTimeを返すようになっていましたがもっとシンプルにstringを返す形にしてみました)

まずヘッダファイルの準備。

applicationdata.h
#ifndef APPLICATIONDATA_H
#define APPLICATIONDATA_H

#include <QObject>
#include <QString>

class ApplicationData:public QObject
{
    Q_OBJECT
public:
    Q_INVOKABLE QString getTextFromCpp(){
        return QString("This is the text from C++");
    }
};

#endif // APPLICATIONDATA_H

続いて、cppファイルとqmlファイルを変更。

main.cpp
#include <QGuiApplication>
#include <QQmlApplicationEngine>

/*added*/
#include <QQmlContext>
#include "applicationdata.h"
//----------

int main(int argc, char *argv[])
{
    QGuiApplication app(argc, argv);

    QQmlApplicationEngine engine;
    engine.load(QUrl(QStringLiteral("qrc:/main.qml")));

    /*added*/
    ApplicationData data;
    engine.rootContext()->setContextProperty("applicationData",&data);
    //----------

    return app.exec();
}

main.qml
import QtQuick 2.5
import QtQuick.Window 2.2

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

    MouseArea {
        anchors.fill: parent
        onClicked: {
            Qt.quit();
        }
    }

    Text {
        /*changed*/
        //text: qsTr("Hello World")
        text: applicationData.getTextFromCpp()
        //----------
        anchors.centerIn: parent
    }


}

結果
qml2cpp.png

Signal & Slot

今回は、QMLのボタンクリックでC++側へシグナルを出し、C++側のスロットで受け取ったら、逆にC++側からQML側へシグナルを送るというのを例にします。

C++でConnect

まずは、C++側でConnectする例。cppからのシグナルでQML側のSlotが認識されない、No such slotというエラーで泣かされました。引数をQVariantにするとうまくいく。

main.qml
import QtQuick 2.5
import QtQuick.Window 2.2
/*added*/
import QtQuick.Controls 2.0
//----------

Window {
    /*added*/
    signal qmlSignal(string msg);
    function qmlSlot(text)
    {
        console.log("qmlSlot is called with the text: " + text)
        textField.text = text;
    }
    //----------

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

    /*deleted*/
//    MouseArea {
//        anchors.fill: parent
//        onClicked: {
//            Qt.quit();
//        }
//    }

    Text {
        /*added*/
        id:textField
        //----------
        text: qsTr("Hello World")
        anchors.centerIn: parent
    }

    /*added*/
    Button{
        id:aButton
        text:"Emit Signal!"
        anchors.centerIn: parent
        anchors.verticalCenterOffset: 30
        onClicked: qmlSignal("Hello from QML")
    }
    //----------

}
cppsignalslot.h
#ifndef CPPSIGNALSLOT_H
#define CPPSIGNALSLOT_H

#include <QObject>
#include <QVariant>

class CppSignalSlot : public QObject
{
    Q_OBJECT
public:
    explicit CppSignalSlot(QObject *parent = 0);

signals:
    void cppSignal(QVariant text);

public slots:
    void cppSlot(QString msg);
};

#endif // CPPSIGNALSLOT_H
cppsignalslot.cpp
#include "cppsignalslot.h"
#include <QDebug>

CppSignalSlot::CppSignalSlot(QObject *parent) : QObject(parent)
{

}

void CppSignalSlot::cppSlot(QString msg)
{
    qDebug() << "cppSlot is called with the message: " << msg;
    emit cppSignal("Hello from C++");
}
main.cpp
#include <QGuiApplication>
#include <QQmlApplicationEngine>
/*added*/
#include <QQmlContext>
#include "cppsignalslot.h"
//----------

int main(int argc, char *argv[])
{
    QGuiApplication app(argc, argv);

    QQmlApplicationEngine engine;
    engine.load(QUrl(QStringLiteral("qrc:/main.qml")));

    CppSignalSlot obj;

    QObject *root = engine.rootObjects().first();

    // Connect QML Signal to C++ Slot
    QObject::connect(root, SIGNAL(qmlSignal(QString)),
                     &obj,   SLOT(cppSlot(QString)));
    // Connect C++ Signal to QML Slot
    QObject::connect(&obj,   SIGNAL(cppSignal(QVariant)),
                     root, SLOT(qmlSlot(QVariant)));

    return app.exec();
}

結果
クリック前
signalslot_beforeClicking.png

クリック後
signalslot_afterClicked.png
(上のセクションとwindowが違うのは試験環境が違うためです。。。それ以上の理由はありません)

[コンソール出力]
cppSlot is called with the message: "Hello from QML"
qml: qmlSlot is called with the text: Hello from C++

QMLでConnect

次は同じ例でQML側でConnectする例です。後述の通り、Cannot assign to non-existent property とReferenceErrorに泣かされました。

main.qml
import QtQuick 2.5
import QtQuick.Window 2.2
import QtQuick.Controls 2.0

Window {
    visible: true
    width: 640
    height: 480
    title: qsTr("Hello World")
    /*deleted*/
//    MouseArea {
//        anchors.fill: parent
//        onClicked: {
//            Qt.quit();
//        }
//    }

    Text {
        id: textField
        text: qsTr("Hello World")
        anchors.centerIn: parent
    }

    Button{
        text:"Emit Signal!"
        anchors.centerIn: parent
        anchors.verticalCenterOffset: 30
        onClicked: cppSignalSlot.cppSlot("Hello from QML")
    }

    Connections
    {
        target:cppSignalSlot
        onCppSignal:{
            console.log("received cppSignal:" + text)
            textField.text = text;
        }
    }
}
cppsignalslot.h
#ifndef CPPSIGNALSLOT_H
#define CPPSIGNALSLOT_H

#include <QObject>

class CppSignalSlot : public QObject
{
    Q_OBJECT
public:
    explicit CppSignalSlot(QObject *parent = 0);

signals:
    void cppSignal(QString text);

public slots:
    void cppSlot(QString text);
};

#endif // SIGNALSLOT_H
cppsignalslot.cpp
#include "cppsignalslot.h"
#include <QDebug>

CppSignalSlot::CppSignalSlot(QObject *parent) : QObject(parent)
{

}

void CppSignalSlot::cppSlot(QString text)
{
    qDebug() << "cppSlot is called with text: " + text;
    emit cppSignal(QString("Hello from C++"));
}
main.cpp
#include <QGuiApplication>
#include <QQmlApplicationEngine>
#include <QQmlContext>
#include "cppsignalslot.h"

int main(int argc, char *argv[])
{
    QGuiApplication app(argc, argv);

    QQmlApplicationEngine engine;

    SignalSlot obj;
    // Put this before lading qml file!
    engine.rootContext()->setContextProperty("cppSignalSlot",&obj);

    engine.load(QUrl(QStringLiteral("qrc:/main.qml")));

    return app.exec();
}

ここでミソなのは、C++からcppSignal()でシグナルを出した時の動作をonCppSignalと書くことです。(要するにC++側はLower Camelで書いて、QML側はon+Upper Camelで書きましょうということみたいです)

あと、コメントにも書きましたが、main.cppでRoot ObjectのContextに設定する前にmain.qmlをロードすると、下記のエラーが出ます。(でも、動作はする。。。)

QML Connections: Cannot assign to non-existent property "OnCppSignal"
ReferenceError: cppSignalSlot is not defined

参考

そして、結局Qtのオフィシャルドキュメントが一番わかりやすいというオチ。。。