2023-02-07

プラグイン・ウィジェットを扱う ~ PySide6 ~

PySide (Qt for Python) は、Qt(キュート)の Python バインディングで、GUI などを構築するためのクロスプラットフォームなライブラリです。配布ライセンスは LGPL で公開されています。最新のバージョンは Qt6 に対応した PySide6(記事執筆時点で 6.4.2)です。

Python で作った GUI アプリケーションで、本体に手を加えずに、外部のスクリプトで機能を追加できるようにしたい場合があります。例えば、アプリケーションからスクリプトを読み込むと、ボタンが追加されて新しい機能を利用できるようになるといった具合にです。

Python の標準ライブラリ importlib パッケージ [1] を利用して、動作確認用のサンプルを作りました。

下記の OS 環境で動作確認をしました。

Fedora Linux 37 (Server Edition) x86_64
python3.11 python3-3.11.1-3.fc37.x86_64
PySide6 6.4.2

プラグインのスクリプトには、QPushButton [2] を継承した Plugin クラスが実装されていて、インスタンスはボタンとして機能することを前提にしました。

下記のサンプル qt_plugin_test.py は、ファイルを選択すると読み込んで、読み込んだファイルから Plugin() クラスのインスタンスを作成して、メインウィンドウにボタンを表示する、というだけのサンプルです。

qt_plugin_test.py
#!/usr/bin/env python
# coding: utf-8
import importlib.util
import sys
from typing import Any
from PySide6.QtGui import QAction
from PySide6.QtWidgets import (
QApplication,
QFileDialog,
QMainWindow,
QPushButton,
QVBoxLayout,
QWidget,
)
def add_module(module_name: str, file_path: str, class_name: str) -> Any:
"""Add module
Args:
module_name (str): module name to add
file_path (str): script path to load as plugin
class_name (str): class name in the module to use
Returns:
Any: plugin instance to add into main application
Note:
https://docs.python.org/3/library/importlib.html
"""
spec = importlib.util.spec_from_file_location(module_name, file_path)
module = importlib.util.module_from_spec(spec)
sys.modules[module_name] = module
spec.loader.exec_module(module)
# example: obj = module.Plugin()
obj = eval('module.%s()' % class_name)
print(type(obj))
return obj
class Example(QMainWindow):
layout: QVBoxLayout = None
class_name = 'Plugin'
def __init__(self):
super().__init__()
self.id_plugin = 0
self.init_ui()
self.setWindowTitle('Plugin Test')
def init_ui(self):
base = QWidget()
self.setCentralWidget(base)
self.layout = QVBoxLayout()
base.setLayout(self.layout)
self.statusBar()
open_file = QAction('Open', self)
open_file.setShortcut('Ctrl+O')
open_file.setStatusTip('Select plugin script.')
open_file.triggered.connect(self.plugin_read_dialog)
menubar = self.menuBar()
file_menu = menubar.addMenu('&File')
file_menu.addAction(open_file)
def plugin_read_dialog(self):
"""Dialog to read plugin script
"""
dialog = QFileDialog()
if dialog.exec():
filename = dialog.selectedFiles()[0]
self.add_plugin_widget(filename)
def add_plugin_widget(self, filename: str):
"""Add plugin widget from specified filename
Args:
filename (str): plugin script to add
"""
plugin_name = 'plugin_%d' % self.id_plugin
self.id_plugin += 1
obj = add_module(plugin_name, filename, self.class_name)
self.layout.addWidget(obj)
def main():
app = QApplication(sys.argv)
ex = Example()
ex.show()
sys.exit(app.exec())
if __name__ == '__main__':
main()

指定ファイルから指定クラスをインスタンス化する処理を使い回せるように、add_module という関数にしました。指定したクラスが存在しなかった場合などのエラー処理は何もしていません。

また、プラグイン・スクリプトのモジュールは、一旦読み込んだらリロードしたり削除することを前提にしていないので、sys.modules の辞書に追加する必要があるのかは疑問に思っていますが、とりあえずモジュール名 (module_name) を一意にして、参考サイト [1] で紹介されているサンプルに倣って登録しています。

動作確認用に読み込むプラグイン・スクリプトとして、QPushButton を継承してボタンに文字列を表示しただけの簡単なサンプル plugin_widget_1.py を用意しました。

plugin_widget_1.py
from PySide6.QtWidgets import QPushButton
class Plugin(QPushButton):
def __init__(self):
super().__init__()
self.setText('Plugin Test 1')

ファイルダイアログを開いて、上のサンプルを選択すると下記のようにボタンが一つ追加されます。

アプリケーション本体側で API を用意して、API を利用した特定用途のボタンをプラグインとして追加する、といったことが使い方ができます。

参考サイト

  1. importlib — The implementation of import — Python 3.11.1 documentation
  2. QPushButton - Qt for Python

 

 

ブログランキング・にほんブログ村へ bitWalk's - にほんブログ村 にほんブログ村 IT技術ブログ オープンソースへ
にほんブログ村

オープンソース - ブログ村ハッシュタグ
#オープンソース



0 件のコメント: