QML(Qt)でListViewにWorkerScriptから追加する時の注意点

なんか変な位置に挿入される現象の対応



これは僕が実際に動かして感じた動作なので具体的な実装とは合ってない可能性が非常に高いです。

なにが起こったか。

1.UIスレッドからListViewへデータを追加。
 (2行追加した。)
 

2.WorkerScriptを使って裏スレッドからListViewへデータを追加
 (4行分追加した。ココまでは正常。)
 

3.もう一度、UIスレッドからデータ追加
 (パッと見は正常。)
 

4.もう一度、裏スレッドからデータを追加
 (3で追加したデータの前に挿入されたように見える。)
 

5.一回スクロールアウト
 (リストが再構成されて3で追加したデータが無かったことになってる。)
 


初めて裏スレッドでModelにアクセスした時点で裏用のデータのコピーが作成されてsync()をする度に表にプッシュされてくる感じ。
その際、表での操作を全く無視してくるのでUI側の操作は消える。


/// まとめると ///
・ListModelの中に表と裏でダブルバッファリングしているイメージ。
 (初めてWorkerScriptでアクセスした時点でコピーっぽい)
・UIからアクセスできるデータとスレッドからアクセスできるデータに分かれてる。
・sync()をすると裏のデータが表にプッシュされる。
・sync()の動作は一方通行で表側の変更を裏に動機をかけることはできないっぽい。
・プッシュされるとき、UIから操作した内容はなかったことにされる。
 ただし、最初に裏スレッドを動かす以前のものは残る。


/// 対策 ///
・常に裏スレッドから操作する。。。

ということで、WorderScriptを使ったListModelへのアクセス用エレメントを作って見ました。
サンプルを今回の現象とラッパーを見れるように作りました。
基本は前回の「QML(Qt)でマルチスレッド処理」です。


余談ですが、
ListModelの中にWorkerScriptを置こうとすると、「ListElement: cannot contain nested elements 」とエラーが表示されてすでに内部に持っている実装になっている様子。

サンプルメインのQML
import QtQuick 1.0

Rectangle {
    id: _root
    width: 300
    height: 360
    // ボタン
    Row{
        id: _btnArea
        anchors.top: parent.top
        width: parent.width

        // WorkerScript1個目から追加
        Rectangle{
            width: parent.width / parent.children.length
            height: _lavel.paintedHeight * 2
            color: "#ddddff"
            border.color: "#555555"
            border.width: 1
            Text{
                id: _lavel
                anchors.centerIn: parent
                text: "from Back 1"
            }
            MouseArea{
                anchors.fill: parent
                onClicked: {
                    console.debug("from background");
                    // スレッド開始
                    _thread1.sendMessage({"model":_list.model
                                            , "id": 1
                                            , "number": 2
                                            , "string": "Start!"
                                        });
                }
            }
        }

        // WorkerScript2個目から追加
        Rectangle{
            width: parent.width / parent.children.length
            height: _lavel2.paintedHeight * 2
            color: "#ddddff"
            border.color: "#555555"
            border.width: 1
            Text{
                id: _lavel2
                anchors.centerIn: parent
                text: "from Back 2"
            }
            MouseArea{
                anchors.fill: parent
                onClicked: {
                    console.debug("from Background 2");
                    // スレッド開始
                    _thread2.sendMessage({"model": _list.model
                                            , "id": 2
                                            , "number": 2
                                            , "string": "set null"
                                        });
                }
            }
        }

        // ラッパーエレメント使ってスレッドから追加
        Rectangle{
            width: parent.width / parent.children.length
            height: _lavel3.paintedHeight * 2
            color: "#ddddff"
            border.color: "#555555"
            border.width: 1
            Text{
                id: _lavel3
                anchors.centerIn: parent
                text: "from Back 3"
            }
            MouseArea{
                anchors.fill: parent
                onClicked: {
                    console.debug("from Background 3");
                    // 追加
                    _listManager.append({"_message": "3 : x : "
                                + Qt.formatDateTime(new Date(), "yyyy/MM/dd hh:mm:ss")});
                }
            }
        }

        // UIスレッドから追加するボタン
        Rectangle{
            width: parent.width / parent.children.length
            height: _lavel4.paintedHeight * 2
            color: "#ddddff"
            border.color: "#555555"
            border.width: 1
            Text{
                id: _lavel4
                anchors.centerIn: parent
                text: "from UI 2"
            }
            MouseArea{
                anchors.fill: parent
                onClicked: {
                    console.debug("from UI 2");
                    // 追加
                    _model.append({"_message": "UI 2 : "
                                + Qt.formatDateTime(new Date(), "yyyy/MM/dd hh:mm:ss")});
                }
            }
        }

    }

    // リストビューのデータ操作用エレメント
    ListModelManager{
        id: _listManager
        model: _list.model
    }

    // スレッド用エレメント
    WorkerScript {
        id: _thread1
        source: "script.js"     // スレッド処理をするスクリプトファイルの指定
        onMessage: {
            // Workerスレッドからの通信イベント
            console.debug("finish thread 1:" + messageObject.result
                          + ", split=" + messageObject.split);
        }
    }

    // スレッド用エレメント
    WorkerScript {
        id: _thread2
        source: "script.js"     // スレッド処理をするスクリプトファイルの指定
        onMessage: {
            // Workerスレッドからの通信イベント
            console.debug("finish thread 2:" + messageObject.result
                          + ", split=" + messageObject.split);
        }
    }

    // リストモデル
    ListModel{
        id: _model
    }
    // リスト用レイアウト
    Component{
        id: _delegate
        Rectangle {
            width: _root.width
            height: _text.paintedHeight * 2
            border.color: "#dddddd"
            border.width: 1
            Text{
                id: _text
                anchors.centerIn: parent
                text: _message
            }
        }

    }
    // リスト
    ListView{
        id: _list
        anchors.top: _btnArea.bottom
        anchors.left: _root.left
        anchors.right: _root.right
        anchors.bottom: _root.bottom
        clip: true
        model: _model
        delegate: _delegate

        onCountChanged: {
            // 自動スクロール
//            positionViewAtEnd();  // コレ使うなら1.1に変更
        }
    }
}


