オランのまったりな
開発日記
PymelのWith文でまとめれる(Cmds)のUIレイアウトの書き方を、Qt-PysideのUIにでも構築できるようにしてみよう
Sorry, failed to load main article image
Bantukul Olarn
    
Nov 26 2017 01:04
  
Qt-PysideのUIクラスを拡張してPymelのWith文でウイジェット・レイアウトを組めるようにしてみる


目的&試作の機能について

Withブロックを出る時点で呼ばれるexit()関数の中でWithブロックの中である前Python Stackからローカル変数を取得して自動的に新しいレイアウトに付ける。また、WithブロックがClassのInstanceの中に宣言されたらレイアウト処理をした後、取得したローカル変数をそのInstanceの__dict__へ追加する(Bound Variableになる)

  • 本作では QtのQVBoxLayoutとQHBoxLayoutのみ実装する
  • 前Stackのローカル変数のほか、前Stack(Withの中)で宣言したBoundVariableも取得できるが、本作では実装していない。
  • 一回Withで構築したを再利用できるようにすることもできそうが、本作では実装していない。
  • Withのexit()が逆順で呼ばれるためLayout変数にParentウイジェットを与えないと即座CGで削除されてしまう。

Original Pymel UI Code
pymel with statement ui example

Pymel (cmds) UI

from __future__ import print_function, absolute_import, division

from pymel.core import *

def main():

    with window(menuBar=True, menuBarVisible=True,) as win:
        # start the template block
        # with template:
        with columnLayout(adjustableColumn=True):
            button(label='One')
            button(label='Two')
            with columnLayout(adjustableColumn=True):
                button(label='sub1V1')
                with horizontalLayout():
                    button(label='sub2H1')
                    button(label='sub2H2')
            button(label='Three')
            with horizontalLayout():
                button(label='sub3H1')
                button(label='sub3H2')
                with columnLayout(adjustableColumn=True):
                    button(label='sub4H1')
                    button(label='sub4H2')
            button(label='Four')

main()
UI構築するための基礎テンプレート
pyside2 empty window

Empty skeletal UI

必要最低限Pyside2 UI (Standalone)
from __future__ import absolute_import, division, print_function
import sys
from PySide2 import QtWidgets, QtGui, QtCore

class _MinimalPySide2Window(object):
    def __init__(self):
        self.qapp = QtWidgets.QApplication(sys.argv)

        self.win = QtWidgets.QDialog(
            windowTitle="Example!",
            objectName="exampleLayoutDialog"
        )

    def show(self):
        self.win.show()
        sys.exit(self.qapp.exec_())


demo = _MinimalPySide2Window()
demo.show()
必要最低限Pyside2 UI (Maya)
from __future__ import absolute_import, division, print_function

from PySide2 import QtWidgets, QtGui, QtCore
import maya.OpenMayaUI as omui
import shiboken2

def maya_window_kudasai():
    return shiboken2.wrapInstance(
        long(omui.MQtUtil.mainWindow()),
        QtWidgets.QWidget
    )

class _MinimalPySide2Window(object):
    def __init__(self):
        self.win = QtWidgets.QDialog(
            parent=maya_window_kudasai(),
            windowTitle="Example!",
            objectName="exampleLayoutDialog"
        )

    def show(self):
        self.win.show()

demo = _MinimalPySide2Window()
demo.show()

前のStackの中身を取得するのに

いろいろ試した結果Inspectで実装するようにした

以下は前Stackの中身を取得するサンプル

from __future__ import absolute_import, division, print_function
import inspect, pprint

def afunction():
    stack = inspect.stack()
    previous_frame = stack[1][0]

    print("All stacks")
    pprint.pprint(stack)

    print("Previous frame")
    pprint.pprint(previous_frame)

    print("Previous frame locals")
    pprint.pprint(previous_frame.f_locals)

def containingfunction():

    # Target local variable
    local_a = 0
    local_b = 1
    local_c = "aaaa"

    # With statement's exit() statement equivalent
    afunction()

containingfunction()

"""
example output:

All stacks
[(<frame object at 0x000001CE9BD41200>,
  'fol/config/scratches/scratch_5.py',
  5,
  'afunction',
  ['    stack = inspect.stack()\n'],
  0),
 (<frame object at 0x000001CE9BD41048>,
  'fol/config/scratches/scratch_5.py',
  25,
  'containingfunction',
  ['    afunction()\n'],
  0),
 (<frame object at 0x000001CE9980EA38>,
  'fol/config/scratches/scratch_5.py',
  27,
  '<module>',
  ['containingfunction()\n'],
  0)]
Previous frame
<frame object at 0x000001CE9BD41048>
Previous frame locals
{'local_a': 0, 'local_b': 1, 'local_c': 'aaaa'}

"""

一つのスコープしか持っていないWithブロックを整理する方法

PythonのWithブロックは本来スコープやスタックを持っていない。いくつWithブロックを宣言しても重ねても中身が同じスコープに残されてしまう。もちろんWith文を出た後もアクセスできる。そのためにWithブロックごとの「バーチャルWithスコープ」を実装しなければならない。

