graphics.hatenablog.com

技術系テクニカルアーティストのあれこれ

Maya 用 PySide メインウィンドウの雛形。

ホリデープログラミング向けの Maya メインウィンドウの雛形を晒してみる。
ちょいちょい雑な実装もしてるけど、まぁそこはあくまで個人開発のテスト向け*1ってことで。
maya_test/maya.py at master · hal1932/maya_test · GitHub

ライブラリコード

class MayaMainWindowBase(QMainWindow, MayaQWidgetBaseMixin):

    @staticmethod
    def get_maya_window():
        # type: () -> QWidget
        maya_main_window_ptr = omui.MQtUtil.mainWindow()
        return wrapInstance(long(maya_main_window_ptr), QWidget)

    @property
    def absolute_name(self):
        # type: () -> NoReturn
        return '{}.{}'.format(self.__module__, self.__class__.__name__)

    def __init__(self):
        maya_window = MayaMainWindowBase.get_maya_window()

        for child in maya_window.children():
            # reload でポインタが変わったときのために名前で比較する
            if child.objectName() == self.absolute_name:
                child.close()

        super(MayaMainWindowBase, self).__init__(parent=maya_window)
        self.setObjectName(self.absolute_name)
        self.setAttribute(Qt.WA_DeleteOnClose)

    def setup_ui(self):
        # type: () -> MayaMainWindowBase
        widget = self.centralWidget()
        if widget is not None:
            widget.deleteLater()
        widget = QWidget()

        self.setCentralWidget(widget)
        self._setup_ui(widget)
        return self

    def closeEvent(self, _):
        # type: (QCloseEvent) -> NoReturn
        self._shutdown_ui()

    def _setup_ui(self, central_widget):
        # type: (QWidget) -> NoReturn
        pass

    def _shutdown_ui(self):
        pass
class MayaAppBase(object):

    def __init__(self):
        self._window = None

    def execute(self):
        app = QApplication.instance()
        self._initialize(app)
        self._window = self._create_window()
        if self._window is not None:
            self._window.setup_ui().show()

    def _initialize(self, app):
        # type: (QApplication) -> nore
        pass

    def _create_window(self):
        # type: () -> MayaMainWindowBase
        pass

ポイントはだいたい以下のとおり。

findChildren() を使わない多重起動防止

class MayaMainWindowBase(QMainWindow, MayaQWidgetBaseMixin):
    def __init__(self):
        ...
        for child in maya_window.children():
            if child.objectName() == self.absolute_name:
                child.close()

自分の手許では こんなコード を運用してる都合で、findChildren() を使うわけにはいかなかった。

というのも、findChildren() は TypeObject を引数にとる。つまり、TypeObject のポインタの参照先が一致するかどうかで判定してるわけだ。てことは、TypeObject 自体のアドレスが変わると当然ながら破綻する。
普通に考えたらそんなことは起きようがないんだけど、Maya の場合は reload を多用する*2都合上、特定のケースでモジュール内に埋め込まれてるクラス定義のポインタが変わってしまう。となると、TypeObject を使った比較をするわけにはいかない。

しかなたくクラスのフルネームを objectName に設定して、それで文字列比較をすることにしてる。

UI を動的に再構築するための一番楽な実装

class MayaMainWindowBase(QMainWindow, MayaQWidgetBaseMixin):
    def setup_ui(self):
        widget = self.centralWidget()
        if widget is not None:
            widget.deleteLater()
        widget = QWidget()
        ...

centralWidget をライブラリ層で抱えて、再構築するときは centralWidget を丸ごと破棄してる。おそらくこれが一番楽で、何も考える必要がない。

ちなみに「UI の動的な再構築」のユースケースとしては、多言語対応したときの言語切替とか、状況に応じた動的な UI を組みたいときとか。だいたいそんなかんじ。もちろん実戦投入するときは、ちゃんとそのへんも丁寧にハンドリングすべきなんだけど、いろんなパターンを矢継ぎ早にいくつも試すような状況でそんな面倒なことはしてられない。