WorderScriptから使用するスクリプト
// scripts.js
WorkerScript.onMessage = function(message) {
    console.debug("start worker script:" + message.number
                  + "," + message.string);

    if(message.model === null){
        // nop
    }else{
        var now = 0;
        var split = new Date();

        message.model.sync();

        for(var i=0; i<message.number; i++){
            delay(1000);

            now = new Date();
            message.model.append({"_message": message.id + " : "
                                + i + " : "
                                + Qt.formatDateTime(now, "yyyy/MM/dd hh:mm:ss")});
            message.model.sync();
        }
        split = (new Date()) - split;
    }

    // メインスレッドへの応答
    WorkerScript.sendMessage({ "result": "tRue", "split": split })
}

// ウエイトを入れる
function delay(msec){
    var start = 0;
    start = new Date();
    do{
        now = new Date();
    }while((now - start) < msec);
}


常にスレッドから操作するためのラッパーエレメント
import QtQuick 1.0

WorkerScript {
    id: _root
    source: "./ListModelManagerScript.js"   // スレッド処理をするスクリプトファイルの指定

    property variant model: null
    property int count: (model === null) ? 0 : model.count

    // Workerスレッドからの通信イベント
    onMessage: {
        console.debug("finish thread : " + messageObject.result);
    }

    // データチェック
    function isOk(object){
        if((model === null) || (object === null)
                || (object === undefined)){
            return false;
        }else{
            return true;
        }
    }

    // 追加する
    function append(object){
        if(!isOk(object)){
            // nop
        }else{
            sendMessage({"model": model
                            , "type": "append"
                            , "object": object
                               });
        }
    }

    // クリアする
    function clear(){
        if(!isOk(true)){
            // nop
        }else{
            sendMessage({"model": model
                            , "type": "clear"
                            });
        }
    }

    // 取得する
    function get(index){
        if(!isOk(true)){
            return null;
        }else{
            return model.get(index);
        }
    }

    // 挿入する
    function insert(index, object){
        if(!isOk(object)){
            // nop
        }else{
            sendMessage({"model": model
                            , "type": "insert"
                            , "index": index
                            , "object": object
                               });
        }
    }

    // 移動する
    function move(from, to, n){
        if(!isOk(true)){
            // nop
        }else{
            model.move(from, to, n);
//            sendMessage({"model": model
//                            , "type": "move"
//                            , "from": from
//                            , "to": to
//                            , "n": n
//                               });
        }
    }

    // 内容を変更する
    function set(index, object){
        if(!isOk(object)){
            // nop
        }else{
            sendMessage({"model": model
                            , "type": "set"
                            , "index": index
                            , "object": object
                               });
        }
    }

    // プロパティの変更
    function setProperty(index, property, value){
        if(!isOk(value)){
            // nop
        }else{
            sendMessage({"model": model
                            , "type": "setProperty"
                            , "index": index
                            , "property": property
                            , "value": value
                               });
        }
    }

    // 同期
    function sync(){
        sendMessage({"model": model, "type": "sync"});
    }
}


常にスレッドから操作するためのラッパーエレメント用のスレッドスクリプト
WorkerScript.onMessage = function(message) {
    console.debug("start worker script");

    if(message.model === null){
        // nop
    }else{
        switch(message.type){
        case "append":
            message.model.append(message.object);
            break;
        case "clear":
            message.model.clear();
            break;
        case "insert":
            message.model.insert(message.index, message.object);
            break;
        case "move":
            message.model.move(message.from, message.to, message.n);
            break;
        case "set":
            message.model.set(message.index, message.object);
            break;
        case "setProperty":
            message.model.setProperty(message.index, message.property, message.value);
            break;
        default:
            break;
        }

        message.model.sync();
    }

    // メインスレッドへの応答
    WorkerScript.sendMessage({ "result": "true" })
}