Python + MaxScriptのツール開発入門(Max2020版)その7

2021年9月3日

 前回、ツール本体になるPythonファイル先頭のインポート部分までを書きました。今回はツール本体のクラス、およびそのクラスを呼び出す実行部分を実装していきます。

ツール本体のクラスを作る

 ツール本体のクラスは、QtDesignerで作ったUIをpy化してできたクラスと、そのUIを作るときに元にしたウィジェットのクラスの二つを継承して定義します。この、「元にしたウィジェットのクラス」も継承するというところがポイントです。

 今回、UIを作るときに「Dialog without Buttons」というテンプレートから開始しました。このテンプレートで作成したのはダイアログウィジェットで、PySideではPySide.QtWidgetsの中にQDialogというクラスとして定義されています。

 前回も確認しましたが、今一度、UIから生成したpyファイル(gridCopy_ui.py)を開いて、そのクラスの定義を確認しましょう。

 このクラスを継承します。このクラスにはコンストラクタ(__init__()という関数)が無く、setupUi()というUIを構築するメソッドと、retranslateUi()という、UI上に表示するユニコード文字(日本語などASCIIではない文字)を設定するメソッドの二つが定義されています。retranslateUi()はsetupUi()の中で呼ばれており、私たちが実際に使うのはsetupUi()だけです。

 このUi_Dialogクラスをもう少し見てみましょう。objectクラスを継承して定義されていますが、objectクラスというのはPythonの最も基本となるクラスです。

 setupUi()メソッドは引数としてselfとDialogを受け取っていますが、Pythonではクラスメソッドは必ず1つ目の引数に自分自身を渡すという決まりがあり、selfはその自分自身を指しています。実際に呼び出す際にはこのselfは無いのと同じような形になり、このsetupUi()にはDialogを引数として渡す、ということになります。

クラス定義を書く

 では実際のクラスを書いていきましょう。まずクラス定義の先頭を書きます。

class GridCopy(QtWidgets.QDialog, UI.Ui_Dialog):

 ここではGridCopyというクラスを定義することにします。前述のようにQtWidgetsのQDialogクラスと、UIのクラスを継承します。

 ここでインポート部分を見ると、PySide2パッケージからQtWidgetsモジュールをインポートしています。QDialogクラスはQtWidgetsモジュールに含まれているので、QtWidgets.QDialogのように書きます。

 UIの方はgridCopy_uiモジュールをas UIとしてインポートしました。継承するのはその中で宣言されているUi_Dialogクラスです。そのためUI.Ui_Dialogのように書きます。

 この二つのクラスを継承したGridCopyクラスを定義していきます。

コンストラクタを書く

 Pythonのクラスでは、__init__()というメソッドを定義すると、それがコンストラクタになります。まずはこれを書いていきます。

 コンストラクタはクラス定義の先頭に書くのが良いでしょう。

 前述のようにクラスメソッドは必ず一つ目の引数として自分自身が渡されるので、それをselfで受け取るように書きます。

 今回はさらにparentとf_pathという二つの引数を受け取るようにしました。この部分は実装する内容によって適宜変更します。今回この二つの引数を用意した理由は後述します。

継承したクラスを初期化する 1

 Pythonクラスのコンストラクタでは、まず最初に継承してきたクラスのコンストラクタを呼んで初期化します。

class GridCopy(QtWidgets.QDialog, UI.Ui_Dialog):
    def __init__(self, parent, f_path):
        super(GridCopy, self).__init__(parent)

 この呼び出し方にはいくつかの方法がありますが、今回はsuper関数でGridCopyのスーパークラス(継承してきたクラス)を取得し、その__init__()メソッドを明示的に実行しました。

 GridCopyクラスのコンストラクタで引数に指定したparent変数をここで使用しています。

 この部分をもう少し詳しく確認しましょう。

 今、GridCopyクラスはQDialogクラスを継承しています。QDialogクラスはQWidgetというクラスを継承していて、そのコンストラクタは引数として親になるオブジェクトを受け取ることができます。(渡さなければデフォルト値はNullです)

 親を渡した場合、生成されるインスタンスはその親の子になります。PySide2では、あるウィジェットを別のウィジェットの子にすると、子ウィジェットは親ウィジェットの上に表示されるようになります。

 ここで何をやろうとしているかというと、今回作るツールのダイアログを3dsmaxのウィンドウ上に表示して、下に行かないようにしたい、ということです。3dsmaxのウィンドウの子にすることで、3dsmax側をアクティブにしても、その上に載っている子ウィジェットであるツールの窓は下に隠れず、3dsmaxの上に表示されたままになります。こうした動作はウィンドウを親子付けすることで実現できるわけです。

 これを実現するために、GridCopyクラスのコンストラクタではparentという引数を受け取るようにし、ここに3dsmax本体のウィンドウを渡す、という設計にしたということです。

