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

2021年9月3日

 前回まででツールの根幹は実装できました。実行上の問題はほとんどありませんが、改善できるポイントはまだまだありそうです。今回はその修正作業、いわゆるリファクタリングを行いながら、さらにアップグレードできそうな部分を見つけていきたいと思います。

 まずはコードを眺め、改善できるところを探しましょう。

コード全体を眺めて改善点を探す

 まずはちょっと長いですが、コードの全貌を見てみましょう。

# -*- coding: utf-8 -*-
from __future__ import absolute_import, print_function, division
import os
import sys
import MaxPlus
import pymxs
from PySide import QtWidgets
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
import gridCopy_ui as UI
reload(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)
    def doGridCopy(self):
        sel_count = MaxPlus.SelectionManager.GetCount()
        if sel_count != 1:
            mb = QtWidgets.QMessageBox()
            mb.setText(u"コピー元オブジェクトを1つ選択してください。")
            mb.exec_()
            return
        script_path = os.path.join(self.f_path,  "gridCopy.ms")
        pymxs.runtime.fileIn(script_path)
        pymxs.runtime.doGridCopy(self.spn_x_count.value(), self.spn_y_count.value(), self.spn_z_count.value(), self.spn_x_interval.value(), self.spn_y_interval.value(), self.spn_z_interval.value())
        MaxPlus.ViewportManager.ForceCompleteRedraw()
def main(f_path):
    win = GridCopy(MaxPlus.GetQMaxMainWindow(), f_path)
    win.show()
if __name__ == "__main__":
    main(os.path.dirname(os.path.abspath(__file__)))

 ざっと眺めてみて、改善できそうなところを探します。

 すると、やはり実行のdoGridCopy()メソッドが気になります。

 ここでfileIn()を使ってmsファイルを評価していますが、ここで行うのは関数の評価なので、一度実行すれば以降は必要ありません。このfileIn()がここに書いてあると実行ボタンを押すたびに毎回評価が行われてしまい、無駄が生じています。

 ここで評価しているmsファイルは分量が多くないため、実行速度にはほとんど影響はありませんが、それでもやはり、無駄な処理が毎回行われていると思うとあまり気持ちの良いものではありません。これをなんとかしましょう。

 どうなれば良いかと言えば、ツールの起動時に一回だけ実行されれば良いということになります。というわけで、このfileIn()の実行部分をコンストラクタの中へ移動しましょう。

 そうすると、コンストラクタ内でプロパティに格納していたディレクトリパスの情報は、プロパティとして持たせる必要はなくなります。このfileIn()の実行時にしか使わない情報だからです。

 この修正を行うと以下のようになります。

class GridCopy(QtWidgets.QDialog, UI.Ui_Dialog):
    def __init__(self, parent, f_path):
        super(GridCopy, self).__init__(parent)
        self.setupUi(self)
        # MaxScript関数の評価
        script_path = os.path.join(f_path,  "gridCopy.ms")
        pymxs.runtime.fileIn(script_path)
        # シグナルとスロットのコネクト
        self.pb_exec.clicked.connect(self.doGridCopy)
    def doGridCopy(self):
        sel_count = MaxPlus.SelectionManager.GetCount()
        if sel_count != 1:
            mb = QtWidgets.QMessageBox()
            mb.setText(u"コピー元オブジェクトを1つ選択してください。")
            mb.exec_()
            return
        pymxs.runtime.doGridCopy(self.spn_x_count.value(), self.spn_y_count.value(), self.spn_z_count.value(), self.spn_x_interval.value(), self.spn_y_interval.value(), self.spn_z_interval.value())
        MaxPlus.ViewportManager.ForceCompleteRedraw()

 コンストラクタに移動させた部分で、self.f_pathとなっていたところは単なるf_pathに書き換えます。プロパティとして保持させるのをやめ、受け取った引数をその場で使用して終わり、という処理にします。

 これで、コンストラクタの呼び出し時に一度だけfileIn()が実行されるようになりました。

※実はこれでもまだ無駄があります。一度fileIn()された関数は3dsmaxを終了しない限り、グローバルスペースに居続けます。そのため、本来は3dsmaxの起動時に一度だけ評価すれば良い、ということになります。それを実現するには、gridCopy.msを3dsmaxのStartupフォルダに置けば良い、ということになります。実際にツールを配布して使用するような場合には、こうしたファイルの配置をインストーラで実現するのが良いでしょう。

ツールそのものの改善点を探す

 さらに、ツール本体の改善ポイントを探しましょう。実は改善できそうな点はいくつかあります。

  • 入力した値をクリア(0に戻す)ボタンをつける
  • 間隔の値には少数を入れられるようにする
  • ダイアログのタイトルがDialogなのをなんとかする
  • ダイアログの上にある謎の「?」を表示しないようにする
  • タブストップ(タブキーでの入力欄の移動)を正しくする

 などです。自分だけが使用するツールであればこうしたポイントは「自分が使いやすいかどうか」という判断で良いのですが、配布して複数の人に使ってもらうような場合には、実際に使う人の意見などを集めて改善点を見出していく必要があるでしょう。

 このチュートリアルではさしあたり、上記の点を改善してみたいと思います。少しずつ何回かにわけて実装を進めます。

ツールの改善を行う 1

入力欄のクリアボタンを追加する

 

 さっそくですがこれはUIの変更が必要です。つまりQtDesignerに戻って作業することになります。

 QtDesignerでgridCopy_ui.uiを開き、クリアボタンを追加します。

 UIを変更したら上書き保存し、このuiファイルを改めてpyファイルにコンバートします。

※コンバート方法はこのチュートリアルの「その5」で紹介しています。

 コンバートを終えたら改めてgridCopy.pyを起動してみましょう。

 このように、UIの変更が反映されます。反映されない場合、uiファイルの上書き保存は正しくできているか、コンバートは正常に完了したか、UIのインポート後にreload()しているか、などを確認しましょう。

 この時点ではまだクリアボタンのシグナルはどこにも接続されていないため、押しても何も起きません。

クリアボタンのハンドラを実装する

 続いてこのクリアボタンに処理を実装しましょう。ここでは、clearInputs()というメソッドを定義し、それをpb_clearのclickedシグナルに接続する、という形で実装してみます。

 この部分を追加します。その結果GridCopyクラスは以下のようになります

class GridCopy(QtWidgets.QDialog, UI.Ui_Dialog):
    def __init__(self, parent, f_path):
        super(GridCopy, self).__init__(parent)
        self.setupUi(self)
        # MaxScript関数の評価
        script_path = os.path.join(f_path,  "gridCopy.ms")
        pymxs.runtime.fileIn(script_path)
        # シグナルとスロットのコネクト
        self.pb_exec.clicked.connect(self.doGridCopy)
        self.pb_clear.clicked.connect(self.clearInputs)
    def doGridCopy(self):
        sel_count = MaxPlus.SelectionManager.GetCount()
        if sel_count != 1:
            mb = QtWidgets.QMessageBox()
            mb.setText(u"コピー元オブジェクトを1つ選択してください。")
            mb.exec_()
            return
        pymxs.runtime.doGridCopy(self.spn_x_count.value(), self.spn_y_count.value(), self.spn_z_count.value(), self.spn_x_interval.value(), self.spn_y_interval.value(), self.spn_z_interval.value())
        MaxPlus.ViewportManager.ForceCompleteRedraw()
    def clearInputs(self):
        self.spn_x_count.setValue(0)
        self.spn_y_count.setValue(0)
        self.spn_z_count.setValue(0)
        self.spn_x_interval.setValue(0)
        self.spn_y_interval.setValue(0)
        self.spn_z_interval.setValue(0)

 これで実行してみると、クリアボタンの操作でUIの値が全部0になります。

 次回は「間隔」の値に少数を許容できるようにします。

まとめ

  • ツールの改善は「現状の機能をより効率よく実装する」と「さらに便利になるように設計を拡張する」の二方面から検討する
  • 改善点は自分で触ってみたり、実際にユーザに触ってもらって洗い出す