見出し画像

PySide MainWindowのラッパークラス

PySideQMainWindowでツールを作るさい、同じ記述を何回もするのがめんどうなので、共通で記述する部分をラッパークラスを作ってまとめてみる。

ライブラリ化

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関数
この関数でウィンドウを閉じるイベントをキャッチして、閉じた位置とサイズを取得する。
また、適切にデストラクタを呼び出し、メモリの開放を行うようになる。

フォルダ階層

フォルダ階層

とりあえずは図のような感じ。
libフォルダの下に、パッケージとしてeastbosk.uiのフォルダがある。
logフォルダはロガーで出力するログファイルの場所。
uiフォルダはQt Designerで作成したuiファイルの場所。
pysideフォルダ直下にpyファイルがあるので、それはsrcフォルダを作った方がいいかもと考えてる。
後は、設定ファイル類はconfフォルダとか・・まだ改良は必要。

テンプレート

Qt Designerで図のようなUIを作成。

サンプルUI
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()
起動画面

このコードがテンプレートになるので、新たにツールを作成する場合は、このコードをコピーして使用する感じになる。
相対パス関連は適宜変更する。

QDialogも後で作るかも。

とりあえず、今後のツールはこれで作成していくと思う・・

いいなと思ったら応援しよう!