UIを構築する

 続いて、Ui_Dialogから継承してきたsetupUiメソッドを実行します。

class GridCopy(QtWidgets.QDialog, UI.Ui_Dialog):
    def __init__(self, parent, f_path):
        super(GridCopy, self).__init__(parent)
        self.setupUi(self)

 setupUiはUi_Dialogクラスのメソッドですが、GridCopyクラスはUi_Dialogクラスを継承しており、__init__()内の一行目でスーパークラスを初期化したため、GridCopyクラスもsetupUiメソッドを持っています。

 ここではself.setupUiという形でGridCopyクラスのインスタンスが持っているsetupUiメソッドを呼びます。

 さらに、selfをsetupUiの引数として渡していることにも注目してください。本来、クラスメソッドは引数無しで実行しても一つ目の引数に自分自身を渡します。ここでは明示的に、もう一つselfを渡しています。

 これが、Ui_Dialog.setupUiの実装のところでのDialogという変数に入ることになります。ここで、このDialog変数に入るものはQDialogクラスのオブジェクトである必要があります。このUIはもともとQtDesignerで「Dialog without Buttons」として生成したものだからです。

 しかし、Ui_Dialogクラスはobjectクラスしか継承していないため、これ自身にはQDialogの要素はありません。そのため、ツール本体のクラスはQtWidgets.QDialogクラスも継承してきて、自身がQDialogのサブクラスとなる必要があるのです。

 ここまでが、MaxでPySideのUIを持つツールのクラスを開発するときに共通の内容となります。

※ここで継承するクラスは、QtDesignerでどのWidgetを元にしてUIを作ったかに左右されます。それによってQMainWindowになったり、QDockWidgetになったりすることになります。

クラスメンバを初期化する

 UIを初期化したら、次はクラスのプロパティ(メンバ変数)を初期化します。

 今回のツールはシンプルなものなのでメンバ変数は1つしか用意しませんでした。

class GridCopy(QtWidgets.QDialog, UI.Ui_Dialog):
    def __init__(self, parent, f_path):
        super(GridCopy, self).__init__(parent)
        self.setupUi(self)
        # プロパティ
        self.f_path = f_path

 コンストラクタで受け取る二つ目(実装では三つ目)の引数、f_pathを受け取り、同名のプロパティを初期化します。この変数は後で外部のMaxScriptファイルを読む際に必要になります。詳しくはその部分を実装するときに解説します。

シグナルとスロットを接続する

 プロパティを初期化したら、最後はシグナルとスロットの接続を行います。

 PySide2ではオブジェクト間の通信を行うための、シグナルとスロットという仕組みが用意されています。ごく簡単に言うと、シグナルがUI要素に発生するイベントで、スロットがそれを受け取るものというイメージです。これを接続することによっていわゆるイベントハンドラを実装することになります。

 今回は、実行ボタンがクリックされたときにメソッドが実行される、という仕組みを作ります。実行ボタンが発する「クリックされた」というシグナルを、実行されるメソッド(スロット)に接続します。

class GridCopy(QtWidgets.QDialog, UI.Ui_Dialog):
    def __init__(self, parent, f_path):
        super(GridCopy, self).__init__(parent)
        self.setupUi(self)
        # プロパティ
        self.f_path = f_path
        # シグナルとスロットのコネクト
        self.pb_exec.clicked.connect(self.doGridCopy)

 こんな風に書きます。この部分のpb_execというのは、QtDesignerで実行ボタンにつけたobjectNameです。このようにクラスの実装内でUI要素のオブジェクト名が必要になるので、わかりやすい名前を付けておいた方が良いのです。

 接続先のdoGridCopy()というメソッドはまだ実装していませんが、これからこの名前で、既に書いたMaxScriptの関数を呼び出す内容を実装していくことになります。

まとめ

  • ツールのクラスは、UIのクラスと、そのUIを作るときに元にしたウィジェットのクラスの二つを継承して定義する
  • コンストラクタでは継承元クラスの初期化、UI構築関数の実行、プロパティの初期化、シグナルとスロットのコネクトを行う