【MotionBuilder】Python SDK 入門 第8回 『UI・ツール開発 - PySide』
この記事は、Python SDK 入門 の第8回目の記事です。
今回は、UI・ツール開発の前の準備として、Qt(PySide) を利用してUIの要素を取り扱う基礎的な方法について説明します。
1. はじめに
1.1. Qt / PySide について
Qt とは、クロスプラットフォームのGUI作成フレームワークです。
複数のモジュール群から成る大規模なプロジェクトであり[1]、ユーザーは用途に応じて各モジュールを導入して開発を行います。
Qt Documentation - All Modules
工業製品や描画ソフト、DCC ツールなど様々な場所で導入されていており、MotionBuilder や Maya のUIにも Qt が利用されています。
PySide (Qt for Python) は C++ の Qt のライブラリを Python から利用できるようにしたものです。Maya や Houdini のUI作成で馴染みのある方もいらっしゃるかもしれません。
PySide の導入は簡単で、下記コマンドで必要なパッケージが全てインストールされます。
pip install PySide6
このコマンドで PySide とともにインストールされる Shiboken パッケージは、Python とのバインディングの内部情報を提供するものです[2]。Qt を利用したアプリケーションでは、例えば以下のような処理で SDK から取得した UI 要素を PySide で利用します。
from PySide6 import QtWidgets
from shiboken6 import wrapInstance
# Creates a Python wrapper for a C++ object instantiated at a given memory address
# PySide6.QtWidgets.QWidget型へ
widget = wrapInstance(widget_CppPtr_FromSDK, QtWidgets.QWidget)
1.2. MotionBuilder と Qt
MotionBuilder のインストール時、Qt のライブラリも一緒にダウンロードされています。
MotionBuilder 起動時にリンクされる Qt ライブラリ
PySide のパッケージも同様にインストールされていて、MotionBuilder 内部の python.exe のモジュール探索パス下にあるため、Python Editor から利用できます。
import PySide2
print(PySide2)
>>>
<module 'PySide2' from 'C:\\Program Files\\Autodesk\\MotionBuilder 2024\\bin\\x64\\python\\site-packages\\PySide2\\__init__.py'>
MotionBuilder のバージョンによって、使用されている Qt のバージョンが下記の通り異なるので注意が必要です[3]。
- MotionBuilder 2025 : Qt 6.5.3 / PySide6
- MotionBiulder 2024 : Qt 5.15.2 / PySide2
2024以前と2025のどちらのバージョンにも対応するには、import文を以下のように書きます。
try:
# MotionBuilder 2025 向け
from PySide6.QtWidgets import QWidget
from shiboken6 import wrapInstance, getCppPointer
except:
# MotionBuilder 2024 以前向け
from PySide2.QtWidgets import QWidget
from shiboken2 import wrapInstance, getCppPointer
2. Qt で UI を分析する
2.1. MainWindow と全体のUI
MotionBuilder の SDK には FBGetMainWindow()
という、MainWindow へのポインターを取得する関数があります。
from pyfbsdk import FBGetMainWindow
try:
from PySide6.QtWidgets import QMainWindow
from shiboken6 import wrapInstance
except:
from PySide2.QtWidgets import QMainWindow
from shiboken2 import wrapInstance
win_ptr = FBGetMainWindow()
win = wrapInstance(win_ptr, QMainWindow) # get MainWindow
print(win_ptr)
print(win)
>>>
1772773305808
<PySide2.QtWidgets.QMainWindow(0x19cc185edd0) at 0x0000019CD76E4E40>
QMainWindow
は、アプリケーションのメインのUI(下記構造)を提供するクラスです。
PySide6.QtWidgets.QMainWindow - Qt for Python
一度簡単なアプリケーションを作り、MainWindow を確認してみましょう。
import sys
try:
from PySide6 import QtCore
from PySide6.QtGui import Qt
from PySide6.QtWidgets import (QApplication, QMainWindow, QMenuBar,
QStatusBar, QToolBar, QDockWidget,
QWidget, QHBoxLayout, QVBoxLayout, QLabel)
except:
from PySide2 import QtCore
from PySide2.QtGui import Qt
from PySide2.QtWidgets import (QApplication, QMainWindow, QMenuBar,
QStatusBar, QToolBar, QDockWidget,
QWidget, QHBoxLayout, QVBoxLayout, QLabel)
class CustomMainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("Application Title")
'''
(中略)
'''
app = QApplication(sys.argv)
win = CustomMainWindow()
win.showMaximized()
if(int(QtCore.qVersion()[0]) < 6):
app.exec_()
else:
app.exec()
ソース全文
import sys
try:
from PySide6 import QtCore
from PySide6.QtGui import Qt
from PySide6.QtWidgets import (QApplication, QMainWindow, QMenuBar,
QStatusBar, QToolBar, QDockWidget,
QWidget, QHBoxLayout, QVBoxLayout, QLabel)
except:
from PySide2 import QtCore
from PySide2.QtGui import Qt
from PySide2.QtWidgets import (QApplication, QMainWindow, QMenuBar,
QStatusBar, QToolBar, QDockWidget,
QWidget, QHBoxLayout, QVBoxLayout, QLabel)
class CustomMainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("Application Title")
# Allow dockwidgets to dock in horizontal order
self.setDockNestingEnabled(True)
# add menu bar
mbar = QMenuBar()
mbar.addMenu("Menubar menu")
self.setMenuBar(mbar)
# Add status bar
sbar = QStatusBar()
sbar.showMessage("Statusbar message")
sbar.setHidden(False)
self.setStatusBar(sbar)
# Add Tool bar
top_toolbar = QToolBar()
top_toolbar.setObjectName("top toolbar")
top_toolbar.addAction("Toolbar menu1")
side_toolbar = QToolBar()
side_toolbar.setObjectName("side toolbar")
side_toolbar.addAction("Toolbar\nmenu2")
self.addToolBar(Qt.ToolBarArea.TopToolBarArea, top_toolbar)
self.addToolBar(Qt.ToolBarArea.LeftToolBarArea, side_toolbar)
# Add Dock Widget1
dwig1 = QDockWidget()
dwig1.setWindowTitle("dockwidget1")
dwig1.setObjectName("dockwidget1")
container1 = QWidget()
dwig1.setWidget(container1)
layout1 = QVBoxLayout()
label1 = QLabel("Widget in dockwidget1")
layout1.addWidget(label1)
container1.setLayout(layout1)
dwig1.setAllowedAreas(Qt.AllDockWidgetAreas)
self.addDockWidget(Qt.LeftDockWidgetArea, dwig1)
# Add Dock Widget2
dwig2 = QDockWidget()
dwig2.setWindowTitle("dockwidget2")
dwig2.setObjectName("dockwidget2")
container2 = QWidget()
dwig2.setWidget(container2)
layout2 = QVBoxLayout()
label2 = QLabel("Widget in dockwidget2")
layout2.addWidget(label2)
container2.setLayout(layout2)
dwig2.setAllowedAreas(Qt.AllDockWidgetAreas)
self.addDockWidget(Qt.RightDockWidgetArea, dwig2)
def showEvent(self, event):
super().showEvent(event)
self.PrintWindowStructure()
def PrintWindowStructure(self):
for child in self.children():
print(child)
app = QApplication(sys.argv)
win = CustomMainWindow()
win.showMaximized()
if int(QtCore.qVersion()[0]) < 6:
app.exec_()
else:
app.exec()
python test.py
でアプリケーションが起動します。
作成されたアプリケーション
Toolbar menu の部分は見覚えのある方もいるかもしれません。Maya の Shelf や 3dsMax のリボン等に使われている Widget[4]です。
Maya における Toolbar の例
アプリケーションのUIが全てこの構成だとは限らず、実際 MotionBuilder では "Toolbars" や "Central Widget"、"Status Bar" にあたる要素はありません。また、MotionBuilder における Menu Bar については、上記スクリプトで作成されるような単純なものではなく、ユーザーが独自に作成した Widget で構成される Custom Menu Bar というものが使用されています。詳しくは 2.3.節 で解説します。
2.2. DockWidget
QDockWidget
クラスが提供する DockWidget は、Float/Dock および Close という特徴をもつ[5]Widget で、最初は MainWindow に Dock(接続)された状態で存在しています。
Float ボタンと Close ボタン
Qtにおいて、QObject
クラスを継承するすべてのオブジェクトは一種の特殊な親子関係[6]を持ちうるのですが、この特殊な関係があることで、親にあたるオブジェクトが削除された時に子オブジェクトも削除されます。ここで、MotionBuilder の MainWindow の構成(ペアレントされた Widget)を調べてみましょう。
from pyfbsdk import FBGetMainWindow
try:
from PySide6.QtWidgets import QMainWindow
from shiboken6 import wrapInstance
except:
from PySide2.QtWidgets import QMainWindow
from shiboken2 import wrapInstance
win_ptr = FBGetMainWindow()
win = wrapInstance(win_ptr, QMainWindow)
# ペアレントされた Widget を出力
for child in win.children():
print(child)
>>>
<PySide2.QtWidgets.QLayout(0x22a86218fc0, name = "_layout") at 0x0000022A9669E680>
<PySide2.QtWidgets.QWidget(0x22a8052d9f0) at 0x0000022A966CE6C0>
<PySide2.QtWidgets.QDockWidget(0x22a97eadc20, name="ToolWindow_2") at 0x0000022A966CE780>
<PySide2.QtWidgets.QDockWidget(0x22a97621a60, name="ToolWindow_3") at 0x0000022A966CEB80>
<PySide2.QtWidgets.QDockWidget(0x22a97620140, name="ToolWindow_4") at 0x0000022A966CE7C0>
<PySide2.QtWidgets.QDockWidget(0x22afd58f510, name="ToolWindow_5") at 0x0000022A966CEA40>
<PySide2.QtWidgets.QDockWidget(0x22a97636370, name="ToolWindow_6") at 0x0000022A966CE800>
<PySide2.QtWidgets.QDockWidget(0x22a97636130, name="ToolWindow_7") at 0x0000022A966CE980>
<PySide2.QtWidgets.QDockWidget(0x22a976386b0, name="UniqueName_00:59:05:17 (90)") at 0x0000022A966CE880>
<PySide2.QtWidgets.QDockWidget(0x22a955c2740, name="ToolWindow_1") at 0x0000022A966CEAC0>
DockWidget には、上記出力で示された objectName(accessibleName) の他に、表示名(windowTitle)があります。
# DockWidget のタイトルを表示
for child in win.children():
if type(child) == QDockWidget:
print(child.windowTitle())
>>>
Python Editor
Asset Browser
Properties
Transport Controls - Keying Group: TR
Navigator
Python Tool Manager
Profiling Center
Viewer
出力されたタイトルには見覚えがあるはずです。
MotionBuilder の「Window」のタイトルですね。
出力されたのはここの表示
スクリプト実行時に表示されていた Window のタイトル一覧が出力されました。これを踏まえると、MotionBuilder において Window と呼ばれていたものは、Qt における DockWidget に対応することになります。
補足:出力された DockWidget 以外の child について
<PySide2.QtWidgets.QLayout(0x22a86218fc0, name = "_layout") at 0x0000022A9669E680>
<PySide2.QtWidgets.QWidget(0x22a8052d9f0) at 0x0000022A966CE6C0>
QLayout は Widget を整列させるはたらきをするもので、この QLayout
型のオブジェクトは各 DockWidget(= Window) を MainWindow 内で整列させているものです。すなわち、画面左上「Layout」で選択するレイアウトに対応します。
もう一つの QWidget
型のオブジェクトは前節で触れた Custom Menu Bar にあたるものです。
2.3. Menu Bar
MotionBuilder の Menu Bar は通常のものとは異なる Custom Menu Bar です。
Menu Bar にあたる箇所
まずは Custom Menu Bar 全体にあたる Widget を取得してみましょう。
from pyfbsdk import FBGetMainWindow
try:
from PySide6.QtWidgets import QMainWindow
from shiboken6 import wrapInstance
except:
from PySide2.QtWidgets import QMainWindow
from shiboken2 import wrapInstance
win_ptr = FBGetMainWindow()
win = wrapInstance(win_ptr, QMainWindow)
# Menu Bar 部にあたる Widget を取得
menuwidget = win.menuWidget()
print(menuwidget)
>>>
<PySide2.QtWidgets.QWidget(0x20ab93b32e0) at 0x0000020ACF3AC940>
続いて、Custom Menu Bar 内の各 Widget も取得してみましょう。
# Menu Bar 部の Widget を構成する要素を出力
for child in menuwidget.children():
print(child)
>>>
<PySide2.QtWidgets.QHBoxLayout(0x20ab93b3d90) at 0x0000020ACF392F00>
<PySide2.QtWidgets.QMenuBar(0x20ab93b34e0) at 0x0000020ACD8CB040>
<PySide2.QtWidgets.QWidget(0x20ad00a5500) at 0x0000020ACEEB3C80>
QHBoxLayout
型のオブジェクトは、QLayout.QBoxLayout
型を継承していて、特に水平に(Horizontal)Widgetを整列させる場合に使用されるものです(垂直 - Vertical - の場合はQVBoxLayout
型を使います)。このレイアウトによって、残りの QMenubar
型と QWidget
型の Widget が整列して存在しています。
出力されたQWidget
型のオブジェクトは、画面右上のアカウント情報に関する部分です。
この部分の Widget は複雑なので解説は割愛します。
最後に QMenuBar
型のオブジェクトについてです。画面左上の「File」「Edit」等のメニューを表示している部分になります。
QMainWindow
クラスには、Menu Bar 部の Widget を取得するメンバ関数として menuBar()
と menuWidget()
がありますが、以下のような実装の違いがあり[7]、注意が必要です。
関数 | 実装内容 |
---|---|
menuBar() |
Menu Bar 部にある Widget が QMenuBar 型(およびその派生型)であればその Widget を返し、そうでなければ作り変える |
menuWidget() |
型によらず、Menu Bar 部にある Widgetを(あれば)返す |
特に menuBar()
はおすすめ出来ません。
2024以前のバージョンではクラッシュし、2025では Menu Bar が消えるためです。
win_ptr = FBGetMainWindow()
win = wrapInstance(win_ptr, QMainWindow)
# 非推奨 !!!
menuwidget = win.menuBar()
以上を踏まえると、Menu Bar およびその中のメニューを取得する方法は以下の通りになります。
from pyfbsdk import FBGetMainWindow
try:
from PySide6.QtWidgets import QMainWindow, QMenuBar
from shiboken6 import wrapInstance
except:
from PySide2.QtWidgets import QMainWindow, QMenuBar
from shiboken2 import wrapInstance
win_ptr = FBGetMainWindow()
win = wrapInstance(win_ptr, QMainWindow)
# Custom Menu Bar 内のメニュー部を取得
menuwidget = win.menuWidget()
menubar = None
for child in menuwidget.children():
if type(child) == QMenuBar:
menubar = child
# メニュー名の表示
for menu in menubar.actions():
print(menu.text())
>>>
File
Edit
Animation
Settings
Layout
Open Reality
Python Tools
Window
Help
Tools
Scripts
では、Menu Bar のメニュー部に新たなメニューを追加することを考えます。
QMenuBar
クラスにおけるメニューの追加方法は、addMenu(title)
, addMenu(icon,title)
, addMenu(menu)
の3つです。
from pyfbsdk import FBGetMainWindow, FBMessageBox
try:
from PySide6.QtWidgets import QMainWindow, QMenuBar, QMenu
from PySide6.QtGui import QIcon
from shiboken6 import wrapInstance
except:
from PySide2.QtWidgets import QMainWindow, QMenuBar, QMenu
from PySide2.QtGui import QIcon
from shiboken2 import wrapInstance
win_ptr = FBGetMainWindow()
win = wrapInstance(win_ptr, QMainWindow)
menuwidget = win.menuWidget()
menubar = None
for child in menuwidget.children():
if type(child) == QMenuBar:
menubar = child
# 3通りの方法で Menu Bar にメニューを追加
newmenu1 = menubar.addMenu("menu1")
action1 = newmenu1.addAction("content1")
action1.triggered.connect(lambda checked=False,
message = "menu1 - content1 is triggered."
: FBMessageBox("Message", message, "OK"))
newmenu2 = menubar.addMenu(QIcon("path/to/icon.png"),"menu2")
action1 = newmenu2.addAction("content2")
action1.triggered.connect(lambda checked=False,
message = "menu2 - content2 is triggered."
: FBMessageBox("Message", message, "OK"))
newmenu3 = QMenu("menu3", None)
menubar.addMenu(newmenu3)
action3 = newmenu3.addAction("content3")
action3.triggered.connect(lambda checked=False,
message = "menu3 - content3 is triggered."
: FBMessageBox("Message", message, "OK"))
Menu Bar に新たにメニューが作られ、メニュー内のコンテンツをクリックすると、メッセージボックスが表示されます。
作成されたメニューと、表示されたメッセージボックス
補足:SDK を利用する方法
MotionBuilder の SDK には、上部の Menu Bar のメニューを扱う専用のクラス FBMenuManager
があります。
詳しくは FBMenuManager Class Reference をご覧ください。
3. 応用例
3.1. プラグインのように用いる
第3回 で触れたように、MotionBuilder は起動時に特定のパス下[8]のモジュールを全て実行します。そのパス下にUIを作成する処理をするスクリプトを置いておけば、起動時に実行されることで起動直後には新たなUIが表示されていることになります。
自分の場合、作成したスクリプトを特定のディレクトリに保存しておき、Menu Bar へのメニュー追加とファイルの読み込みを行うスクリプトを起動時に実行させるようにすることで、Menu Bar からオリジナルのスクリプトを実行できるようにしています。
作成スクリプト実行用のメニュー
以下リポジトリに作成方法およびソースをまとめているので、よければ覗いてみてください。
3.2. ドラッグアンドドロップに対応させる
Drag and Drop の機能を備えた ToolBar を作成します。スクリプトファイルをドラッグアンドドロップするとファイル名が登録され、クリックするとスクリプトが実行されるようにします。
from pyfbsdk import FBGetMainWindow, FBApplication
from pathlib import Path
try:
from PySide6.QtWidgets import QMainWindow, QMenuBar, QToolBar, QAction
from PySide6.QtGui import Qt, QDragEnterEvent, QDropEvent
from shiboken6 import wrapInstance
except:
from PySide2.QtWidgets import QMainWindow, QMenuBar, QToolBar, QAction
from PySide2.QtGui import Qt, QDragEnterEvent, QDropEvent
from shiboken2 import wrapInstance
class CustomToolBar(QToolBar):
def __init__(self):
super().__init__()
self.setAcceptDrops(True) # ドラッグアンドドロップ有効化
def dragEnterEvent(self, event: QDragEnterEvent):
if event.mimeData().hasUrls() or event.mimeData().hasText():
event.acceptProposedAction()
else:
event.ignore()
def dropEvent(self, event: QDropEvent):
mime_data = event.mimeData()
if mime_data.hasUrls():
file_paths = [url.toLocalFile() for url in mime_data.urls()]
for path in file_paths:
script_path = Path(path)
script_name = script_path.name
action = self.addAction(script_name)
action.triggered.connect(lambda checkd = False : FBApplication().ExecuteScript(str(script_path)))
event.acceptProposedAction()
def RemoveActions(self):
for action in self.actions():
if(action.text() != "Remove\nActions"):
self.removeAction(action)
win_ptr = FBGetMainWindow()
win = wrapInstance(win_ptr, QMainWindow)
menuwidget = win.menuWidget()
menubar = None
for child in menuwidget.children():
if type(child) == QMenuBar:
menubar = child
# ToolBar を新規作成
side_toolbar = CustomToolBar()
win.addToolBar(Qt.ToolBarArea.LeftToolBarArea, side_toolbar) # 左側に作成
action = QAction("Remove\nActions")
side_toolbar.addAction(action)
action.triggered.connect(lambda checked = False : side_toolbar.RemoveActions())
以下のように ToolBar が作成されます。
作成された ToolBar と登録されたスクリプト
「Remove Actions」ボタンを押すと、ドラッグアンドドロップして登録された各 Action を消去できます。また、上記スクリプトでは複数のスクリプトをまとめてドロップして登録することも可能です。
この、スクリプトをドラッグアンドドロップさせてUIに登録するアイデアは、Digital Frontier 様の記事を参考にさせていただきました。
次回
今回は『UI・ツール開発』のテーマの2回目として、Qt(PySide) を利用してUIの要素を取り扱う基礎的な方法を示しました。次回は最終回として、PySide を利用した実践的なツール開発の手法を解説します。
それでは、今回はここまで。最後までお読みくださりありがとうございました。
-
各バージョンで MotionBuilder SDK Requirements を参照 ↩︎
-
Float や Close ボタンの無い(機能を持たない)DockWidget もあります ↩︎
-
"Parenting system" と呼ばれます。詳細は Qt for Beginners を参照 ↩︎
-
QMainWindow::menuWidget()
は内部でQLayout::menuBar()
を呼び出すため、QMainWindow::menuBar()
と異なりQMenuBar
型へのキャストを行いません(qmainwindow.cpp を参照) ↩︎
Discussion