Matplotlib は、Python および NumPy のためのグラフ描画ライブラリです。Qt for Python (PySide) などと組み合わせて GUI アプリを作成するのに重宝しています。調子に乗ってプロットに時間がかかるような、ちょっと重い処理にも応用を広げているのですが、当然、プロットされるまでの間、GUI が固まってしまいます。
自分だけで使うときは、判っているので、ちょっと待つという我慢で済みます。しかし、他の人も使うような場合は、このままにはできません。時間のかかる処理は QThread を使って GUI が固まらないように別スレッドで処理させるように配慮する必要があります。
なんかうまくいかない
当初の方針は、Matplotlib で作成するプロットを別スレッドで処理して、その出力である canvas をメインのスレッドに渡して GUI に表示する、というものでした。しかし、テスト用のプログラムは、もっともらしい動作をするものの、肝心のプロットの内容を表示できませんでした。
ふと、下記の警告に気が付きました。
Starting the Matplotlib GUI outside the main thread may fail.
なんと、別スレッドで Matplotlib の描画処理をするのはうまくいかないようです。
プロットのためのデータセット準備の部分をスレッドに
考えてみると、プロットを描画する時間がボトルネックになっているわけではなく、プロットするためのデータセットの準備に時間がかかっています。
上記 Matplotlib の処理を別スレッドでできるやり方を探すよりも、時間がかかるであろうデータセット準備の処理部分を別スレッドにするようにした方が確実そうです。
そうすると、今まで作っていたアプリの構成が悪く、スレッド化のために作り直しが多く発生しそうで青くなっています。スレッド化を後回しにしたツケが回ってきました。
とにもかくにも、簡単なサンプルを作って、これをもとに基本構成を作り直すことにしました。
サンプル
長い前置きでしたが、本記事では、その簡単なサンプルを紹介します。なお、下記の OS 環境で動作確認をしました。
![]() |
Fedora 35 Silverblue | x86_64 |
Python | 3.10.2 | |
PySide6 | 6.2.3 | |
matplotlib | 3.5.1 |
以下にサンプルの実行例を示します。上部ツールバーの plot をクリックすると散布図を描画するという単純なサンプルです。
![]() |
![]() |
サンプルコードを下記に示しました。時間がかかる処理の雰囲気を出すために、ダミーで 2 秒のスリープを入れました。
#!/usr/bin/env python | |
# coding: utf-8 | |
from PySide6.QtCore import ( | |
QObject, | |
QThread, | |
Qt, | |
Signal, | |
) | |
from PySide6.QtGui import QAction | |
from PySide6.QtWidgets import ( | |
QApplication, | |
QMainWindow, | |
QProgressDialog, | |
QToolBar, | |
) | |
import numpy as np | |
import pandas as pd | |
import matplotlib.pyplot as plt | |
from matplotlib.backends.backend_qtagg import FigureCanvasQTAgg as FigureCanvas | |
from matplotlib.backends.backend_qtagg import NavigationToolbar2QT as NavigationToolbar | |
from matplotlib.figure import Figure | |
from matplotlib.widgets import EllipseSelector, RectangleSelector | |
import seaborn as sns | |
import sys | |
import time | |
class Scatter(FigureCanvas): | |
fig = Figure() | |
def __init__(self, df): | |
super().__init__(self.fig) | |
self.init_chart(df) | |
def init_chart(self, df): | |
# Seaborn Scatter | |
ax = sns.scatterplot(data=df, x=df.columns[0], y=df.columns[1], ax=self.fig.add_subplot(111)) | |
ax.set(title='Scatter Sample') | |
# Selector | |
plt.RS = RectangleSelector( | |
ax, self.select_callback, useblit=True, | |
button=[1], # disable middle & right buttons | |
minspanx=5, minspany=5, spancoords='pixels', interactive=True, | |
props=dict(facecolor='pink', edgecolor='red', alpha=0.2, fill=True) | |
) | |
@staticmethod | |
def select_callback(eclick, erelease): | |
x1, y1 = eclick.xdata, eclick.ydata | |
x2, y2 = erelease.xdata, erelease.ydata | |
print(f"({x1: 3.2f}, {y1: 3.2f}) --> ({x2: 3.2f}, {y2: 3.2f})") | |
class Example(QMainWindow): | |
navtoolbar = None | |
# for QThread | |
progress = None | |
thread = None | |
worker = None | |
def __init__(self): | |
super().__init__() | |
self.resize(600, 600) | |
self.setWindowTitle('Scatter + QThread') | |
self.init_ui() | |
def init_ui(self): | |
toolbar = QToolBar() | |
menu = QAction("plot", self, triggered=self.prep_start) | |
toolbar.addAction(menu) | |
self.addToolBar(Qt.TopToolBarArea, toolbar) | |
# _/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_ | |
# DATA PREPARATION | |
def prep_start(self): | |
# remove widget from central | |
self.takeCentralWidget() | |
if self.navtoolbar is not None: | |
self.removeToolBar(self.navtoolbar) | |
# progress bar | |
self.progress = QProgressDialog(labelText='Working...', parent=self) | |
self.progress.setWindowModality(Qt.WindowModal) | |
self.progress.setCancelButton(None) | |
self.progress.setRange(0, 0) | |
self.progress.setWindowTitle('progress') | |
self.progress.show() | |
# threading | |
self.thread = QThread() | |
self.worker = Worker() | |
self.worker.moveToThread(self.thread) | |
# signal 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.prepCompleted.connect(self.prep_end) | |
# start threading | |
self.thread.start() | |
def prep_end(self, df): | |
# stop QProgressDialog | |
self.progress.cancel() | |
# plotting chart | |
canvas: FigureCanvas = Scatter(df) | |
self.setCentralWidget(canvas) | |
# navigation toolbar | |
navtoolbar = NavigationToolbar(canvas, self) | |
self.addToolBar( | |
Qt.BottomToolBarArea, | |
navtoolbar | |
) | |
self.navtoolbar = navtoolbar | |
class Worker(QObject): | |
""" | |
DATA PREPARATION WORKER | |
""" | |
prepCompleted = Signal(pd.DataFrame) | |
finished = Signal() | |
def __init__(self): | |
super().__init__() | |
def run(self): | |
df = pd.DataFrame(np.random.random(size=(1000, 2)), columns=['X', 'Y']) | |
# dummy!! dummy!! dummy!! | |
time.sleep(2) | |
self.prepCompleted.emit(df) | |
self.finished.emit() | |
def main(): | |
app = QApplication(sys.argv) | |
ex = Example() | |
ex.show() | |
sys.exit(app.exec()) | |
if __name__ == '__main__': | |
main() |
※ コードを更新しました。[2022-04-10]
参考サイト

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