pythonでGUIで操作するアプリを作ってみました。その時のことを、簡単にまとめてみます。
仕様:特定の拡張子のファイルのリストを作成し、1件ずつ処理し、その進捗を表示する
例えば、特定フォルダ以下にあるPythonファイルを開いて、1件ずつ処理して、○○件中××件目を実行中と表示するようなイメージです。
特定の拡張子のファイルを集めるクラスを作る
まずは、特定の拡張子のファイルを集めてリストを作るクラスFileList
を作ります。
import os
class FileList():
''' store file list'''
def __init__(self, root_dir, ext):
self.root_dir = root_dir
self.ext = ext
self.files = []
def retrieve(self):
for rd, _, fl in os.walk(self.root_dir):
for f in fl:
_, fext = os.path.splitext(f)
if fext == self.ext:
self.files.append(os.path.join(rd, f))
def print(self):
for f in self.files:
print(f)
残念なほど簡単なクラスです。
試しに実行してみます。
うん、多分、正しい。
GUIの設計
GUIを設計します。必要な要素は、以下のようになります。
- 対象フォルダを入力するパーツ
- 実行を指示するためのパーツ
- 進捗を表示するパーツ
対象フォルダを入力するためのパーツとは、「フォルダ参照」ボタンと、指定したフォルダを表示するテキストボックスがあれば良いでしょう。
実行を指示するためのパーツは、「実行」ボタンがあれば良いでしょう。
進捗を表示するパーツは、テキストボックスに処理中のファイル名を表示して、プログレスバーに全体の進捗割合を表示するようにします。
このパーツ配置について、適切なGUI設計ツールを使うと簡単にできるんじゃないかと思うのですが、とりあえず、地道に手で書きます。
基本的には、First programs in PyQt5 から丸パクリします。
import sys
import os
from PyQt5.QtWidgets import (QWidget, QApplication,
QPushButton, QLineEdit,
QHBoxLayout, QVBoxLayout,
QTextEdit, QProgressBar,
QFileDialog)
from PyQt5.QtCore import Qt
class MainWidget(QWidget):
dirname = ''
step = 0
def __init__(self, parent=None):
super(MainWidget, self).__init__(parent)
self.initUI()
def initUI(self):
self.resize(480, 360)
self.txtFolder = QLineEdit()
self.btnFolder = QPushButton('参照...')
hb1 = QHBoxLayout()
hb1.addWidget(self.txtFolder)
hb1.addWidget(self.btnFolder)
self.btnExec = QPushButton('実行')
self.btnExec.setEnabled(False)
hb2 = QHBoxLayout()
hb2.addWidget(self.btnExec)
self.txtLog = QTextEdit()
self.pbar = QProgressBar()
self.pbar.setTextVisible(False)
layout = QVBoxLayout()
layout.addLayout(hb1)
layout.addLayout(hb2)
layout.addWidget(self.txtLog)
layout.addWidget(self.pbar)
self.setLayout(layout)
self.setWindowTitle('PyQt5 Sample')
def main(args):
app = QApplication(args)
dialog = MainWidget()
dialog.show()
sys.exit(app.exec_())
if __name__ == '__main__':
main(sys.argv)
だいたい、想定どおりな感じです。
実装
「参照...」ボタン(btnFolder)の機能
btnFolderのclickedイベントとして定義します。
self.btnFolder.clicked.connect(self.show_folder_dialog)
def show_folder_dialog(self):
''' open dialog and set to foldername '''
dirname = QFileDialog.getExistingDirectory(self,
'open folder',
os.path.expanduser('.'),
QFileDialog.ShowDirsOnly)
if dirname:
self.dirname = dirname.replace('/', os.sep)
self.txtFolder.setText(self.dirname)
self.btnExec.setEnabled(True)
self.step = 0
フォルダを指定するダイアログを開いて、「実行」ボタンを有効化します。
「実行」ボタン(btnExec)の機能
指定したフォルダ以下の、「.py」ファイルを集めて、txtLogにファイル名を表示します。
また、実行中の進捗をプログレスバーで表現します。
FileListクラスをimportする
from filelist import FileList
FileList class のretrieve() メソッドを実行する
FileList classを当初のバージョンから少し修正します。
class FileList():
''' store file list'''
def __init__(self):
self.root_dir = ''
self.ext = ''
self.files = []
def setup(self, root_dir, ext):
self.root_dir = root_dir
self.ext = ext
self.retrieve()
def retrieve(self):
self.files = []
for rd, _, fl in os.walk(self.root_dir):
for f in fl:
_, fext = os.path.splitext(f)
if fext == self.ext:
self.files.append(os.path.join(rd, f))
def print(self):
for f in self.files:
print(f)
init()
で、 root_dir
と ext
を指定していましたが、それを新たに定義した setup()
メソッドに移します。
QThreadからの継承に
GUIのプログラムでは、GUIのアレコレがmutli-threadで動作しているので、ファイルをアレコレする作業自体もmulti-threadで動作するように、FileList classをQThreadからの継承にします。
また、1件の処理ごとに、プログレスバーを動かすことができるように、シグナルを送るための機能も実装します。
本来ならば、process_file()の中で、例えば画像ファイルであればなんらかの編集をする等するのですが、このsample中ではソレは本筋ではないので、ひとまず何もしません。
import os
import sys
from PyQt5.QtCore import pyqtSignal, QMutexLocker, QMutex, QThread
class FileList(QThread):
''' store file list'''
sig_file = pyqtSignal(str)
def __init__(self, parent=None):
super(FileList, self).__init__(parent)
self.stopped = False
self.mutex = QMutex()
def setup(self, root_dir, ext):
self.root_dir = root_dir
self.ext = ext
self.retrieve()
self.stopped = False
def stop(self):
with QMutexLocker(self.mutex):
self.stopped = True
def run(self):
for f in self.files:
fname = f
self.process_file(fname)
self.sig_file.emit(fname) # sginal送信
self.stop()
self.finished.emit() # signal送信
def retrieve(self):
''' root_dirからext拡張子を持つファイルを取得する '''
self.files = []
for rd, _, fl in os.walk(self.root_dir):
for f in fl:
_, fext = os.path.splitext(f)
if fext == self.ext:
self.files.append(os.path.join(rd, f))
self.length = len(self.files)
def process_file(self, path):
''' ひとまず何もしない '''
cnt = 0
if os.path.exists(path):
cnt += 1
else:
cnt = 0
def print(self):
for f in self.files:
print(f)
def main(args):
root_dir = '.'
ext = '.py'
if len(args) == 3:
root_dir = args[1]
ext = args[2]
fileList = FileList()
fileList.setup(root_dir, ext)
fileList.print()
if __name__ == '__main__':
main(sys.argv)
このFileList classを参照して、ボタンを押したら、指定したフォルダを走査して、その過程を表示するguimain.pyは以下のようになります。
import sys
import os
from PyQt5.QtWidgets import (QWidget, QApplication, QPushButton, QLineEdit,
QHBoxLayout, QVBoxLayout, QTextEdit, QProgressBar,
QFileDialog)
from PyQt5.QtCore import pyqtSlot, Qt
from filelist import FileList
class MainWidget(QWidget):
dirname = ''
step = 0
def __init__(self, parent=None):
super(MainWidget, self).__init__(parent)
self.initUI()
self.fileList = FileList()
self.fileList.sig_file.connect(self.update_status)
self.fileList.finished.connect(self.finish_process)
def initUI(self):
self.resize(480, 360)
self.txtFolder = QLineEdit()
self.txtFolder.setReadOnly(True)
self.btnFolder = QPushButton('参照...')
self.btnFolder.clicked.connect(self.show_folder_dialog)
hb1 = QHBoxLayout()
hb1.addWidget(self.txtFolder)
hb1.addWidget(self.btnFolder)
self.btnExec = QPushButton('実行')
self.btnExec.clicked.connect(self.exec_process)
self.btnExec.setEnabled(False)
self.btnExec.setVisible(True)
self.btnExit = QPushButton('終了')
self.btnExit.setVisible(False) # 無効化
self.btnExit.setEnabled(False) # 表示しない
self.btnExit.clicked.connect(self.close)
hb2 = QHBoxLayout()
hb2.addWidget(self.btnExec)
hb2.addWidget(self.btnExit) # 初期状態で不可視のボタンを追加
self.txtLog = QTextEdit()
self.txtLog.setReadOnly(True)
self.pbar = QProgressBar()
self.pbar.setTextVisible(False)
layout = QVBoxLayout()
layout.addLayout(hb1)
layout.addLayout(hb2)
layout.addWidget(self.txtLog)
layout.addWidget(self.pbar)
self.setLayout(layout)
self.setWindowTitle('PyQt5 Sample')
def show_folder_dialog(self):
''' open dialog and set to foldername '''
dirname = QFileDialog.getExistingDirectory(self,
'open folder',
os.path.expanduser('.'),
QFileDialog.ShowDirsOnly)
if dirname:
self.dirname = dirname.replace('/', os.sep) # ディレクトリの区切りをOSに合わせて変換しておく
self.txtFolder.setText(self.dirname)
self.btnExec.setEnabled(True)
self.step = 0
def print_log(self, logstr):
self.txtLog.append(logstr)
@pyqtSlot()
def exec_process(self):
if os.path.exists(self.dirname):
try:
QApplication.setOverrideCursor(Qt.WaitCursor)
self.fileList.setup(self.dirname, '.py')
maxCnt = self.fileList.length
self.pbar.setValue(0)
self.pbar.setMinimum(0)
self.pbar.setMaximum(maxCnt)
self.fileList.start()
except Exception as e:
self.print_log(str(e))
finally:
QApplication.restoreOverrideCursor()
else:
self.print_log('{0} is not exists'.format(self.dirname))
@pyqtSlot(str)
def update_status(self, filename):
self.txtLog.append(filename)
self.step += 1
self.pbar.setValue(self.step) # progressBarを進める
@pyqtSlot()
def finish_process(self):
self.fileList.wait()
# 実行ボタンを隠す
self.btnExec.setEnabled(False)
self.btnExec.setVisible(False)
# 終了ボタンを表示する
self.btnExit.setEnabled(True)
self.btnExit.setVisible(True)
def main(args):
app = QApplication(args)
dialog = MainWidget()
dialog.show()
sys.exit(app.exec_())
if __name__ == '__main__':
main(sys.argv)
この後半の急加速っぷり
なんか面倒になってきたので、以上のように書くと、とりあえず動きますよって感じです。
FileList.run()の中で、1件ずつ self.sig_file.emit(fname)
することで、シグナルが送られ、それをguimain.update_status()で受けることで、プログレスバーを一つずつ進められます。
まとめ
PythonでGUIアプリを書いてみました。QThreadを継承するのがキモになります。