PySide (Qt for Python) は、Qt(キュート)の Python バインディングで、GUI などを構築するためのクロスプラットフォームなライブラリです。Linux/X11, macOS および Microsoft Windows をサポートしています。配布ライセンスは LGPL で公開されています。
QThread を利用したスレッド処理のサンプルは、本ブログ記事で既に取り上げていますが [1]、使い出すと、あれもこれもと応用したくなるので、わかりやすいサンプルを用意して備忘録としてまとめました。
時間が掛かる処理でも GUI が固まらないアプリにするために QThread を利用する。
下記の OS 環境で動作確認をしています。
![]() |
Fedora Linux 36 beta | x86_64 |
Python | 3.10.4 | |
PySide6 | 6.3.0 |
ベースとするサンプル
サンプルでは、スレッド処理の部分をダミーで sleep 関数を使うというのもアリですが、具体的な例を扱いたかったので、ファイルをダウンロードするサンプルにしました。ある程度のサイズがないとダウンロードがすぐに終わってしまうので、160MB 程度の PLD Linux を固定でダウンロードすることにしました。ダウンロード元は KDDI 総合研究所の FTP サイトを利用させていただきました。
#!/usr/bin/env python | |
# coding: utf-8 | |
# Reference: | |
# https://stackoverflow.com/questions/1517616/stream-large-binary-files-with-urllib2-to-file | |
import shutil | |
import urllib.request | |
from benchmark import time_elapsed | |
@time_elapsed | |
def download(url): | |
filename = url[url.rfind('/') + 1:] | |
status = True | |
length = 16 * 1024 | |
try: | |
req = urllib.request.urlopen(url) | |
with open(filename, 'wb') as fp: | |
try: | |
shutil.copyfileobj(req, fp, length) | |
except Exception as e: | |
print(e) | |
status = False | |
except OSError as e: | |
print(e) | |
status = False | |
return status | |
if __name__ == '__main__': | |
url = 'https://ftp.kddilabs.jp/Linux/distributions/PLD/iso/2.0/i386/pld-2.0-MINI.i386.iso' | |
result = download(url) | |
print(result) |
ダウンロードに掛かる時間を把握したかったので、デコレータで計測しています。デコレータの中身は以下のとおりです。
#!/usr/bin/env python | |
# coding: utf-8 | |
import time | |
from functools import wraps | |
# Reference: | |
# https://qiita.com/hisatoshi/items/7354c76a4412dffc4fd7 | |
def time_elapsed(func): | |
@wraps(func) | |
def wrapper(*args, **kargs): | |
start = time.time() | |
result = func(*args, **kargs) | |
elapsed = time.time() - start | |
print('{:s} function took {:.3f} sec'.format(func.__name__, elapsed)) | |
return result | |
return wrapper |
さて sample_download.py を PyCharm 上で実行すると、プロジェクトのディレクトリ内に指定した URL のファイル (pld-2.0-MINI.i386.iso) がダウンロードされます。自分の使っている環境では 10 秒前後でダウンロードが完了します(ばらつきがあります)。
/home/bitwalk/Projects/PySide6_sample/venv/bin/python /home/bitwalk/Projects/PySide6_sample/sample_download.py download function took 8.409 sec True プロセスは終了コード 0 で終了しました
このサンプルを GUI 化します。
PySide6 による GUI サンプル
GUI 化するサンプルは、プッシュボタンをクリックするとファイルのダウンロードを開始して、ダウンロードの間に進捗ダイアログ (QProgressDialog) を表示して、ダウンロードが完了したらダイアログを閉じるというだけのシンプルなものです。
使用するファイルおよびクラス(関数)は下記の通りです。前述のダウンロード用の download 関数も利用します。
- qt_thread.py (Example)
- qt_thread_progress.py (EndlessProgressDialog)
- qt_thread_download.py (URLDownload, URLDownloadWorker)
- sample_download.py (download)
サンプルの動作
まずサンプルの動作を説明します。
サンプル (qt_thread.py) を実行すると Download large file と表示されたボタンが表示されます。
このボタンをクリックすると、進捗ダイアログが表示され、ダウンロードが始まります。
このダイアログはダウンロードの開始と完了をモニターしているだけで、途中の進捗はモニターしていません。そのため進捗 (progress) と言っても、進捗バーが延々と回っているだけの仕様にしています。また、ダウンロード中のキャンセル処理は考慮していません。
このダイアログを modal window にして、ダイアログが表示されている間は、メインのプッシュボタンのウィンドウを操作できないようにしています。
ダウンロードが完了すると、進捗ダイアログが閉じられ、メインのウィンドウの操作ができるようになります。
折角、時間が掛かる処理をスレッド化しているのですから、別スレッドで処理させている間に他の操作ができるようにすべきでしょうが、内容をできるだけシンプルにするために、このような面白くもないサンプルにしました。
qt_thread.py
GUI のメインウィンドウを作成するメインプログラムです。 QMainWindow クラスを継承しています。
#!/usr/bin/env python | |
# coding: utf-8 | |
import sys | |
import PySide6 | |
from PySide6.QtWidgets import ( | |
QApplication, | |
QMainWindow, | |
QPushButton, | |
QStatusBar, | |
) | |
from qt_thread_download import URLDownload | |
from qt_thread_progress import EndlessProgressDialog | |
class Example(QMainWindow): | |
statusbar: QStatusBar = None | |
msec = 3000 | |
obj = None | |
# sample file to download | |
url = 'https://ftp.kddilabs.jp/Linux/distributions/PLD/iso/2.0/i386/pld-2.0-MINI.i386.iso' | |
def __init__(self): | |
super().__init__() | |
self.init_ui() | |
self.setWindowTitle('QThread') | |
# PySide6 version | |
print('PySide', PySide6.__version__) | |
def init_ui(self): | |
# push button | |
button = QPushButton('Download large file') | |
button.clicked.connect(self.on_click) | |
button.setStatusTip('click to start downloading') | |
self.setCentralWidget(button) | |
# status bar | |
self.statusbar = QStatusBar() | |
self.statusbar.showMessage('Welcome!', self.msec) | |
self.setStatusBar(self.statusbar) | |
def on_click(self): | |
# show progress dialog | |
self.dlg = EndlessProgressDialog(self) | |
self.dlg.show() | |
# update status | |
self.statusbar.showMessage('downloading, ...') | |
# instance for download in thread | |
self.obj = URLDownload(self.url) | |
self.obj.completed.connect(self.download_finish) | |
self.obj.start() | |
def download_finish(self, success: bool): | |
print(success) | |
# stop and delete dialog | |
self.dlg.cancel() | |
# update status | |
self.statusbar.showMessage('finish downloading', self.msec) | |
def main(): | |
app = QApplication(sys.argv) | |
hello = Example() | |
hello.show() | |
sys.exit(app.exec()) | |
if __name__ == '__main__': | |
main() |
プッシュボタンをクリックしたときの処理を on_click メソッドで定義しています。
ファイルをダウンロードする処理は URLDownload クラスのインスタンス self.obj が担い、start メソッドで他スレッドでダウンロード処理を行います。
def on_click(self): # 進捗ダイアログの表示 self.dlg = EndlessProgressDialog(self) self.dlg.show() # ステータスバーの表示を更新 self.statusbar.showMessage('downloading, ...') # スレッドで処理する URLDownload クラスのインスタンスを作成し、start メソッドで起動 self.obj = URLDownload(self.url) self.obj.completed.connect(self.download_finish) self.obj.start()
self.obj の処理が終了すると completed シグナルが emit されるので、それを受けて終了処理をするようにしておきます。
終了処理 (self.download_finish) では、completed シグナルが emit された時の引数の値(この例では bool 型の変数)を受け取り、その内容を表示、ダイアログの終了やステータスバーに表示するメッセージを更新しています。
def download_finish(self, success: bool): # ダウンロードの成否を標準出力 print(success) # 進捗ダイアログを終了 self.dlg.cancel() # ステータスバーの表示を更新 self.statusbar.showMessage('finish downloading', self.msec)
qt_thread_progress.py
進捗ダイアログでありながら、詳細な進捗を表示しない、キャンセルボタンを表示しない、modal window にしてメインのウィンドウでの操作をさせない、という進捗ダイアログらしからぬ EndlessProgressDialog クラスを、QProgressDialog を継承して定義しています。
#!/usr/bin/env python | |
# coding: utf-8 | |
from PySide6.QtCore import Qt | |
from PySide6.QtWidgets import QProgressDialog | |
class EndlessProgressDialog(QProgressDialog): | |
def __init__(self, parent): | |
super().__init__(labelText='Working...', parent=parent) | |
self.setWindowModality(Qt.WindowModal) | |
self.setCancelButton(None) | |
self.setRange(0, 0) | |
self.setWindowTitle('in progress') |
qt_thread_download.py
別スレッドでファイルをダウンロードするクラスを二つ定義しています。
#!/usr/bin/env python | |
# coding: utf-8 | |
from PySide6.QtCore import ( | |
QObject, | |
QThread, | |
Signal, | |
) | |
from sample_download import download | |
class URLDownload(QObject): | |
completed = Signal(bool) | |
def __init__(self, url): | |
super().__init__() | |
self.thread = QThread() | |
self.worker = URLDownloadWorker(url) | |
def start(self): | |
# move this instance to other thread | |
self.worker.moveToThread(self.thread) | |
# event handling | |
self.thread.started.connect(self.worker.run) | |
self.worker.finished.connect(self.thread.quit) | |
self.worker.finished.connect(self.worker.deleteLater) | |
self.thread.finished.connect(self.thread.deleteLater) | |
self.worker.downloadCompleted.connect(self.end) | |
# start the thread which starts self.worker.run | |
self.thread.start() | |
def end(self, success: bool): | |
self.completed.emit(success) | |
class URLDownloadWorker(QObject): | |
downloadCompleted = Signal(bool) | |
finished = Signal() | |
def __init__(self, url): | |
super().__init__() | |
self.url = url | |
def run(self): | |
status = download(self.url) | |
self.downloadCompleted.emit(status) | |
self.finished.emit() |
URLDownload
QObject を継承した URLDownload クラスは、ダウンロードに関わるスレッドの処理をします。すわわち、ダウンロード処理をするインスタンスの処理を、別スレッドへ移す処理をしています。
ます、コンストラクタで、別スレッド(QThread クラス)のインスタンス self.thread とダウンロード処理(URLDownloadWorker クラス)をするインスタンスを、ダウンロード元の情報 (url) を引数にして定義します。
def __init__(self, url): super().__init__() self.thread = QThread() self.worker = URLDownloadWorker(url)
QThread は、start()(スロットと呼ばれる、connect に紐づくメソッド)を実行すると、スレッドのインスタンスの run() を呼び出して、スレッドの実行を開始します。
そのため、URLDownload クラスも start() メソッドを用意して、その中で、まず QThread のインスタンスの start() を実行するための準備をしてから、スレッドの実行を開始します。
def start(self): # スレッド処理するインスタンスを、別スレッドに移動 self.worker.moveToThread(self.thread) # スロットの処理 self.thread.started.connect(self.worker.run) self.worker.finished.connect(self.thread.quit) self.worker.finished.connect(self.worker.deleteLater) self.thread.finished.connect(self.thread.deleteLater) self.worker.downloadCompleted.connect(self.end) # run() を呼び出して、スレッドの実行を開始 self.thread.start()
ダウンロードを処理するインスタンス self.worker を、moveToThread(self.thread) で別スレッドへ移します。
その上で、別スレッドの実行が開始されたとき (started) に self.worker.run を実行すること、処理が終わったとき (finished) の処理(インスタンスの破棄を含む)を定義し、さらに、カスタム処理(この場合、self.worker.downloadCompleted))を定義します。
最後に self.thread.start() を実行して、別スレッドでの処理を始めます。
end メソッドではself.worker.downloadCompleted) に対応する処理を定義しています。と言っても、スレッド処理の完了時の結果を、completed シグナルを emit して、呼び出し元に引き渡すだけの処理です。
def end(self, success: bool): self.completed.emit(success)
URLDownloadWorker
URLDownloadWorker クラスは、ダウンロード処理をします。シグナルとスロットでスレッド間のやりとりをするため、QObject を継承してます。
シグナルは、下記のように2種類、クラス変数で用意しておきます。
downloadCompleted = Signal(bool) finished = Signal()
run メソッドでは、冒頭で紹介したダウンロード用の download 関数を実行後、self.downloadCompleted シグナルを emit してダウンロードの成否を返して、終了の self.finished シグナルを emit しています。
def run(self): status = download(self.url) self.downloadCompleted.emit(status) self.finished.emit()
シグナルを一応、self.downloadCompleted と self.finished に分けてはいますが、まとめられるかもしれません。
まとめ
PySide6 を利用した GUI プログラミングで、スレッド化するために必要な最低限の処理をまとめました。そのため、スレッド処理を中断する場合などは、ここでは全然考慮していません。この記事を書いているときに、もっと詳しく説明されているサイト [2] を見つけましたので、ご興味のある方は、参考にされてはいかがでしょうか。
参考サイト
- bitWalk's: Matplotlib と QThread [2022-03-05]
- [PySide] QThread を使って時間のかかる処理をスレッド化する - へっぽこプログラマーの備忘録 [2022-02-20]

にほんブログ村
0 件のコメント:
コメントを投稿