Multiparmのあれこれ [移動、コピー、複製、リファレンス]

# はじめに
この記事は[Houdini Advent Calender 2024](https://qiita.com/advent-calendar/2024/houdini)の6日目の記事です

Houdini 20.5から新しく追加されたHOMの関数でMultiparmの扱いが便利になったので
今回はそれらを使って、<strong>いい感じ！</strong> なMultiparm Blockを作っていきたいと思います！

使用バージョン : Houdini 20.5.332

[MultiparmBlock 移動編](#multiparmblock-移動編)
![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/233156/b5fa57dc-6c81-acc3-748a-e0f9fe013ce6.gif)

[MultiparmBlock 入替え編](#multiparmblock-入替え編)
![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/233156/2e4bc3ce-2c40-a667-a08c-f954b337ada1.gif)

[MultiparmBlock コピー・複製編](#multiparmblock-コピー複製編)
![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/233156/e3000749-e1b8-3dc4-147e-34dbb96b850b.gif)

[MultiparmBlock リファレンス編（番外）](#multiparmblock-リファレンス編番外)
![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/233156/6a091e00-5c5b-724c-8b87-bbbefc235e67.gif)

# 関数
```python:20.0.270〜
hou.Parm.moveMultiParmInstances(moves)
hou.Parm.moveDownMultiParmInstance(inst_index)
hou.Parm.moveUpMultiParmInstance(inst_index)
```
20.0.270から追加された関数でMultiparmの移動ができるようになっています
単純な移動であればこちらの関数を使うだけでBlockの移動ができますね

:::note warn
moveMultiParmInstancesに関しては正しく引数を与えても例外が発生するというバグがありましたが、20.5.340で修正されたみたいです
![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/233156/183f3872-f6e8-8731-8db7-810d218f00d0.png)
</details>
:::

```python:20.5〜
hou.Parm.multiParmInstancesAsData()
hou.Parm.setMultiParmInstancesFromData(data)
```
そして、[Recipes](https://www.sidefx.com/docs/houdini/network/recipes.html)の追加に伴って、20.5からDataを扱う関数が増えました！
今回はMultiparmに関するものだけを挙げていますが、他にもあるので[Parm](https://www.sidefx.com/docs/houdini/hom/hou/Parm.html#data_methods)もご確認下さい

***

hou.Parm.<strong>multiParmInstancesAsData()</strong>の関数を実行すると、
各インスタンスの情報を含んだ<strong>リスト</strong>が返ってきます。

リストの各要素は辞書で構成されていて、
基本的には<strong>キーにパラメータの名前</strong>、<strong>値にパラメータの値</strong>が含まれます
```python:multiParmInstancesAsData()
# return: [{<parm_name>_#: <parm_value>,...},
#          {<parm_name>_#: <parm_value>,...}...]

# e.g)
#  単一の値
[{'parm1_#': '0', 'parm2_#': '2'},
 {'parm1_#': '5', 'parm2_#': '3'}]

#  複数の値
[{'parm1_#': ['0','1','2'], 'parm2_#': ['1','2','3']},
 {'parm1_#': ['3','4','5'], 'parm2_#': ['2','3','1']}]

# エクスプレッション
[{'parm1_#': {'expression': 'ch("ref_parm")'}},
 {'parm1_#': {'expression': 'hou.pwd().evalParm("ref_parm")', 'language': 'Python'}}]

#  キーフレーム
[{'parm1_#':
  {'keyframes': [ {'frame': 1, 'value': 1.0, 'slope': 0.0,
                   'accel': 0.0, 'inaccel': 0.3, 'expression': 'bezier()'},
                  {'frame': 2, 'value': 0.5, 'slope': 0.0,
                   'accel': 0.0, 'inaccel': 0.3, 'expression': 'bezier()'}
                ]
  }
}]
```

# MultiparmBlock 移動編
![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/233156/b5fa57dc-6c81-acc3-748a-e0f9fe013ce6.gif)
それではこちらを作っていきたいと思います！

まずは<strong>パラメータのテンプレートを作成</strong>していきましょう
Edit Parameter Interaceからこのようなパラメータを作成しました
![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/233156/328e9316-fd41-2a83-d1bf-5170cd85913f.gif)
Folder TypeはMultiparm Block (list)
Button IconにそれぞれBUTTONS_up、BUTTONS_downを設定しています
***
![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/233156/55d3ee1b-4e6d-ef40-3fba-33d7d806e043.gif)
もちろん<strong>このままでは何も動かない</strong>ので、
矢印のボタンを押した時に移動するように<strong>コールバックを書いていきます</strong>
***
![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/233156/a84ae593-a5b9-8981-3e64-48d72979e647.jpeg)
直接だと書きにくいので、自分は別途エディタなどで書いてから貼り付けています
HDAや別途パラメータで関数を用意して、それを評価する方法もありますが今回はシンプルに

```python:up_#
parent = kwargs["parm"].parentMultiParm();
idx    = int(kwargs["script_multiparm_index"]) - parent.multiParmStartOffset();
n      = parent.eval();
parent.moveDownMultiParmInstance(idx) if (n > 1) and (idx > 0) else None
```

```python:down_#
parent = kwargs["parm"].parentMultiParm();
idx    = int(kwargs["script_multiparm_index"]) - parent.multiParmStartOffset();
n      = parent.eval();
parent.moveUpMultiParmInstance(idx) if (n > 1) and (idx < n-1) else None
```

:::note
Callback Scriptではスクリプトを一行にまとめないといけないため、
行の末尾に <strong>;</strong> をつけて文の区切りとして使用しています
:::

では、スクリプトで何をしているか見ていきましょう！
```python:
# はじめに自身の属しているMultiparmを取得します
parent = kwargs["parm"].parentMultiParm();

# インデックスを取得
# ただ、そのままだとパラメータのインデックスがオフセットされている場合があるので、
# 0から開始されるように調整
idx = int(kwargs["script_multiparm_index"]) - parent.multiParmStartOffset();

# インスタンスの総数を取得
n = parent.eval();

# 三項演算子を使って、条件に一致する場合のみ関数を実行
# up_#:   インスタンスの数が2つ以上かつ、インデックスの値が最下位ではない場合のみ上に移動
parent.moveDownMultiParmInstance(idx) if (n > 1) and (idx > 0)   else None
# down_#: インスタンスの数が2つ以上かつ、インデックスの値が最上位ではない場合のみ下に移動
parent.moveUpMultiParmInstance(idx)   if (n > 1) and (idx < n-1) else None
```
<details><summary>三項演算子</summary>

:::note
```python
x if condition else y
```
この書き方は三項演算子と呼ばれ、if-else 文を1行で書くことができます
- condition は評価される条件式
- x は、condition が 真（True）であれば選ばれる値
- y は、condition が 偽（False）であれば選ばれる値
:::
</details>

up_#、down_#それぞれの<strong>Callback Scriptにスクリプトを設定</strong>し、Acceptを押したら
<strong>移動ボタンの付いたMultiparmの完成</strong>です！

# MultiparmBlock 入替え編
先程のMultiparmだと、繰り上げ、繰り下げしかできないので
もう少し拡張して、<strong>任意の場所と入替えられるように</strong>して行きましょう

![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/233156/2e4bc3ce-2c40-a667-a08c-f954b337ada1.gif)

## QMenuでメニューを作ろう
早速、準備していくのですが、
どのMultiparmと入替えるのかを選択できるようにしたいので、

<strong>Qtを使用してメニューを作成</strong>していきます
![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/233156/a5dd6d2c-43f5-9ad8-0b99-7bfd636072e4.jpeg)

### Action Button
先程はButtonパラメータのCallback Scriptでスクリプトを書いていきましたが
一行では何かと難しいので、今回はAction Buttonを使って実装していきたいと思います。

<strong>Edit Parameter Interface</strong>を見てみると、パラメーターのタブの中に<strong>Action Button</strong>というタブがあります。
ここに<strong>Pythonスクリプトを書き込むと</strong>パラメーターの横に<strong>アイコンが表示される</strong>ので、それをクリックすると書き込んだ<strong>スクリプトを実行する</strong>事が出来ます。

<img alt="pitch" src='https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/233156/f806033d-9938-1975-5812-dfcebdbf94a8.png'>

### シンプルなメニュー
Floatなど適当なパラメータを用意して、Action Buttonに以下を書き込みます
```python:Action Button
from hutil.Qt.QtGui     import QCursor
from hutil.Qt.QtWidgets import QMenu, QAction
# NOTE: QActionはQt6からQtGuiに属するので、今後修正が必要になるかも

def test():
    hou.ui.displayMessage("Hello!")

menu    = QMenu("Hoge")                   # メニューの作成
submenu = QMenu("Fuga",   parent=menu)    # サブメニューの作成
action  = QAction("Fugo", parent=submenu) # アクションの作成

action.triggered.connect(test) # アクションをクリックした時の関数を登録

submenu.addAction(action) # アクションの追加
menu.addMenu(submenu)     # サブメニューの追加

# UIの見え方をHoudini仕様にするために、スタイルシートを設定
menu.setStyleSheet(hou.qt.styleSheet())

# マウスカーソルの位置を取得
pos = QCursor().pos()

# 実行
menu.exec_(pos)
```
これを実行すると
![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/233156/22f80966-80ec-bad3-4b35-a99a4fe6e498.gif)
簡単なメニューが作れました
それではこれらを踏まえてMultiparmを拡張していきましょう！

## 実装
![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/233156/782417f2-d519-e81b-0f86-d1363ccc0a3d.png)
[移動編](#multiparmblock-移動編)でのパラメーターにDataパラメータを追加しました。
こちらのパラメータタイプは本来の使い方ではないと思うのですが
Action Buttonだけを使いたかったので、こちらを使用しています
※これにより処理が増えてしまいましたっ...
[パラメータタイプ](https://www.sidefx.com/ja/docs/houdini/ref/windows/optype.html#parmtypes)


```python:Action Button
from hutil.Qt.QtGui     import QCursor
from hutil.Qt.QtWidgets import QMenu, QAction
# NOTE: QActionはQt6からQtGuiに属するので、今後修正が必要になるかも

# 引数を含んだ関数を与えたいのでpartialをimport
from functools import partial

# geometryが空の状態だとhou.Parm.setMultiParmInstancesFromDataを実行した時に
# エラーを引き起こすので、それを回避するための関数
def checkNoneGeometry(data):
    for d in data:
        for k in d.keys():
            if isinstance(d[k], dict) and ("geometry" in d[k]) and (d[k]["geometry"] is None):
                d[k].pop('geometry')
    return data

# Multiparmを入れ替えるための関数
def swapBlock(multiparm, idx, target_idx):
    # Multiparmのデータを取得
    data = multiparm.multiParmInstancesAsData()
    # geometryにNoneが含まれていないか確認
    checkNoneGeometry(data)
    # 順番の入替え
    data[idx],data[target_idx] = data[target_idx],data[idx]
    # Multiparmのセット
    multiparm.setMultiParmInstancesFromData(data)

# パラメータの情報を取得
parm      = kwargs["parmtuple"][0]
multiparm = parm.parentMultiParm()
offset    = multiparm.multiParmStartOffset()
idx       = int(kwargs["script_multiparm_index"]) - offset
n         = multiparm.eval()

# メインメニューの作成
menu = QMenu()

# サブメニューの作成
submenu = QMenu("Swap",                                 # Label
                icon   = hou.qt.Icon("SOP_attribswap"), # Icon
                parent = menu)                          # Parent
for i in range(n):
    # クリックしたインデックスの場合はスキップ
    if i == idx:
        continue
    # アクションの作成
    action = QAction(hou.qt.Icon("SOP_enumerate"),                # Icon
                     "%s_%d"%(multiparm.description(), i+offset), # Label
                     parent=submenu)                              # Parent
    # アクションをクリックした時の関数を登録
    # NOTE: lambdaを使う事もできますが、イテレーションの中で設定すると
    #       iなどの値が先にキャプションされてしまい意図しない結果となるので
    #       functoolsのpartialを使用
    action.triggered.connect(partial(swapBlock,multiparm,idx,i))
    # サブメニューにアクションを追加
    submenu.addAction(action)

# サブメニューをメニューに追加
menu.addMenu(submenu)

# UIの見え方をHoudini仕様にするために、スタイルシートを設定
menu.setStyleSheet(hou.qt.styleSheet())

# マウスカーソルの位置を取得
pos = QCursor().pos()

# 実行
menu.exec_(pos)
```
<details close><summary>checkNoneGeometry()</summary>

:::note
<strong>Data、Geometry Dataタイプのパラメータ</strong>は、ジオメトリを格納できるのですが、
その値が<strong>空の場合</strong>、hou.Parm.<strong>setMultiParmInstancesFromData</strong>で<strong>エラーを引き起こす</strong>ため、それを回避するために処理をいれています
:::
</details>

今回アイコンも追加しています、詳しくはこちらを [hou.qt.Icon](https://www.sidefx.com/ja/docs/houdini/hom/hou/qt/Icon.html)

これを適用すると<strong>入替えメニューの付いたMultiparmの完成</strong>です！

# MultiparmBlock コピー・複製編
さぁどんどん、いきましょう
次はコピー、複製できるように拡張していきます

![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/233156/e3000749-e1b8-3dc4-147e-34dbb96b850b.gif)

[入替え編](#実装)のコードを少し調整します

```python:Action Button
from hutil.Qt.QtGui     import QCursor
from hutil.Qt.QtWidgets import QMenu, QAction
# NOTE: QActionはQt6からQtGuiに属するので、今後修正が必要になるかも

# 引数を含んだ関数を与えたいのでpartialをimport
from functools import partial

# geometryが空の状態だとhou.Parm.setMultiParmInstancesFromDataを実行した時に
# エラーを引き起こすので、それを回避するための関数
def checkNoneGeometry(data):
    for d in data:
        for k in d.keys():
            if isinstance(d[k], dict) and ("geometry" in d[k]) and (d[k]["geometry"] is None):
                d[k].pop('geometry')
    return data

# Multiparmを操作するための関数
def processBlock(multiparm, idx, target_idx, method):
    # Multiparmのデータを取得
    data = multiparm.multiParmInstancesAsData()
    # geometryにNoneが含まれていないか確認
    checkNoneGeometry(data)
    # 順番の入替え
    if method == "swap":
        data[idx],data[target_idx] = data[target_idx],data[idx]
    # コピー
    elif method == "copy":
        data[idx] = data[target_idx]
    # 複製
    elif method == "dup":
        data.insert(target_idx, data[idx])
    # Multiparmのセット
    multiparm.setMultiParmInstancesFromData(data)

# パラメータの情報を取得
parm      = kwargs["parmtuple"][0]
multiparm = parm.parentMultiParm()
offset    = multiparm.multiParmStartOffset()
idx       = int(kwargs["script_multiparm_index"]) - offset
n         = multiparm.eval()

# メインメニューの作成
menu = QMenu()

# サブメニューのリストを作成
submenu_list = []
submenu_list.append(QMenu("Swap",              icon=hou.qt.createIcon("SOP_attribswap"), parent=menu, objectName="swap"))
submenu_list.append(QMenu("Copy\tfrom",        icon=hou.qt.createIcon("BUTTONS_copy"),   parent=menu, objectName="copy"))
submenu_list.append(QMenu("Duplicate\tbefore", icon=hou.qt.createIcon("SOP_duplicate"),  parent=menu, objectName="dup"))
for submenu in submenu_list:
    method = submenu.objectName()
    for i in range(n):
        # 複製以外で、クリックしたインデックスの場合はスキップ
        if (i == idx) and (method != "dup"):
            continue
        # アクションの作成
        action = QAction(hou.qt.createIcon("SOP_enumerate"),          # Icon
                         "%s_%d"%(multiparm.description(), i+offset), # Label
                         parent=submenu)                              # Parent
        # アクションをクリックした時の関数を登録
        # NOTE: lambdaを使う事もできますが、イテレーションの中で設定すると
        #       iなどの値が先にキャプションされてしまい意図しない結果となるので
        #       functoolsのpartialを使用
        action.triggered.connect(partial(processBlock,multiparm,idx,i,method))
        # サブメニューにアクションを追加
        submenu.addAction(action)
    # サブメニューをメニューに追加
    menu.addMenu(submenu)

# UIの見え方をHoudini仕様にするために、スタイルシートを設定
menu.setStyleSheet(hou.qt.styleSheet())

# マウスカーソルの位置を取得
pos = QCursor().pos()

# 実行
menu.exec_(pos)

```
サブメニューの追加と、それらを処理する関数に条件をつけています
少し調整を加えるだけで実装できました！

<strong>コピー・複製機能の付いたMultiparmの完成です！</strong>~~雑~~

# MultiparmBlock リファレンス編（番外）
![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/233156/6a091e00-5c5b-724c-8b87-bbbefc235e67.gif)
こちらなんですが、リファレンスになるとリストの編集だけではなく、
データの中身を少し書き換える必要があるのと、ネストされたMultiparmやRampなどの処理が複雑になるため、詳しい解説は省略します

コードは貼り付けておくので、ご興味あればご覧ください！

<details close><summary>Script</summary>

```python:Action Button
from hutil.Qt.QtGui     import QCursor
from hutil.Qt.QtWidgets import QMenu, QAction
# NOTE: QActionはQt6からQtGuiに属するので、今後修正が必要になるかも

import re

# 引数を含んだ関数を与えたいのでpartialをimport
from functools import partial
# 辞書を子階層までコピーしたいのでdeepcopyをimport
from copy      import deepcopy

# geometryが空の状態だとhou.Parm.setMultiParmInstancesFromDataを実行した時に
# エラーを引き起こすので、それを回避するための関数
def checkNoneGeometry(data):
    for d in data:
        for k in d.keys():
            if isinstance(d[k], dict) and ("geometry" in d[k]) and (d[k]["geometry"] is None):
                d[k].pop('geometry')
    return data
    
# Rampのリンクが残っている場合があるのでそれらを解除
def unlinkMultiparm(node):
    path  = node.path()
    links = re.findall("'(.*?)'",hou.hscript("opmultiparm %s"%(path))[0])
    [hou.hscript("opmultiparm '%s' '%s' ''"%(path,link)) for link in links]

# 再帰的にdataをリファレンスへと書き換え
def toReferenceData(node, data, target_idx, offset, indices=[], depth=0):
    for i,parm_set in enumerate(data):
        if depth == 0 and target_idx != i:
            continue
        current_indices = indices[:] + [i+offset,]
        
        for parm_name,parm_value in parm_set.items():
            resolved_parm_name = parm_name.replace("#","{}").format(*current_indices)
            expr     = 'ch("%s")'%(resolved_parm_name)
            expr_str = 'chs("%s")'%(resolved_parm_name)
            
            # Get Template
            template_group = node.parmTemplateGroup()
            template = template_group.find(parm_name)
            if template:
                type_    = template.type()
                datatype = template.dataType()
                
                # Ramp
                if datatype == hou.parmData.Ramp:
                    ramp_offset = int(template.tags().get('multistartoffset',1))
                    parm_set[parm_name] = {'value' : {'expression': expr}, 'points' : parm_set[parm_name]}
                    for point_idx,point in enumerate(parm_set[parm_name]['points']):
                        for point_name, point_value in point.items():
                            if isinstance(point_value, list):
                                point[point_name] = [{'expression': 'ch("%s_%d%s")'%(resolved_parm_name,point_idx+ramp_offset,channel)}
                                                      for channel in ["cr","cg","cb"]]
                            else:
                                point[point_name] =  {'expression': 'ch("%s_%d%s")'%(resolved_parm_name,point_idx+ramp_offset,point_name)}
                                
                # MultiparmBlock       
                elif type_ in (hou.parmTemplateType.Folder, hou.parmTemplateType.FolderSet):
                    if template.folderType() in (
                        hou.folderType.MultiparmBlock,
                        hou.folderType.ScrollingMultiparmBlock,
                        hou.folderType.TabbedMultiparmBlock
                    ):
                        offset = int(template.tags().get('multistartoffset',1))
                        if isinstance(parm_set[parm_name], list):
                            parm_set[parm_name] = {
                                'multiparm_links': {},
                                'multiparms'     : parm_set[parm_name][:],
                                'value'          : {'expression': expr}
                            }
                            parm_value = parm_set[parm_name]['multiparms']
                        elif isinstance(parm_set[parm_name], dict):
                            parm_value = parm_set[parm_name].get('multiparms')
                            parm_set[parm_name]['value'] = {'expression': expr}
                        else:
                            parm_set[parm_name] = {'expression': expr}
                            
                # String / Float
                else:
                    expr = expr_str if datatype == hou.parmData.String else expr
                    parm_set[parm_name] = {'expression': expr}
                    
                # List
                if isinstance(parm_value, list):
                    toReferenceData(node, parm_value, target_idx, offset, current_indices, depth+1)
    if depth == 0:
        return data[target_idx]

# Multiparmを入れ替えるための関数
def processBlock(multiparm, idx, target_idx, method):
    # Multiparmのデータを取得
    data = multiparm.multiParmInstancesAsData()
    # geometryにNoneが含まれていないか確認
    checkNoneGeometry(data)
    # 順番の入替え
    if method == "swap":
        data[idx],data[target_idx] = data[target_idx],data[idx]
    # コピー
    elif method == "copy":
        data[idx] = data[target_idx]
    # 複製
    elif method == "dup":
        data.insert(target_idx, data[idx])
    # リファレンス
    elif method == "ref":
        # リンクを解除
        unlinkMultiparm(multiparm.node())
        # 一度パラメータをリセット
        multiparm.removeMultiParmInstance(idx)
        multiparm.insertMultiParmInstance(idx)
        # リファレンスに書き換え
        data[idx] = toReferenceData(multiparm.node(),
                                    deepcopy(data),
                                    target_idx,
                                    multiparm.multiParmStartOffset())
    # Multiparmのセット
    multiparm.setMultiParmInstancesFromData(data)

# パラメータの情報を取得
parm      = kwargs["parmtuple"][0]
multiparm = parm.parentMultiParm()
offset    = multiparm.multiParmStartOffset()
idx       = int(kwargs["script_multiparm_index"]) - offset
n         = multiparm.eval()

# メインメニューの作成
menu = QMenu()

# サブメニューのリストを作成
submenu_list = []
submenu_list.append(QMenu("Swap",              icon=hou.qt.createIcon("SOP_attribswap"), parent=menu, objectName="swap"))
submenu_list.append(QMenu("Copy\tfrom",        icon=hou.qt.createIcon("BUTTONS_copy"),   parent=menu, objectName="copy"))
submenu_list.append(QMenu("Duplicate\tbefore", icon=hou.qt.createIcon("SOP_duplicate"),  parent=menu, objectName="dup"))
submenu_list.append(QMenu("Reference\tfrom",   icon=hou.qt.createIcon("LOP_reference"),  parent=menu, objectName="ref"))
for submenu in submenu_list:
    method = submenu.objectName()
    for i in range(n):
        # 複製以外で、クリックしたインデックスの場合はスキップ
        if (i == idx) and method != "dup":
            continue
        # アクションの作成
        action = QAction(hou.qt.createIcon("SOP_enumerate"),          # Icon
                         "%s_%d"%(multiparm.description(), i+offset), # Label
                         parent=submenu)                              # Parent
        # アクションをクリックした時の関数を登録
        # NOTE: lambdaを使う事もできますが、イテレーションの中で設定すると
        #       iなどの値が先にキャプションされてしまい意図しない結果となるので
        #       functoolsのpartialを使用
        action.triggered.connect(partial(processBlock,multiparm,idx,i,method))
        # サブメニューにアクションを追加
        submenu.addAction(action)
    # サブメニューをメニューに追加
    menu.addMenu(submenu)

# UIの見え方をHoudini仕様にするために、スタイルシートを設定
menu.setStyleSheet(hou.qt.styleSheet())

# マウスカーソルの位置を取得
pos = QCursor().pos()

# 実行
menu.exec_(pos)
```

</details>

# おわりに
新しく追加された関数で、Multiparmを簡単に扱うことができました
今までは、複製するのにも結構大変だったので個人的には嬉しいところです

また、QMenuを使ったカスタムのメニューもさらに書き加えていけば、
独自のWidgetを表示したりと出来ることが増えるので是非色々と試してみて下さい！

ありがとうございました！