本作では2つのグローバル変数で実装しています。

g_with_parent_ctx_stack = []

enter()/exit()関数が呼ばれる順番がStackの挙動に近いため、このグローバルリスト変数をWithバーチャルスタックとして用いる。

g_ext_ctx = set()

処理する前に存在または処理したオブジェクト(要するに現在のWithブロックの外にあるすべてのオブジェクト)をスルーするために置いてあったグローバルセット変数。バーチャルスタックが進むたびに調整される

取得した変数を自動的にレイアウトに付ける

QWidgetかQLayoutの継承クラスかを判断して適切のBound関数を呼ぶ。

# 'self' is a layout object
    def _auto_add(self, obj):
        if issubclass(type(obj), QtWidgets.QWidget):
            self.addWidget(obj)
        elif issubclass(type(obj), QtWidgets.QLayout):
            self.addLayout(obj)
        else:
            raise NotImplementedError(self, obj)
今作で使用されるSetオペレーションの簡単説明
#encoding: utf8
from pprint import pprint

dict_from_dict_comprehension = {key:val for key, val in [
    (0,1),(3,4),(5,6)
]}

pprint(dict_from_dict_comprehension)
# {0: 1, 3: 4, 5: 6}

# set converted from dict only contains key
# Dictでセットを生成されるとKey値のセットになる
set_from_dict= set(dict_from_dict_comprehension)
pprint(set_from_dict)
# set([0, 3, 5])

# set comprehension (Not dict comprehension!)
# ↓DictではなくSetタイプ
set_from_comprehension = {val for val in [0,1,2,3,4]}
pprint(set_from_comprehension)
# set([0, 1, 2, 3, 4])

del dict_from_dict_comprehension, set_from_dict, set_from_comprehension

set_a = set([1,2,3])
set_b = set([3,4,5])
set_c = set([5,6,7,8])

# UNION
pprint( set_a | set_b )
# set([1, 2, 3, 4, 5])
pprint( set_a | set_b | set_c )
# set([1, 2, 3, 4, 5, 6, 7, 8])

# DIFFERENCE l
pprint( set_a - set_b )
# set([1, 2])


# DIFFERENCE r
pprint( set_b - set_a)
# set([4, 5])


# INTERSECT
pprint( set_a & set_b )
# set([3])

# CHAINED
pprint(set_c & set_b | set_a)
# set([1, 2, 3, 5])

前Stackの変数宣言順番の再構築

前Stackの変数の取得先のf_localsがDictタイプの為宣言順番の情報(Keyの順番)が失われてしまう。レイアウトの追加順番がウイジェットの位置を決めるため再現できないとかなり致命的な問題となるが、幸運なことに同じFrameオブジェクトの中のCodeオブジェクトにあるco_varnames変数が宣言順番を保持してある。このため宣言順番はこの情報で再構築した。

Final working code

# -*- coding: utf-8 -*-

# !/usr/bin/env python
# coding=utf-8

from __future__ import absolute_import, division, print_function

import inspect

import maya.OpenMayaUI as omui
import shiboken2
from PySide2 import QtWidgets, QtGui, QtCore

import sys

g_with_parent_ctx_stack = []
g_ext_ctx = set()


def maya_window_kudasai():
    return shiboken2.wrapInstance(
        long(omui.MQtUtil.mainWindow()),
        QtWidgets.QWidget
    )


def previous_locals(stack):
    # return deep copy to ensure original stack aren't disturbed
    return stack[1][0].f_locals


class AutoScopedLayoutMixin(object):
    def _auto_add(self, obj):
        if issubclass(type(obj), QtWidgets.QWidget):
            self.addWidget(obj)
        elif issubclass(type(obj), QtWidgets.QLayout):
            self.addLayout(obj)
        else:
            raise NotImplementedError(self, obj)

    def __enter__(self, *args):
        global g_with_parent_ctx_stack, g_ext_ctx

        raw_locals = previous_locals(inspect.stack())

        owner_instance = raw_locals.get('self', None)

        # records owner instace's __dict__ state
        self._owner_instance_dict = set(owner_instance.__dict__.keys())

        _ctx = set(raw_locals) - g_ext_ctx
        g_with_parent_ctx_stack.append(_ctx)

        # local parent context recorded, add current locals to ext context
        g_ext_ctx |= _ctx

        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        global g_with_parent_ctx_stack, g_ext_ctx

        raw_stack = inspect.stack()
        raw_locals = previous_locals(raw_stack)

        # get owner instace, if any
        owner_instance = raw_locals.get('self', None)

        # different between previous local stack at enter and exit
        new_exit_key = {key for key in set(raw_locals.keys()) - g_ext_ctx if not raw_locals[key] == self}

        # last stack -> frame -> internals -> f_code -> co_names
        co_varnames = raw_stack[1][0].f_code.co_varnames
        # print(self, co_varnames)

        # rearrange new keys according to last stack's co_varname order
        reordered_new_exit_key = [co_vn for co_vn in co_varnames if co_vn in new_exit_key]

        for n in reordered_new_exit_key:
            if n == 'self':
                continue

            if owner_instance:
                # add to parent object if context is an object
                # print("self in prev locals, add to self", self, n, owner_instance, raw_locals)
                setattr(owner_instance, n, raw_locals[n])

            item = raw_locals[n]
            # add to layout
            self._auto_add(item)

        # should corresponds to correct enter-exit pair since it behave like stacks..
        parent_ctx = g_with_parent_ctx_stack.pop()
        # make parent context active again (for parent layout to use)
        g_ext_ctx = g_ext_ctx - parent_ctx
        # push processed keys out of scope
        g_ext_ctx |= new_exit_key
        # g_oi_ext_ctx |= new_oi_exit_key

        # reset ext context on last exit
        if not g_with_parent_ctx_stack:
            # print("resetting ext context")
            g_ext_ctx = set()

            # add root layout in last stack
            if owner_instance:
                for key in raw_locals:
                    if raw_locals[key] == self:
                        setattr(owner_instance, key, self)


