![見出し画像](https://assets.st-note.com/production/uploads/images/150894472/rectangle_large_type_2_33d743922932ea1b1765581b104af625.png?width=1200)
PySide MainWindowのラッパークラス
PySideのQMainWindowでツールを作るさい、同じ記述を何回もするのがめんどうなので、共通で記述する部分をラッパークラスを作ってまとめてみる。
ライブラリ化
import os
import json
from logging import config, getLogger
import qtmax
from PySide2.QtCore import (
QFile,
QSetting
)
from PySide2.QtWidgets import (
QMainWindow,
QStatusBar
)
from PySide2.QtUiTools import QUiLoader
##
# @class EbMainWindow
# @brief ウィンドウの基底クラス
#
class EbMainWindow(QMainWindow):
# 変数
cur_name = None
cur_dir = None
##
# @method __init__
# @brief コンストラクタ
#
# @param py_path 実行Pythonファイルパス
#
def __init__(self, py_path:str=None):
try:
QMainWindow.__init__(self, qtmax.GetQMaxMainWindow())
self.cur_name = os.path.basename(py_path).split('.')[0]
self.cur_dir = os.path.dirname(py_path)
file_name = os.path.join(pymxs.runtime.getDir(pymxs.runtime.Name('temp')), (self.cur_name + r".ini"))
self.__settings = QSettings(file_name, QSettings.IniFormat)
self.__settings.setIniCodec('utf-8')
except Exception:
raise
finally:
pass
##
# @method __del__
# @brief デストラクタ
#
def __del__(self):
try:
self.logger.debug('Delete Instance')
except Exception:
raise
finally:
if self.logger is not None:
del self.logger
self.logger = None
##
# @method restore
# @brief ウィンドウの位置とサイズを復元
#
def restore(self):
try:
self.restoreGeometry(self.__settings.value('geometry'))
self.logger.debug('Restore')
except Exception as e:
self.logger.error("{0}".format(e))
raise
finally:
pass
##
# @method show
# @brief ウィンドウの表示(オーバーライド)
#
def show(self):
try:
self.restore()
super(EbMainWindow, self).show()
self.logger.debug('Show')
except Exception as e:
self.logger.error("{0}".format(e))
raise
finally:
pass
##
# @method closeEvent
# @brief ウィンドウが閉じたときに呼び出される(オーバーライド)
# @note ここで閉じる際の処理を行う。その後デストラクタが呼ばれる
#
# @param event イベント
#
def closeEvent(self, event):
try:
self.__settings.setValue('geometry', self.saveGeometry())
self.deleteLater()
event.accept()
self.logger.debug("Close Event")
except Exception as e:
self.logger.error("{0}".format(e))
raise
finally:
pass
##
# @method setupLogger
# @brief ロガーの設定を行う。
#
# @param dir ログファイルの出力先
# @param config_file ロガーの設定ファイル
# @param logger_name ログ名
#
def setupLogger(self, dir:str, config_file:str, logger_name:str):
try:
config_path = pathlib.Path(os.path.join(dir, config_file)).resolve()
with open(config_path, 'r') as f:
log_conf = json.load(f)
log_conf['handlers']['fileHandler']['filename'] = pathlib.Path(os.path.join(dir, log_conf['handlers']['fileHandler']['filename'])).resolve()
config.dictConfig(log_conf)
self.logger = getLogger(logger_name)
except Exception:
raise
finally:
pass
##
# @method loadUI
# @brief UIをロードする
#
# @param dir ファイルのディレクトリ
# @param file_name UIファイル
#
def loadUI(self, dir:str, file_name:str):
try:
# Load ui file
loader = QUiLoader()
ui_file_path = str(pathlib.Path(os.path.join(dir, file_name)).resolve())
ui_file = QFile(ui_file_path)
ui_file.open(QFile.ReadOnly)
self.ui = loader.load(ui_file, self)
ui_file.close()
except Exception as e:
self.logger.error("{0}".format(e))
raise
finally:
pass
##
# @method initUI
# @brief UIの初期設定
#
# @param menu_bar メニューバーの有効無効
# @param status_bar ステータスバーの有効無効
#
def initUI(self, menu_bar:bool, status_bar:bool):
try:
# Set Window Title
title = self.ui.windowTitle()
self.setWindowTitle(title)
# Set Window Size
w = self.ui.size().width()
h = self.ui.size().height()
self.resize(w, h)
if menu_bar is True:
self.__enableMenuBar()
if status_bar is True:
self.__enableStatusBar()
except Exception as e:
self.logger.error("{0}".format(e))
raise
finally:
pass
##
# @method __enableMenuBar
# @brief メニューバーを有効にする(プライベート関数)
#
def __enableMenuBar(self):
try:
menubar = self.ui.findChild(QMenuBar, 'menubar')
self.setMenuBar(menubar)
except Exception as e:
self.logger.error("{0}".format(e))
raise
finally:
pass
##
# @method __enableStatusBar
# @brief ステータスバーを有効にする(プライベート関数)
#
def __enableStatusBar(self):
try:
# Set Status Bar
# Instance Variable
self.statusbar = self.ui.findChild(QStatusBar, 'statusbar')
self.setStatusBar(self.statusbar)
except Exception as e:
self.logger.error("{0}".format(e))
raise
finally:
pass
##
# @method addMenuAction
# @brief メニューを追加する
#
# @param menu_name メニュー名
# @param action アクション
#
def addMenuAction(self, menu_name:str, action:QAction):
try:
menu = self.menuBar().findChild(QMenu, menu_name)
menu.addAction(action)
self.menuBar().addMenu(menu)
except Exception as e:
self.logger.error("{0}".format(e))
raise
finally:
pass
##
# @method showMessage
# @brief ステータスバーにメッセージを表示する
#
# @param message メッセージ
#
def showMessage(self, message:str):
try:
if self.statusBar is not None:
self.statusbar.showMessage(message)
except Exception as e:
self.logger.error("{0}".format(e))
raise
finally:
pass
共通に出来そうな部分は、ロガーの設定、UIファイルの読み込み、MainWindowで共通なタイトル、サイズ、ステータスバー。
とりあえず、今回思いつくのはこの辺だったので、他に共通化できそうならバージョンアップしていきたい。
ロガーについてはWebにいろいろあるので、ここでは説明しない。
■追加
# menuのparamを追加
def initUI(self, menu_bar:bool, status_bar:bool):
# 上記に以下を追加
if menu_bar == True:
self.__enableMenuBar()
def __enableMenuBar(self):
menubar = self.ui.findChild(QMenuBar, 'menubar')
self.setMenuBar(menubar)
def addMenuAction(self, menu_name:str, action:QAction):
menu = self.menuBar().findChild(QMenu, menu_name)
menu.addAction(action)
self.menuBar().addMenu(menu)
前回のラッパーに、メニューの項目を追加。
継承側では以下の記述でメニューを登録できている。
※階層メニューは検証が必要
# Set MenuBar
ui_action = self.ui.findChild(QAction, 'action_manual')
manual_action = QAction(ui_action.text(), self)
manual_action.setObjectName(ui_action.objectName())
manual_action.triggered.connect(self.showManual)
self.addMenuAction('menu_help', manual_action)
更新
3dsMax用に特化した形に変更
ウィンドウを閉じる処理を追加
ウィンドウを閉じた際に、ウィンドウの位置とサイズを保持・復元する機能を追加
def __init__(self, py_path:str=None):
try:
QMainWindow.__init__(self, qtmax.GetQMaxMainWindow())
self.cur_name = os.path.basename(py_path).split('.')[0]
self.cur_dir = os.path.dirname(py_path)
file_name = os.path.join(pymxs.runtime.getDir(pymxs.runtime.Name('temp')), (self.cur_name + r".ini"))
self.__settings = QSettings(file_name, QSettings.IniFormat)
self.__settings.setIniCodec('utf-8')
except Exception:
raise
finally:
pass
コンストラクタ
親クラスへ渡す引数に、直接3dsMaxメインウィンドウを設定し、3dsMax専用のクラスへしている。
QSettingを用いて、前回閉じた位置とサイズを保持するファイルを設定している。
ファイルはユーザークラスのtempフォルダ内に保存するようにしている。
def restore(self):
try:
self.restoreGeometry(self.__settings.value('geometry'))
self.logger.debug('Restore')
except Exception as e:
self.logger.error("{0}".format(e))
raise
finally:
pass
def show(self):
try:
self.restore()
super(EbMainWindow, self).show()
self.logger.debug('Show')
except Exception as e:
self.logger.error("{0}".format(e))
raise
finally:
pass
restore関数
ここでウィンドウの位置とサイズを復元する。
show関数
親クラスのshow関数をオーバーライドして、restore関数を呼び出す。
def closeEvent(self, event):
try:
self.__settings.setValue('geometry', self.saveGeometry())
self.deleteLater()
event.accept()
self.logger.debug("Close Event")
except Exception as e:
self.logger.error("{0}".format(e))
raise
finally:
pass
closeEvent関数
この関数でウィンドウを閉じるイベントをキャッチして、閉じた位置とサイズを取得する。
また、適切にデストラクタを呼び出し、メモリの開放を行うようになる。
フォルダ階層
![](https://assets.st-note.com/img/1723783359906-lOIAqNs6kU.png)
とりあえずは図のような感じ。
libフォルダの下に、パッケージとしてeastbosk.uiのフォルダがある。
logフォルダはロガーで出力するログファイルの場所。
uiフォルダはQt Designerで作成したuiファイルの場所。
pysideフォルダ直下にpyファイルがあるので、それはsrcフォルダを作った方がいいかもと考えてる。
後は、設定ファイル類はconfフォルダとか・・まだ改良は必要。
テンプレート
Qt Designerで図のようなUIを作成。
![](https://assets.st-note.com/img/1723783637208-UPCG83Hiyr.png?width=1200)
import sys
import os
import qtmax
from PySide2 import *
from PySide2.QtWidgets import *
eb_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), '.\\lib')
sys.path.append(eb_path)
from eastbosk.ui.EbPySideLibs import EbMainWindow
class EbWindow(EbMainWindow):
def __init__(self):
super().__init__(qtmax.GetQMaxMainWindow())
# 実行ディレクトリ取得
self.cur_dir = os.path.dirname(os.path.realpath(__file__))
# ロガーの設定
self.setupLogger(self.cur_dir, "log_config.json", "debugLogger")
# PySideのUIファイルを読み込み
self.loadUI(self.cur_dir, ".\\ui\\ui_EbMainWindow.ui")
# UIの初期設定、TrueでStatusBarが有効になる。
self.initUI(True)
# 個別UIの設定
self.__setupUI()
# ここのプライベート関数で、それぞれ個別のUI設定をする。
def __setupUI(self):
widget = self.ui.centralWidget()
h_lo = self.ui.findChild(QHBoxLayout, 'h_lo')
ok_btn = self.ui.findChild(QPushButton, 'ok_btn')
ok_btn.clicked.connect(self.okClicked)
h_lo.addWidget(ok_btn)
widget.setLayout(h_lo)
self.setCentralWidget(widget)
self.logger.debug("Setup UI")
def okClicked(self):
self.showMessage("OK")
def main():
window = EbWindow()
window.show()
if __name__ == '__main__':
main()
![](https://assets.st-note.com/img/1723784303891-Joer8hUmsY.png?width=1200)
このコードがテンプレートになるので、新たにツールを作成する場合は、このコードをコピーして使用する感じになる。
相対パス関連は適宜変更する。
QDialogも後で作るかも。
とりあえず、今後のツールはこれで作成していくと思う・・