リファレンス / 3ds Max / 3ds Max Python チートシート

Qt UI

  • 3dsMax
  • Python
  • pymxs
  • チートシート
3ds Max Python チートシート の章一覧

Python のツールの UI は、3ds Max に同梱の PySide2(Qt for Python) で作ります。MaxScript の rollout の代わりです。 大事なのは 2 つだけです。3ds Max のメインウィンドウを親にすること と、ウィンドウへの参照を持ち続けること です。

3ds Max 2025 以降は PySide6 に変わりました。本章のコードは 3ds Max 2022(PySide2)基準です。2025 以降では from PySide2 import ... を from PySide6 import ... に読み替えます。一部の API は名前が変わっており、たとえばモーダル表示の exec_() は exec() になります。

3ds Max のメインウィンドウを取得する

本章では、以後 qtmax.GetQMaxMainWindow() で取得した QMainWindow を親にします。公式ヘルプにある QWidget.find(rt.windows.getMAXHWND()) でも親にできますが、こちらの QWidget 版はドッキングには使えません。

from PySide2 import QtWidgets
import qtmax

main = qtmax.GetQMaxMainWindow()                             # QMainWindow(ドッキングに使える)
main_w = QtWidgets.QWidget.find(rt.windows.getMAXHWND())      # QWidget 版(公式ヘルプの書き方)
  • 親にしておくと、3ds Max の最小化・終了と一緒に閉じ、3ds Max のウィンドウの裏に隠れることもありません。

最小のツールウィンドウ

from PySide2 import QtCore, QtWidgets
import qtmax


class MyToolDialog(QtWidgets.QDialog):
    def __init__(self, parent=None):
        super(MyToolDialog, self).__init__(parent)
        self.setWindowTitle("My Tool")
        self.setAttribute(QtCore.Qt.WA_DeleteOnClose)      # 閉じたら破棄する

        self.spin = QtWidgets.QSpinBox()
        self.spin.setRange(1, 100)
        self.spin.setValue(5)
        btn = QtWidgets.QPushButton("箱を並べる")
        btn.clicked.connect(self.make_boxes)

        layout = QtWidgets.QVBoxLayout(self)
        layout.addWidget(QtWidgets.QLabel("個数"))
        layout.addWidget(self.spin)
        layout.addWidget(btn)

    def make_boxes(self):
        with pymxs.undo(True, "箱を並べる"):              # ボタン 1 回 = undo 1 回
            for i in range(self.spin.value()):
                rt.Box(pos=rt.Point3(i * 30, 0, 0))
        rt.redrawViews()                                    # 画面に反映する


dlg = MyToolDialog(parent=qtmax.GetQMaxMainWindow())
dlg.show()                                                 # モードレス(3ds Max を操作しながら使える)
  • show() はモードレス、exec_() はモーダル(閉じるまで 3ds Max を操作できない)です。
  • ボタンから rt を呼ぶ処理は pymxs.undo(True, ...) で囲むと、ユーザが Ctrl + Z で一度に戻せます。
  • 3ds Max はスクリプトの実行中に画面を更新しないので、処理のあとに rt.redrawViews() を呼びます。

ウィンドウを 1 つだけ開く(開き直す)

スクリプトを実行し直すたびにモジュールのグローバル変数は作り直されるので、そこに前回のウィンドウへの参照を入れておいても、次に実行したときにはもう参照できません。確実に前のウィンドウを閉じるには、objectName を付けておき、メインウィンドウの子から findChild で探します。

from PySide2 import QtCore, QtWidgets
import qtmax


def show_my_tool():
    main = qtmax.GetQMaxMainWindow()
    prev = main.findChild(QtWidgets.QDialog, "MyToolDialog")
    if prev is not None:
        prev.close()                                # 前のウィンドウを閉じる

    dlg = QtWidgets.QDialog(main)
    dlg.setObjectName("MyToolDialog")               # findChild で見つけるための名前
    dlg.setWindowTitle("My Tool")
    dlg.setAttribute(QtCore.Qt.WA_DeleteOnClose)    # 閉じたら破棄する
    dlg.show()
    return dlg


_my_tool_window = show_my_tool()
_my_tool_window = show_my_tool()   # 2 回呼んでも見えているウィンドウは 1 つ
  • ⚠️ 親を付けていないウィンドウは、参照が無くなると消えることがあります。親を付けていても、あとで閉じたり開き直したりするために、ウィンドウへの参照はどこかに持っておきます。
  • WA_DeleteOnClose を付けていないと、close() はウィンドウを隠すだけで破棄しません。

ドッキングできるウィンドウ

from PySide2 import QtCore, QtWidgets
import qtmax


class MyDock(QtWidgets.QDockWidget):
    def __init__(self, parent=None):
        super(MyDock, self).__init__(parent)
        self.setWindowTitle("My Dock")
        self.setObjectName("MyToolDock")                 # Qt の状態保存に使う名前(付けないと警告が出る)
        self.setAttribute(QtCore.Qt.WA_DeleteOnClose)
        body = QtWidgets.QWidget()
        layout = QtWidgets.QVBoxLayout(body)
        btn = QtWidgets.QPushButton("選択を表示")
        btn.clicked.connect(lambda: print([o.name for o in rt.selection]))
        layout.addWidget(btn)
        self.setWidget(body)


main = qtmax.GetQMaxMainWindow()
dock = MyDock(parent=main)
main.addDockWidget(QtCore.Qt.RightDockWidgetArea, dock)  # 右端にドッキング
dock.setFloating(True)                                    # 浮かせて表示する場合(最初からドッキングさせるならこの行を外す)
dock.show()
  • ドッキングさせたいときは、親を qtmax.GetQMaxMainWindow()(QMainWindow)にします。QWidget.find(...) で取った親ではドッキングできません。

メッセージ・確認・入力

rt.messageBox("完了しました", title="My Tool")                   # MXS: messageBox "完了しました"
ok = rt.queryBox("続けますか?", title="My Tool")                 # はい → True
from PySide2 import QtWidgets
import qtmax

parent = qtmax.GetQMaxMainWindow()
ret = QtWidgets.QMessageBox.question(parent, "My Tool", "削除しますか?")
yes = ret == QtWidgets.QMessageBox.Yes
text, ok = QtWidgets.QInputDialog.getText(parent, "My Tool", "名前:")   # 文字の入力
  • 簡単な確認なら rt.messageBox / rt.queryBox が手軽です。Qt のツールの中なら、見た目をそろえるために QMessageBox を使います。
  • いずれもモーダルなので、ユーザが閉じるまで処理が止まります。

進捗バーを出す(キャンセル可能)

nodes = list(rt.objects)
rt.progressStart("処理中...")                            # MXS: progressStart "処理中..."
try:
    for i, o in enumerate(nodes):
        o.wirecolor = rt.Color(255, 128, 0)
        if not rt.progressUpdate(100.0 * (i + 1) / len(nodes)):   # キャンセルされたら False
            break
finally:
    rt.progressEnd()                                     # 例外が起きても必ず閉じる
  • 3ds Max 下部のステータスバーに進捗バーが出ます。progressUpdate は 0〜100 を受け取り、ユーザが [キャンセル] を押すと False を返します。
  • ⚠️ progressEnd() を呼ばずに終わると進捗バーが残ります。try / finally で必ず閉じます。

Qt のツールの中で出すなら QProgressDialog も使えます。

from PySide2 import QtCore, QtWidgets
import qtmax

nodes = list(rt.objects)
prog = QtWidgets.QProgressDialog("処理中...", "キャンセル", 0, len(nodes), qtmax.GetQMaxMainWindow())
prog.setWindowModality(QtCore.Qt.WindowModal)
prog.setMinimumDuration(0)                          # 待たずにすぐ表示する
for i, o in enumerate(nodes):
    if prog.wasCanceled():
        break
    o.wirecolor = rt.Color(0, 128, 255)
    prog.setValue(i + 1)
    QtWidgets.QApplication.processEvents()             # 画面を更新してキャンセルを受け付ける
prog.close()

Qt Designer の .ui を読み込む

import os
from PySide2 import QtCore, QtUiTools, QtWidgets
import qtmax

ui_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "my_tool.ui")
f = QtCore.QFile(ui_path)
f.open(QtCore.QFile.ReadOnly)
dlg = QtUiTools.QUiLoader().load(f, qtmax.GetQMaxMainWindow())
f.close()


def make_teapot():
    rt.Teapot()
    rt.redrawViews()


dlg.findChild(QtWidgets.QPushButton, "pushButton").clicked.connect(make_teapot)
dlg.show()
  • __file__ はスクリプトファイルとして実行したときだけ使えます(リスナーに貼った場合は使えません)。

ツールバーやメニューから起動する(マクロスクリプト)

body = 'python.ExecuteFile @"C:\\tools\\my_tool\\main.py"'      # 中身は MaxScript の文字列
macro_id = rt.macros.new("My Tools", "MyTool_Open", "My Tool を開く", "My Tool", body)
# MXS: macroScript MyTool_Open category:"My Tools" ( python.ExecuteFile @"..." )
  • 作ったマクロスクリプトは、[カスタマイズ] → [ユーザインタフェースをカスタマイズ] のカテゴリ「My Tools」に現れ、ツールバー・メニュー・ショートカットに割り当てられます。
  • macros.new を実行するたびに、そのマクロスクリプトのコピーがユーザのマクロスクリプトフォルダ(#userMacros)に .mcr ファイルとして保存されます。
  • マクロスクリプトの中身は MaxScript なので、Python は python.ExecuteFile で呼びます。Python の関数を直接呼びたいときは、rt.myToolOpen = 関数 のように MaxScript のグローバルに入れておき、中身を 'myToolOpen()' にします(→「MaxScript との連携」章)。この方法は、3ds Max の起動時にその関数を定義するモジュールを読み込んでおく必要があります(→「モジュール管理・デバッグ」章)。

落とし穴まとめ

  • 親は 3ds Max のメインウィンドウ。ドッキングするなら qtmax.GetQMaxMainWindow()。
  • 親を付けていないウィンドウへの参照をどこにも持たないと、ガベージコレクションで消えることがある。
  • 同じスクリプトを実行し直すとウィンドウが増える。objectName と findChild で前のものを見つけて閉じてから開く。
  • rt の操作のあとは rt.redrawViews()。ボタン単位で pymxs.undo(True) で囲む。
  • progressEnd() は finally で必ず呼ぶ。

関連

← 3ds Max Python チートシート へ