class AutoScopedQVBoxLayout(QtWidgets.QVBoxLayout, AutoScopedLayoutMixin):
    pass


class AutoScopedQHBoxLayout(QtWidgets.QHBoxLayout, AutoScopedLayoutMixin):
    pass


MAX_STRETCH_SIZE_POLICY = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding)
def button(text):
    return QtWidgets.QPushButton(sizePolicy=MAX_STRETCH_SIZE_POLICY, text=text)


class _AutoScopedLayoutDemo(object):
    def __init__(self):
        self.qapp = QtWidgets.QApplication(sys.argv)

        self.win = QtWidgets.QDialog(
            # parent=maya_window_kudasai(),
            windowTitle="Example!",
            objectName="exampleLayoutDialog"
        )

        ####

        with AutoScopedQVBoxLayout(self.win) as root_layout:
            # btn1 = button("One")
            btn1 = button("One")
            btn2 = button("Two")
            with AutoScopedQVBoxLayout(self.win) as layout2:
                s1_btn1 = button("sub1V1")
                with AutoScopedQHBoxLayout(self.win) as sub2hlayout:
                    s2_btn1 = button("sub2H1")
                    s2_btn2 = button("sub2H2")
            btn3 = button("Three")
            with AutoScopedQHBoxLayout(self.win) as sub3hlayout:
                s3_btn1 = button("sub3H1")
                s3_btn2 = button("sub3H2")
                with AutoScopedQVBoxLayout(self.win) as vlayout3:
                    s4_btn1 = button("sub4H1")
                    s4_btn2 = button("sub4H2")
            btn4 = button("Four")

    def show(self):
        self.win.show()
        sys.exit(self.qapp.exec_())

demo = _AutoScopedLayoutDemo()
demo.show()
Final result
pyside2 with statement final result

Final result

ローカル変数で宣言したオブジェクトが__dict__に追加されたかの確認
pprint output of self.__dict__ after self.win.show():

{'btn1': <PySide2.QtWidgets.QPushButton object at 0x0000022B0CB35688>,
 'btn2': <PySide2.QtWidgets.QPushButton object at 0x0000022B0CB35EC8>,
 'btn3': <PySide2.QtWidgets.QPushButton object at 0x0000022B0CB359C8>,
 'btn4': <PySide2.QtWidgets.QPushButton object at 0x0000022B0CB3A1C8>,
 'layout2': <sc_mayapyqtexample.AutoScopedQVBoxLayout object at 0x0000022B0CB35748>,
 'qapp': <PySide2.QtWidgets.QApplication object at 0x0000022B0CB315C8>,
 'root_layout': <sc_mayapyqtexample.AutoScopedQVBoxLayout object at 0x0000022B0CB31688>,
 's1_btn1': <PySide2.QtWidgets.QPushButton object at 0x0000022B0CB35808>,
 's2_btn1': <PySide2.QtWidgets.QPushButton object at 0x0000022B0CB35708>,
 's2_btn2': <PySide2.QtWidgets.QPushButton object at 0x0000022B0CB35948>,
 's3_btn1': <PySide2.QtWidgets.QPushButton object at 0x0000022B0CB35B08>,
 's3_btn2': <PySide2.QtWidgets.QPushButton object at 0x0000022B0CB35CC8>,
 's4_btn1': <PySide2.QtWidgets.QPushButton object at 0x0000022B0CB35C48>,
 's4_btn2': <PySide2.QtWidgets.QPushButton object at 0x0000022B0CB35D48>,
 'sub2hlayout': <sc_mayapyqtexample.AutoScopedQHBoxLayout object at 0x0000022B0CB35908>,
 'sub3hlayout': <sc_mayapyqtexample.AutoScopedQHBoxLayout object at 0x0000022B0CB35A88>,
 'vlayout3': <sc_mayapyqtexample.AutoScopedQVBoxLayout object at 0x0000022B0CB35BC8>,
 'win': <PySide2.QtWidgets.QDialog object at 0x0000022B0CB31608>}

以上でWith文でもQtのUIを構築ことが可能の例を挙げました!

(しかし上記のコードを本気で使うことはちょっとおススメできませんが、、)

Tags: