はんぎょねこの憂鬱

耳から変な汁が出てきた

PySide: Asynchronously Text Filtering

QThreadを使った非同期テキストフィルタ

ほしいもの
  • 別スレッドで実行されるテキストフィルタ
  • 一定時間ごとにフィルタリング済みのデータを返す
  • 途中でキャンセルもしたい
コード
from PySide import QtCore, QtGui
import datetime
import time

class FilteringThread(QtCore.QThread):
    completed = QtCore.Signal()

    def __init__(self):
        super().__init__()
        self._data = []  # フィルタリングする前のデータリスト
        self._filter = ''  # 部分一致キーワード
        self._queue = []  # フィルタ済みのデータ
        self._lastFlushed = datetime.datetime.now()  # 前回データを流した時間
        self._callback = None  # フィルタ済みデータを受け取る関数
        self._complete = True  # 全体の処理が完了したか判定する
        self._canceled = False  # 途中キャンセルがリクエストされたか

    def setFilter(self, text):
        # フィルタの設定
        self._filter = text

    def setData(self, data):
        # データの設定
        self._data = data

    def setCallback(self, func):
        # データ受取関数の設定
        self._callback = func

    def cancel(self):
        # キャンセルのリクエスト
        self._canceled = True

    def isCompleted(self):
        # 終了判定
        # isFinishedがいい感じで動かない
        return self._complete

    def run(self):
        # スレッドから実行される関数

        self._complete = False
        self._canceled = False
        if self._filter:
            for d in self._data:
                if self._canceled:
                    break

                time.sleep(0.001)  # フィルタリングに時間がかかってるふり

                if self._filter in d:
                    # フィルタを追加したらqueueにいれる
                    self._queue.append(d)

                    # 一定時間経過してたら流す
                    now = datetime.datetime.now()
                    if (now - self._lastFlushed).total_seconds() >= 1.0:
                        self._callback(self._queue)
                        self._queue.clear()
                        self._lastFlushed = now

        # 最後に残ったデータを流して終了
        self._callback(self._queue)
        self._queue.clear()
        self.completed.emit()
        self._complete = True
        self.exit()

# 実験用データ
DATA = [str(i) for i in range(0, 1000)]

# 必要なインスタンスを作る
app = QtGui.QApplication([])
listview = QtGui.QListWidget()
filtertext = QtGui.QLineEdit()
thread = FilteringThread()
message = QtGui.QLabel('')

# フィルタ変更時の処理
def textChanged(text):
    while not thread.isCompleted():
        thread.cancel()
        time.sleep(0.1)
    listview.clear()
    if text == '':
        return
    thread.setFilter(text)
    thread.start()
    message.setText('')

# フィルタ項目取得時
def flushed(queue):
    for i in queue:
        listview.addItem(QtGui.QListWidgetItem(i))

# 開始時
def started():
    message.setText('filteringなう...')

# 終了時
def completed():
    message.setText('{0} matching records'.format(listview.count()))

# 設定
listview.setUniformItemSizes(True)  # ちょっぴり速くなるよ
listview.setSortingEnabled(False)  # もしソートするならflushedで一回だけやる
filtertext.textChanged.connect(textChanged)
thread.setData(DATA)
thread.setCallback(flushed)
thread.started.connect(started)
thread.completed.connect(completed)

# 実行
widget = QtGui.QWidget()
widget.setLayout(QtGui.QVBoxLayout())
layout = widget.layout()
layout.addWidget(filtertext)
layout.addWidget(listview)
layout.addWidget(message)
widget.show()
app.exec_()

表示するデータがあまりに多くなると今度はListWidgetのclearやらpaintやらで時間がかかってしまうようになるっぽい。これはどうしたものか…