以前PySide6を導入して簡単なGUIを作る記事とprintの内容をファイルに出力する方法の記事を書きました。
今回はこれらの応用としてPySide6を使ってprintの内容をGUIに表示させる方法の備忘録になります。
では、始めます。
1:PySide6でバックグラウンド処理を行う
PySide6で何かの処理中に画面を随時更新する場合にはバックグラウンド処理を行う必要があるので、まずはその説明をします。
基本的にはバックグラウンド処理したいクラスにSignalのスレッドを定義し、emit関数を使ってSignalの値を呼び出し側へ渡します。そして呼び出し元でそのSignalの値を取り出して処理していくという流れです。
ちゃんと書くとそこそこ長くなるので、以下に要点だけを抽出した簡単なソースコードを記述しておきます。このソースコード単体では動かないので注意してください。
sub_thread = SubThread()
sub_thread.connect(get_signal)
sub_thread.finished.connect(finish_thread)
def get_signal(signal_str):
print(signal_str)
def finish_thread():
print("finished")
class SubThread(QThread):
sub_signal = Signal(str)
def __init__(self, parent=None):
QThread.__init__(self, parent)
def send_signal(self):
self.sub_signal.emit("test")
メイン側でバックグラウンド処理のクラスのオブジェクトを作成し、そこに「connect」関数でシグナルを受け取る処理の関数を設定します。「finished.connect」で処理が終了した後の処理も設定できます。
そしてバックグラウンド処理のクラスではSignalをクラス変数として定義し、「emit」関数を設定することで値を渡すことができます。
これでバックグラウンド処理がわかったので、この方法を使ってprintの内容をGUIに表示させるコードを書いていきます。
2:printの内容をGUIに表示させるサンプル
今回はループでprintを行うpythonファイルをGUIのファイルから呼び出すという形でサンプルを作成しました。
ループでprintを行うファイルは以下の通りです。
・print_log.py
import sys
from time import sleep
def count_up(num, count):
print("--- start ----")
for i in range(0, count):
print(num + i)
sleep(0.5)
print("--- end ----")
if __name__ == '__main__':
count_up(1, 10)
GUIのファイルは以下の通りです。
・gui_test.py
import sys, threading, re
from copy import deepcopy
from PySide6.QtWidgets import *
from PySide6.QtCore import *
from PySide6.QtGui import *
import print_log
class Form(QDialog):
""" GUIクラス
"""
def __init__(self, parent=None):
super(Form, self).__init__(parent)
self.setWindowTitle("Title test")
self.setFixedWidth(400)
self.setFixedHeight(260)
num_layout = QHBoxLayout()
self.num_edit = QLineEdit("")
num_layout.addWidget(QLabel("num :"), 1)
num_layout.addWidget(self.num_edit, 4)
count_layout = QHBoxLayout()
self.count_edit = QLineEdit("")
count_layout.addWidget(QLabel("count :"), 1)
count_layout.addWidget(self.count_edit, 4)
self.text_layout = QHBoxLayout()
self.textbox = QListView()
self.text_list = QStringListModel()
self.textbox.setModel(self.text_list)
self.text_layout.addWidget(self.textbox)
pb_layput = QHBoxLayout()
self.pb = QProgressBar()
self.pb.setFixedWidth(370)
self.pb.setTextVisible(False)
pb_layput.addWidget(self.pb)
run_layout = QHBoxLayout()
self.run_button = QPushButton("start")
self.run_button.clicked.connect(self.run_log)
run_layout.addWidget(QLabel(""), 2)
run_layout.addWidget(self.run_button, 1)
run_layout.addWidget(QLabel(""), 2)
layout = QVBoxLayout()
layout.addLayout(num_layout)
layout.addLayout(count_layout)
layout.addLayout(self.text_layout)
layout.addLayout(pb_layput)
layout.addLayout(run_layout)
self.setLayout(layout)
self.lp = LogThread()
self.lp.log_thread.connect(self.show_log)
self.lp.finished.connect(self.show_result)
def run_log(self):
num = self.num_edit.text()
count = self.count_edit.text()
if self.is_number(num) and self.is_number(count):
self.set_all_enabled(False)
self.pb.setMinimum(0)
self.pb.setMaximum(0)
self.lp.set_count(int(num), int(count))
self.lp.start()
else:
QMessageBox.warning(self, "注意", "numとcountには正の半角整数を入力してください。")
def is_number(self, number):
if re.fullmatch(r"[0-9]+", number) is None:
return False
else:
return True
def show_log(self, log):
log_list = self.text_list.stringList()
log_list.append(str(log))
self.text_list.setStringList(log_list)
self.textbox.scrollToBottom()
def show_result(self):
QMessageBox.information(self, "終了", "終了しました。")
self.pb.setMinimum(0)
self.pb.setMaximum(100)
self.set_all_enabled(True)
def set_all_enabled(self, flg):
self.num_edit.setEnabled(flg)
self.count_edit.setEnabled(flg)
self.run_button.setEnabled(flg)
class LogThread(QThread):
""" ログファイルを読み取るクラス
"""
log_thread = Signal(str)
log_file_path = "./log.txt"
read_flg = False
num = 0
count = 0
def __init__(self, parent=None):
""" コンストラクタ
"""
QThread.__init__(self, parent)
def __del__(self):
self.wait()
def set_count(self, num, count):
self.num = num
self.count = count
def run(self):
sys.stdout = open(self.log_file_path, "w")
self.read_flg = True
read_thread = threading.Thread(target=self.read_log)
read_thread.setDaemon(True)
read_thread.start()
try:
print_log.count_up(self.num, self.count)
except Exception as e:
print(e)
finally:
sys.stdout.close()
sys.stdout = sys.__stdout__
self.read_flg = False
def read_log(self):
old_lines = list()
new_lines = list()
while self.read_flg:
with open(self.log_file_path, "r", encoding="utf-8") as f:
sys.stdout.flush()
new_lines = deepcopy(f.readlines())
old_size = len(old_lines)
new_size = len(new_lines)
if old_size < new_size:
for i in range(old_size, new_size):
self.log_thread.emit(new_lines[i].replace("\n",""))
old_lines = deepcopy(new_lines)
if __name__ == '__main__':
app = QApplication(sys.argv)
form = Form()
form.show()
sys.exit(app.exec())
上記2ファイルを以下のように同階層に配置します。

gui_test.pyの方を実行すると以下のようにGUIが表示されるのでnumとcountのところに半角数字を入力します。
startボタンを押下して実行すると画面が非活性になりリアルタイムでログの内容が表示され、処理が終わると終了ダイアログが表示されます。
詳しくはソースコードを読んでいただきたいのですが、少し解説すると、ログの書き出しを行っている時にログファイルを開いてその行数を取得し、以前の行数と差分があれば差分の行を1行ずつSignalで送っているという処理を行っています(read_log関数)。
重要なのが「sys.stdout.flush() 」という一文で、これをどこかに書いていないとprintの内容がリアルタイムでGUIに反映されず、処理が終わった後にまとめてGUIに反映されるという動きになります。
この理由は、printのみではバッファに溜まり続けるだけで出力されないため、flushでバッファの内容を出力してあげる必要があるからです。
以上がPySide6を使ってprintの内容をGUIに表示させる方法になります。
Signalを使うのが初めてだったりflushの必要性を知らなかったりと割と手こずりましたが、GUIのバックグラウンド処理やリアルタイムに反映する記述方法がわかったので色々と楽しいことに使えそうです。
・参考資料