どうもです、タドスケです。
新しい職場で Qt(Pyside6)を使い始めて1年ちょっと。
様々な処理が簡単に書けていいなぁと思う反面、気を付けて実装しなかったために問題を起こしてしまったこともありました。
この記事では、僕が現場で実際にやらかした間違いを紹介します。
(僕がやらかすたびに記事が増えていきます)
Qt を学ばれる方にとって、参考になれば幸いです。
QPixmap の生成をメインスレッド外でやってしまう
画像を GUI 上に表示できる QPixmap。
画像の読み込みは時間がかかるため、非同期(メインスレッド外)で行われることが多いです。
そこで僕がやらかしたのは、メインスレッド外で呼ばれる関数の中で QPixmap の生成を行ってしまうパターンでした。
ダメな例
"""QPixmap の生成をメインスレッド外で行ってしまうサンプル:ダメな例."""
import concurrent.futures
from PySide6.QtGui import QPixmap
from PySide6.QtWidgets import QApplication, QLabel
_IMAGE_FILE = '../resource/face.png'
def _load_pixmap():
"""Pixmap を読み込む."""
return QPixmap(_IMAGE_FILE)
if __name__ == '__main__':
app = QApplication([])
# QPixmap をメインスレッド外で生成
executor = concurrent.futures.ThreadPoolExecutor()
future = executor.submit(_load_pixmap)
pixmap = future.result()
label = QLabel()
label.setPixmap(pixmap)
label.show()
app.exec()
Qt に限らず、GUI 要素はメインスレッドで操作するのがお決まりらしいです。
とはいえ、時間のかかる画像読み込みを非同期で行いたいのは変わりません。
そんなときは以下の方法で、必要最小限の操作のみをメインスレッド上で行えます。
- 非同期で QImage を読み込む
- 読み込んだ QImage を使って、メインスレッド側で QPixmap を生成する
QImage は画像をメモリ上で表現したものなので、メインスレッド外で扱っても大丈夫です。
この方法で実装した例が以下です。
良い例
"""QPixmap の生成をメインスレッド外で行ってしまうサンプル:よい例."""
import concurrent.futures
from PySide6.QtGui import QPixmap, QImage
from PySide6.QtWidgets import QApplication, QLabel
_IMAGE_FILE = '../resource/face.png'
def _load_image():
"""QImage を読み込む."""
return QImage(_IMAGE_FILE)
if __name__ == '__main__':
app = QApplication([])
# QImage をメインスレッド外で生成
executor = concurrent.futures.ThreadPoolExecutor()
future = executor.submit(_load_image)
image = future.result()
# メインスレッド上で QImage から QPixmap を生成
pixmap = QPixmap.fromImage(image)
label = QLabel()
label.setPixmap(pixmap)
label.show()
app.exec()
QObject の親を設定し忘れる
Qt 上で表示されるウィンドウやボタンなどは、QObject というクラスを継承しています。
QObject は、親子関係を持つオブジェクトツリー上で管理されおり、親が削除されると子も自動で削除されるようになっています。
オブジェクトの親子関係は、生成時の parent 引数や setParent 関数で設定できます。
ここでよくあるミスが、parent を設定し忘れてしまうことです。
親のいないオブジェクトは、親が削除された後も残り続けてしまいます。
ダメな例
"""オブジェクトに親を設定し忘れるサンプル:悪い例."""
from PySide6.QtWidgets import QApplication, QWidget, QPushButton
app = QApplication([])
window = QWidget()
# 親を指定せずにボタンを生成
button = QPushButton("Click Me!")
button.destroyed.connect(lambda _: print('destroyed'))
# window の削除予約
window.deleteLater()
window.show()
app.exec()
# destroyed は表示されない:Button はまだ残っている
これを防ぐには、オブジェクト生成時に parent 引数を指定します。
良い例
"""オブジェクトに親を設定し忘れるサンプル:良い例."""
from PySide6.QtWidgets import QApplication, QWidget, QPushButton
app = QApplication([])
window = QWidget()
# 親を指定してボタンを生成
button = QPushButton("Click Me!", parent=window)
button.destroyed.connect(lambda _: print('destroyed'))
# window(親) の削除予約
window.deleteLater()
window.show()
app.exec()
# destroyed が表示される:Button はもう残っていない
特別な理由がない限り、基本は親オブジェクトを設定しておいたほうが、問題は起こりにくいかと思います。
僕のいる現場では、独立して出てくるメッセージダイアログなども、メインウィンドウの子にしています。
こうしないと、「アプリを複数起動した際に、誰が親なのかがわかりにくくなってしまう」という問題が起きます。
コメント