PySide2 で重い処理をしている間プログレスバーを表示する

 表題の通りの内容。最終コードだけ欲しい人はこちら

いきさつ

 PySide2 を使ってアプリケーションを作るとき、重めの処理を行うとその間UIが「応答なし」になってしまう。自分しか使わないツールならば別にこれでも良いのだけれど、やはり他の人が使う場合に、ソフトウェアが停止したのか処理中なのかわからない、というのはよろしくない。

 そこで、重い処理をしている間プログレスバーを表示したい。どのぐらい時間がかかるかわからない処理ではあるものの、処理中であることを明確にして、少なくともソフトウェアが停止したわけではないと見てわかるようにしたい。

処理を別スレッドにしたのにUIは「応答なし」になる

 処理中にプログレスバーを持ったダイアログを別窓として開き、終了時に自動的に閉じるようにしたい。そのためモードレスのダイアログを用意してそれを開く。最初に書いたコード(抜粋)がこちら。

# 仮の何か時間のかかる処理
def heavyProcess(self):
    time.sleep(10)

# 実行ボタンのハンドラ
def on_btn_clicked(self):
    # 時間のかかる処理を別スレッドで実行
    th = threading.Thread(target=self.heavyProcess, daemon=True)
    th.start()
    # 処理を開始したあとにメインスレッドでプログレスバーのダイアログを表示
    pg = ProgressDialog(self)
    pg.open()
    
    # Thread.join() で別スレッドの終了を待って
    th.join()
    # プログレスバーの窓を閉じる
    pg.accept()

 threading モジュールを使って別スレッドを立て、時間のかかる処理を走らせる。その間にメインスレッドでダイアログを表示し、別スレッドの終了を待ってダイアログを閉じる。

 なんとなく、これで行けるような気がしたのだがうまくいかない。

別スレッドの処理中、UIは「応答なし」になる

 こんな感じで、ダイアログ自体は開くものの、中身が表示されないまま「応答なし」になる。別スレッドの処理中はこの状態が続き、スレッドの終了とともにダイアログが閉じられて以降は正常動作に戻る。

モーダルダイアログにするとプログレスバーは正常に動く

 ためしにこのダイアログをモーダルなダイアログとして表示してみる。

 # 処理を開始したあとにメインスレッドでプログレスバーのダイアログを表示
    pg = ProgressDialog(self)
    # exec_() で開くとモーダルダイアログになる
    pg.exec_()
    
    # Thread.join() で別スレッドの終了を待つ
    th.join()

 これは一見うまくいく。

見た目としては成功な感じ

 ただ、これはモーダルなダイアログ、つまりこのダイアログを閉じるまでメインスレッドが止まったまま、という状態なので別スレッドの処理が終わってもダイアログは表示され続けてしまう。これだと別スレッドがどうなっているか全然わからないためプログレスバーの意味がない。

[解決] processEvents() を使う

 あれこれ模索して結局このモードレスなUIが応答なしになる現象から逃れられなかった私はTwitter に救いを求めた。

 するとCG関係のエンジニアリング界隈で有名なchiyamaさん(@chiyama) が教えてくださった。

 これがビンゴ。

        # ダイアログはモードレスで開く
        pg.open()
        # スレッドが生きている間、定期的にQApplicatiln.processEvents() を呼ぶ
        while th.is_alive():
            QtWidgets.QApplication.processEvents()
        # スレッドが終了するとis_alive() がFalse になってwhile を抜ける
        pg.accept()

 なお、QApplication はPySide ではQtGui に含まれていたけれど、PySide2 になってQtWidgets というモジュールに移されたのでこのような呼び方になる。場合によってwhile 節の中でtime.sleep() などで呼び出しサイクルを設定しても良いかもしれない。

 なお、教えてくれたchiyama さんはCGWorld の連載「痴山紘史の日本CG見聞録」などで有名な方です。助かりました。

最終的なコード(全体)