QApplication 生成ロジックの隠蔽

class MayaAppBase(object):
    def execute(self):
        app = QApplication.instance()
        ...

スタンドアロンアプリ開発のときは app = QApplication() とか app.exec_() が必要だけど、Maya のとき*3は app = QApplication.instance() にしないといけない。どうせならアプリのコアロジックを組むためにアタマを使いたい。QApplication の扱いみたいなちっちゃいとこをいちいち考えたくない。せっかくがんばってコードを書くわけだし、Maya とか関係なくソースコードを使いまわしたい。

class StandaloneAppBase(object):
    def execute(self):
        app = QApplication(sys.argv)
        ...
        sys.exit(app.exec_())

たとえばこんなコードを書いて MayaAppBase と StandaloneAppBase を切り替えながら使ってもいいし、なんなら共通の AppBase クラスを用意して Maya/Standalone の切り替えを Configurable にしてもいい。なんにせよ、考えたくないことは考えないに限る。

QApplication の初期化と QMainWindow の初期化を分離

class MayaAppBase(object):
    def _initialize(self, app):
        """appの初期化"""
        pass

    def _create_window(self):
        """QMainWindowの初期化"""
        pass

「扱う対象のレイヤーに応じた設計をしましょう」っていう、よくある話。

たとえば多言語対応なんかはその典型例で、QTranslator は QApplication に作用させなきゃいけないけど、一方で QMainWindow は QMainWindow として扱ってやる必要はある。実装はちょっとだけ複雑になるけど、慣れてくるとこっちのほうが全然楽に扱える。あと、前段のとおり「いろんなパターンを矢継ぎ早にいくつも試すような状況」というのがここでの典型的なユースケースなので、「コード上のどこに何が書いてあるか」は可能な限り明示的にしておきたい。実装を変えるときに、どこを変えればいいか考える時間を減らすことができる。*4

ユーザーコードの例

class MainWindow(MayaMainWindowBase):

    lang_switch_requested = Signal(str)

    def __init__(self):
        super(MainWindow, self).__init__()

    def _setup_ui(self, central_widget):
        # type: (QWidget) -> NoReturn
        self.setWindowTitle(self.tr('window_title'))

        def switch_lang(name):
            self.lang_switch_requested.emit(name)
            self.setup_ui()

        lang_switch_jajp = QPushButton(self.tr('lang_jajp'))
        lang_switch_jajp.clicked.connect(lambda: switch_lang('ja_JP'))

        lang_switch_enus = QPushButton(self.tr('lang_enus'))
        lang_switch_enus.clicked.connect(lambda: switch_lang('en_US'))

        central_widget.setLayout(vbox(
            hbox(
                lang_switch_jajp,
                lang_switch_enus,
            ),
            QLabel(self.tr('label1')),
            QLabel(self.tr('label2')),
        ))
class MayaApp(MayaAppBase):

    def __init__(self):
        super(MayaApp, self).__init__()
        self.__translator = QTranslator()

    def _initialize(self, app):
        # type: (QApplication) -> NoReturn
        self.__switch_languages('ja_JP')
        app.installTranslator(self.__translator)

    def _create_window(self):
        # type: () -> MayaMainWindowBase
        window = MainWindow()
        window.lang_switch_requested.connect(lambda lang: self.__switch_languages(lang))
        return window

    def __switch_languages(self, lang_name):
        # type: (str) -> NoReturn
        self.__translator.load(lang_name, directory=os.path.join(os.path.dirname(__file__), 'i18n'))


def main():
    app = MayaApp()
    app.execute()

*1:そもそも業務で実戦投入してるコードをこんなとこで晒すわけにはいかない。ほんとはそっちを出したいんだけど……。

*2:詳しくはこちら → Python で from import を reload する。 - graphics.hatenablog.com

*3:Maya 自体が内部で QApplication を抱えてる。

*4:それはそれとして大規模設計なんかのときも、ちゃんと分離しとかないと複数人での共同開発ができなくなって軽く死ねる。