跳转至

maya工具集(new)

** 界面数据化 ** 用 YAML 配置界面,Python 只负责读取并渲染。

这样做有三个巨大的好处:

  1. 零代码加工具:美术或非技术人员想加个新按钮,只需要改改文本,完全不用动 Python 代码。
  2. 极易维护:UI 的排列组合一目了然,不用在密密麻麻的 PySide 代码里找布局。
  3. 动态更新:甚至可以把 YAML 放在服务器上,实现全团队工具箱的远程实时更新。

引入 Python 原生的 yaml 库,并把 Maya 的执行命令直接作为字符串写进配置里。


📂 完美的项目文件结构

建立如下的文件夹结构:

my_maya_pipeline/
│
├── config.yml            # 存放工具按钮和分类的配置文件
└── my_pipeline_gui.py    # 读取配置并生成 Maya 停靠面板的 Python 主脚本

📝 1. 配置文件:config.yml

这里定义了左侧的 Tab 分类、右侧按钮的名称,以及点击按钮时真正触发的 Maya Python 命令。

# my_maya_pipeline/config.yml
# Maya 动态工具箱配置文件

tabs:
  - name: "通用"
    label: "通用工具"
    tools:
      - label: "🧼 一键优化场景大小"
        command: "import maya.mel as mel; mel.eval('cleanUpScene(1);')"
      - label: "📂 打开当前工程目录"
        command: "import maya.cmds as cmds; import os; os.startfile(cmds.workspace(q=True, rootDirectory=True))"

  - name: "模型"
    label: "模型常用操作"
    tools:
      - label: "🧊 冻结变换 + 删除历史"
        command: "import maya.cmds as cmds; sel = cmds.ls(sl=True); cmds.makeIdentity(sel, apply=True, t=1, r=1, s=1); cmds.delete(sel, ch=True)"
      - label: "📐 重置轴心到中心"
        command: "import maya.cmds as cmds; cmds.xform(cp=True)"

🐍 2. 主脚本:my_pipeline_gui.py

在这里,我们引入 yaml 模块。核心逻辑是用 exec() 函数去动态执行配置文件里写好的 command 字符串。

⚠️ 注意:Maya 自带的 Python 环境可能没有预装 pyyaml。在运行此脚本前,需要在 Maya 的 Output Window 或脚本编辑器中运行 pip 安装: import os; import sys; os.system(f'"{sys.executable}" -m pip install pyyaml')

# -*- coding: utf-8 -*-
"""
核心渲染逻辑:读取 config.yml 并自动生成 Maya 停靠界面
"""
import os
import yaml
import maya.cmds as cmds
from maya.app.general.mayaMixin import MayaQWidgetDockableMixin

try:
    from PySide2 import QtWidgets, QtCore
except ImportError:
    from PySide6 import QtWidgets, QtCore

class MyYamlPipelineToolbar(MayaQWidgetDockableMixin, QtWidgets.QWidget):
    def __init__(self, config_path, parent=None):
        super(MyYamlPipelineToolbar, self).__init__(parent=parent)

        self.setObjectName("MyYamlToolbar") 
        self.setWindowTitle("YAML 配置工具箱")

        # 加载 YAML 配置数据
        self.config_data = self.load_config(config_path)

        # 主布局
        self.main_layout = QtWidgets.QHBoxLayout(self)
        self.main_layout.setContentsMargins(2, 2, 2, 2)
        self.main_layout.setSpacing(2)

        # 1. 左侧垂直 Tab
        self.tab_bar = QtWidgets.QTabBar()
        if hasattr(QtWidgets.QTabBar, "Shape"):
            self.tab_bar.setShape(QtWidgets.QTabBar.Shape.RoundedWest)
        else:
            self.tab_bar.setShape(QtWidgets.QTabBar.RoundedWest)

        self.tab_bar.currentChanged.connect(self.stack.setCurrentIndex)
        self.main_layout.addWidget(self.tab_bar)

        # 2. 右侧内容层叠窗口
        self.stack = QtWidgets.QStackedWidget()
        self.main_layout.addWidget(self.stack)

        # 3. 根据 YAML 数据动态解析并构建 UI
        if self.config_data and "tabs" in self.config_data:
            self.build_ui_from_config()

    def load_config(self, path):
        """安全读取 YAML 配置文件"""
        if not os.path.exists(path):
            cmds.warning(f"找不到配置文件: {path}")
            return None
        with open(path, 'r', encoding='utf-8') as f:
            return yaml.safe_load(f)

    def build_ui_from_config(self):
        """核心解析函数:遍历 YAML 自动生成按钮"""
        for tab_info in self.config_data["tabs"]:
            # 添加左侧标签
            self.tab_bar.addTab(tab_info.get("name", "未命名"))

            # 创建右侧对应的页面
            page = QtWidgets.QWidget()
            page_layout = QtWidgets.QVBoxLayout(page)
            page_layout.setContentsMargins(6, 6, 6, 6)
            page_layout.setSpacing(4)

            # 页面标题
            title_label = QtWidgets.QLabel(f"<b>{tab_info.get('label', '')}</b>")
            page_layout.addWidget(title_label)

            # 动态循环生成按钮
            for tool in tab_info.get("tools", []):
                btn = QtWidgets.QPushButton(tool.get("label", "未知按钮"))
                btn.setMinimumHeight(30)

                # 获取该按钮对应的 Python 执行命令
                cmd_str = tool.get("command", "")

                # 关键:使用 Python 的 lambda 和 exec 动态绑定命令
                # 用 default 参数绑定当前的 cmd_str,防止闭包导致的变量污染
                btn.clicked.connect(lambda checked=False, c=cmd_str: exec(c, globals()))

                page_layout.addWidget(btn)

            page_layout.addStretch()
            self.stack.addWidget(page)

def show_toolbar():
    object_name = "MyYamlToolbar"
    workspace_control_name = object_name + "WorkspaceControl"

    # 获取当前脚本所在目录下的 config.yml
    current_dir = os.path.dirname(__file__) if "__file__" in locals() else "D:/my_maya_pipeline" # 备用绝对路径
    config_path = os.path.join(current_dir, "config.yml")

    # 1. 清理旧 UI
    if cmds.workspaceControl(workspace_control_name, exists=True):
        cmds.workspaceControl(workspace_control_name, edit=True, close=True)
        cmds.deleteUI(workspace_control_name, control=True)

    # 2. 生成实例
    global my_yaml_tool_instance
    my_yaml_tool_instance = MyYamlPipelineToolbar(config_path)

    # 3. 停靠模式显示
    my_yaml_tool_instance.show(dockable=True, area='right', floating=False)

    # 4. 吸附到右侧标签组
    target_controls = ["AttributeEditor", "ChannelBoxLayerEditor"]
    for target in target_controls:
        if cmds.workspaceControl(target, exists=True):
            cmds.workspaceControl(workspace_control_name, edit=True, tabToControl=(target, -1))
            break

    cmds.workspaceControl(workspace_control_name, edit=True, raise_=True)

if __name__ == "__main__":
    show_toolbar()

🎯 技术要点

btn.clicked.connect(lambda checked=False, c=cmd_str: exec(c, globals()))

💡 为什么要这样写? 1. exec(c, globals()):YAML 里的命令是字符串,Python 的 exec() 可以直接把字符串当做代码来执行,完美解决了解耦问题。 2. c=cmd_str:这是为了解决 Python 循环中的闭包(Closure)陷阱。如果不写 c=cmd_str,所有按钮被点击时,都只会执行循环里最后一个工具的命令。通过把当前循环的字符串作为默认参数传给 lambda,就能把命令死死地“锁”在各自的按钮上!