マルチスレッドとQMLの取り扱い

Multithreading on QML
操作性をより重視したスマートフォン向けOSでは、メインスレッドがビジー状態になると有無を言わさずアプリをシャットダウンしたり、別スレッドで呼び出さないと動作しないAPIなどがあるため、マルチスレッド処理はアプリ開発において不可欠のテクニックです。

C++でQMLコンポーネントを作成するで取り上げたQTimerクラスの扱い方と同じように、Qt Quickにおけるマルチスレッド処理では、スレッドの開始と終了のタイミングをシグナルとして受け取り、その内部で処理する必要があります。

こちらはスレッドの作成例です。newによりQThreadオブジェクトを作成したら、QThread::start()シグナルとスレッドループの関数をつなぎます。また、QThread::finished()シグナルとQThread::deleteLater()を紐付けることで、スレッドが完了したら自動でオブジェクトを破棄されるようにしています。
newthread.cpp
void ThreadTest::execThread()
{
    QThread *thread = new QThread(this);
    connect(thread, SIGNAL(started()), this, SLOT(onStartThread()), Qt::DirectConnection);
    connect(thread, SIGNAL(finished()), SLOT(deleteLater()));
    thread->start();
}
引数にある「Qt::DirectConnection」はスレッドを直に実行するオプションです。これを「Qt::BlockingQueuedConnection」にすると、キューがタスクに追加され、既存のスレッドが完了するまで待機状態になります。

スレッドを中断させたいときはQThread::requestInterruption()を使い、スレッド側で「QThread::isInterruptionRequested()==true」であるかどうかを調べます。スレッドの完了を待機したいのであれば、QThread::quit()を呼び出してからQThread::wait()を実行させます。
exitthread.cpp

void ThreadTest::exitThread()
{
    // 中断要求を出して、スレッドが完全に終了するのを待機する
    thread->requestInterruption();
    thread->quit();
    thread->wait();
}

void ThreadTest::onExecThread()
{
    if(thread){
        // 既存のスレッドが実行状態なら何もしない
        if(thread->isRunning() == true) return;
        exitThread();
    }

    thread = new QThread(this);
    connect(thread, SIGNAL(started()), this, SLOT(onStartThread()), Qt::DirectConnection);
    connect(thread, SIGNAL(finished()), SLOT(deleteLater()));
    thread->start();
}

void ThreadTest::onStartThread()
{
    while(true){
        // 現在のスレッドで中断要求があればループから抜ける
        if(QThread::currentThread()->isInterruptionRequested() == true) return;
        // スレッド内での処理
    }
}
別スレッドからでもQMLのプロパティを取得・変更することはできますが、QMLのメソッドを呼び出すことはできません。異なるスレッドからUIスレッドを操作したい場合は「QMetaObject::invokeMethod」を使います。QMLの内部はJavaScriptである関係上、引数はQVariant型に変換してvarとして渡す必要があります。
access.qml
import QtQuick 2.9
import QtQuick.Controls 1.0

Item{
    id: w
    signal execThread()
    property alias progressVal: pgb.value

    Column{
        width: parent.width
        anchors.verticalCenter: parent.verticalCenter

        Button{
            id: btn
            objectName: "btn"
            anchors.horizontalCenter: parent.horizontalCenter
            text:"Execute"
            onClicked: execThread()
            function updateText(v){
                btn.text = v;
            }
        }

        ProgressBar{
            id: pgb
            anchors.horizontalCenter: parent.horizontalCenter

            width: parent.width * 0.8
            maximumValue: 1000
            minimumValue: 0
        }
    }
}
access.cpp
class MainView : public QQuickView
{
    Q_OBJECT
private:
    QThread *pgb_thread = nullptr;
public:
    MainView();
    void exitThread();

public slots:
    void onExecThread();
    void onStartThread();
};

MainView::MainView()
{   
    connect(this->rootObject(), SIGNAL(execThread()), this, SLOT(onExecThread()));
}

void MainView::exitThread()
{
    if(!pgb_thread) return;
    pgb_thread->requestInterruption();
    pgb_thread->quit();
    pgb_thread->wait();
}

void MainView::onExecThread()
{
    if(pgb_thread){
        if(pgb_thread->isRunning() == true) return;
        exitThread();
    }

    pgb_thread = new QThread(this);
    connect(pgb_thread, SIGNAL(started()), this, SLOT(onStartThread()), Qt::DirectConnection);
    connect(pgb_thread, SIGNAL(finished()), SLOT(deleteLater()));
    pgb_thread->start();
}

void MainView::onStartThread()
{   
    // QMLから特定のアイテムを検索する。
    // ただし、QMLとC++とのロジック分離の観点より、ルートではないアイテムを取得するのは本来好ましくない。
    // また、キーワードはobjectNameの値でありidでない点に注意!
    QQuickItem* btn = rootObject()->findChild<QQuickItem*>("btn");
    if(btn){
        QMetaObject::invokeMethod(btn, "updateText",
            Q_ARG(QVariant, QVariant::fromValue(QString("Running"))));
        return;
    }

    for(int i = 0; i < 1000; i++){
        if(QThread::currentThread()->isInterruptionRequested() == true) return;
        QThread::msleep(200);
        // プロパティを取得したい場合の例
        // int v = rootObject()->property("progressVal").toInt();
        rootObject()->setProperty("progressVal", i);
    }
}
2018/11/07