これは僕が実際に動かして感じた動作なので具体的な実装とは合ってない可能性が非常に高いです。
なにが起こったか。
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" }) }
コメント