# 项目目标 我想做一个Maya的Metahuman自定义的插件, 语言:基于Python Maya版本:2022, 2023, 2024, 2025 ## 项目描述 本项目是一个Maya插件,主要功能是提供与MetaHuman相同拓扑的模型或者自定义的3d模型以来完成自定义绑定,编辑DNA,校准骨骼位置,保存DNA,载入DNA,导出fbx,保存DNA文件, 编辑BlendShape,等功能。 ## 这个插件主要功能: 提供与MetaHuman相同拓扑的模型或者自定义的3d模型以来完成自定义绑定,编辑DNA,校准骨骼位置,保存DNA,载入DNA,导出fbx,保存DNA文件, 编辑BlendShape,等功能。 ## 注意Reference路径不参与参见功能实现,只作为参考。Reference只作为参考,可以从中拷贝必要的文件到当前项目中 ## 代码实现: 根据Maya和Python版本来获取plugin的路径,并尽可能使用PySide编写UI,要保证PySide的通用性,使用单独的ccs文件来定义定义样式。 根据Maya不同的版本来定义PySide的UI定义,版本通用性参考MSLiveLink。 ## 参考代码: DNA\_Calibration中主要参考DNA编辑等功能更,SuperRigigng主要参考UI样式并获取对应的功能的实现逻辑,MSLiveLink主要参开DNA编辑和文件处理方式。 ## 产品功能对标: DNA Calibration Document : https://epicgames.github.io/MetaHuman-DNA-Calibration/index.html MetaHuman-DNA-Calibration 代码:https://github.com/EpicGames/MetaHuman-DNA-Calibration SuperRigging:https://docs.pointart.net/ AnimCraft:https://geekdaxue.co/read/animcraft@cn/ ## 代码初始结构: . ├── config │ └── \_\_init\_\_.py │ └── data.py ├── plugins │ └── Linux │ │ ├── 2022 │ │ │ ├── \_py3dnacalib.so │ │ │ ├── dnacalib.py │ │ │ ├── libdnacalib.so.6 │ │ │ ├── libembeddedRL4.so │ │ │ ├── libembeddedRL4.so.8 │ │ │ ├── libembeddedRL4.so.8.0.8 │ │ │ ├── MayaUE4RBFPlugin2022.mll │ │ │ ├── MayaUERBFPlugin.mll │ │ ├── 2023 │ │ │ ├── \_py3dnacalib.so │ │ │ ├── dnacalib.py │ │ │ ├── libdnacalib.so.6 │ │ │ ├── libembeddedRL4.so │ │ │ ├── libembeddedRL4.so.8 │ │ │ ├── libembeddedRL4.so.8.0.8 │ │ │ ├── MayaUE4RBFPlugin2023.mll │ │ │ ├── MayaUERBFPlugin.mll │ │ ├── 2024 │ │ │ ├── \_py3dnacalib.so │ │ │ ├── dnacalib.py │ │ │ ├── libdnacalib.so.6 │ │ │ ├── libembeddedRL4.so │ │ │ ├── MayaUERBFPlugin.mll │ │ ├── 2025 │ │ │ ├── \_py3dnacalib.so │ │ │ ├── dnacalib.py │ │ │ ├── embeddedRL4.so │ │ │ ├── libdnacalib.so.6 │ │ │ ├── MayaUERBFPlugin.mll │ │ ├── pydna │ │ │ ├── python3 │ │ │ │ ├── \_py3dna.so │ │ │ │ ├── dna.py │ │ │ │ ├── libdna.so.7.1.0 │ │ │ ├── python311 │ │ │ │ ├── \_py3dna.so │ │ │ │ ├── dna.py │ │ │ │ ├── libdna.so.7 │ │ │ ├── python397 │ │ │ │ ├── \_py3dna.so │ │ │ │ ├── dna.py │ │ │ │ ├── libdna.so.7.1.0 │ │ │ ├── python3108 │ │ │ │ ├── \_py3dna.so │ │ │ │ ├── dna.py │ │ │ │ ├── libdna.so.7.1.0 │ └── Windows │ │ ├── 2022 │ │ │ ├── \_py3dnacalib.pyd │ │ │ ├── dnacalib.dll │ │ │ ├── dnacalib.py │ │ │ ├── embeddedRL4.mll │ │ │ ├── MayaUE4RBFPlugin2022.mll │ │ │ ├── MayaUERBFPlugin.mll │ │ ├── 2023 │ │ │ ├── \_py3dnacalib.pyd │ │ │ ├── dnacalib.dll │ │ │ ├── dnacalib.py │ │ │ ├── embeddedRL4.mll │ │ │ ├── MayaUE4RBFPlugin2023.mll │ │ │ ├── MayaUERBFPlugin.mll │ │ ├── 2024 │ │ │ ├── \_py3dnacalib.pyd │ │ │ ├── dnacalib.dll │ │ │ ├── dnacalib.py │ │ │ ├── embeddedRL4.mll │ │ │ ├── MayaUERBFPlugin.mll │ │ ├── 2025 │ │ │ ├── \_py3dnacalib.pyd │ │ │ ├── dnacalib.dll │ │ │ ├── dnacalib.py │ │ │ ├── embeddedRL4.mll │ │ │ ├── MayaUERBFPlugin.mll │ │ ├── pydna │ │ │ ├── python3 │ │ │ │ ├── \_py3dna.pyd │ │ │ │ ├── dna.dll │ │ │ │ ├── dna.py │ │ │ ├── python311 │ │ │ │ ├── \_py3dna.pyd │ │ │ │ ├── \_py3dna9\_4\_3.pyd │ │ │ │ ├── dna.dll │ │ │ │ ├── dna.py │ │ │ │ ├── dna9\_4\_3.dll │ │ │ │ ├── polyalloc1\_3\_12.dll │ │ │ │ ├── statuscode1\_2\_6.dll │ │ │ │ ├── trio4\_0\_16.dll │ │ │ ├── python397 │ │ │ │ ├── \_py3dna.pyd │ │ │ │ ├── dna.dll │ │ │ │ ├── dna.py │ │ │ ├── python3108 │ │ │ │ ├── \_py3dna.pyd │ │ │ │ ├── dna.dll │ │ │ │ ├── dna.py │ │ │ ├── \_\_init\_\_.py ├── resources │ ├── icons │ │ ├── ARKit52.png │ │ ├── automatic\_grouping.png │ │ ├── backward.png │ │ ├── behaviour.png │ │ ├── blendRaw.png │ │ ├── blendShape\_current.png │ │ ├── blendShape.png │ │ ├── change\_password.png │ │ ├── chinese.png │ │ ├── clone\_blendShape.png │ │ ├── clothing\_weight.png │ │ ├── color.png │ │ ├── CommandButton.png │ │ ├── configuration.png │ │ ├── connect.png │ │ ├── controller.png │ │ ├── copy\_skin.png │ │ ├── copy.png │ │ ├── create\_body\_ctrl.png │ │ ├── create\_lod.png │ │ ├── ctrl\_hide.png │ │ ├── definition.png │ │ ├── delete.png │ │ ├── detector.png │ │ ├── disconnect.png │ │ ├── english.png │ │ ├── exit.png │ │ ├── export\_skin.png │ │ ├── export.png │ │ ├── expression.png │ │ ├── expressions\_blend.png │ │ ├── expressions\_current.png │ │ ├── expressions.png │ │ ├── forward.png │ │ ├── help.png │ │ ├── import\_body\_anim.png │ │ ├── import\_face\_anim.png │ │ ├── import\_skin.png │ │ ├── import.png │ │ ├── joint.png │ │ ├── load\_meshes.png │ │ ├── loading.png │ │ ├── locator.png │ │ ├── lock.png │ │ ├── mark.png │ │ ├── meshes.png │ │ ├── message.png │ │ ├── MetaFusionLogo.png │ │ ├── mirror.png │ │ ├── mirrorL.png │ │ ├── mirrorR.png │ │ ├── motion\_apply.png │ │ ├── open\_camera.png │ │ ├── open.png │ │ ├── pause.png │ │ ├── play.png │ │ ├── plus.png │ │ ├── pose\_A\_To\_T.png │ │ ├── pose\_T\_To\_A.png │ │ ├── presets.png │ │ ├── psd.png │ │ ├── rebuildTargets.png │ │ ├── reduce.png │ │ ├── rename.png │ │ ├── repair\_normals.png │ │ ├── repair\_vertex\_order.png │ │ ├── reset.png │ │ ├── resetname.png │ │ ├── return.png │ │ ├── save\_new.png │ │ ├── save.png │ │ ├── search.png │ │ ├── set\_current.png │ │ ├── set\_no.png │ │ ├── set\_ok.png │ │ ├── set\_range.png │ │ ├── settings.png │ │ ├── standardized\_naming.png │ │ ├── stop.png │ │ ├── supplement\_meshes.png │ │ ├── symmetry.png │ │ ├── target.png │ │ ├── transfer\_maps.png │ │ ├── unmark\_all.png │ │ ├── unreal.png │ │ ├── update.png │ │ ├── user\_login.png │ │ ├── visible.png │ │ ├── warning.png │ ├── styles │ │ ├── style.qss ├── scripts │ ├── ui │ ├── MetaFusion.py ├── CleanPycache.bat ├── Install.mel ├── Install.py ├── README.md ## 重要代码内容: ├── Install.mel ```Python global proc install() { string $scriptPath = `whatIs install`; string $dirPath = `substring $scriptPath 25 (size($scriptPath))`; $dirPath = `dirname $dirPath`; string $pythonPath = $dirPath + "/Install.py"; $pythonPath = substituteAllString($pythonPath, "\\", "/"); string $pythonCmd = "import os, sys\n"; $pythonCmd += "INSTALL_PATH = r'" + $pythonPath + "'\n"; $pythonCmd += "sys.path.append(os.path.dirname(INSTALL_PATH))\n"; $pythonCmd += "import Install\n"; $pythonCmd += "Install.main()\n"; python($pythonCmd); } install(); ``` ├── Install.py ```Python #!/usr/bin/env python # -*- coding: utf-8 -*- #===================================== 1. Module Imports ===================================== # Standard library imports import os import sys import webbrowser # Maya imports import maya.mel as mel import maya.cmds as cmds import maya.OpenMayaUI as omui # Qt imports from PySide2 import QtWidgets, QtGui, QtCore from shiboken2 import wrapInstance # Custom imports from config import data QtCore, QtGui, QtWidgets = data.Qt() #===================================== 2. Global Variables ===================================== ROOT_PATH = data.ROOT_PATH TOOL_NAME = data.TOOL_NAME TOOL_VERSION = data.TOOL_VERSION TOOL_AUTHOR = data.TOOL_AUTHOR TOOL_LANG = data.TOOL_LANG TOOL_WSCL_NAME = data.TOOL_WSCL_NAME TOOL_HELP_URL = data.TOOL_HELP_URL SCRIPTS_PATH = data.SCRIPTS_PATH ICONS_PATH = data.ICONS_PATH TOOL_MAIN_SCRIPT = data.TOOL_MAIN_SCRIPT TOOL_MOD_FILENAME = data.TOOL_MOD_FILENAME TOOL_ICON = data.TOOL_ICON TOOL_COMMAND_ICON = data.TOOL_COMMAND_ICON #===================================== 3. Utility Functions ===================================== def maya_main_window(): """Get Maya main window as QWidget""" main_window_ptr = omui.MQtUtil.mainWindow() return wrapInstance(int(main_window_ptr), QtWidgets.QWidget) def ensure_directory(directory_path): """Ensure directory exists, create if not""" if not os.path.exists(directory_path): os.makedirs(directory_path) print(f"Created directory: {directory_path}") return directory_path def get_maya_modules_dir(): """Get Maya modules directory path""" maya_app_dir = cmds.internalVar(userAppDir=True) return ensure_directory(os.path.join(maya_app_dir, "modules")) #===================================== 4. UI Component Classes ===================================== class SetButton(QtWidgets.QPushButton): """Custom styled button for installation interface""" def __init__(self, text): super(SetButton, self).__init__(text) #===================================== 5. Main Window Class ===================================== class InstallDialog(QtWidgets.QDialog): def __init__(self, parent=maya_main_window()): super(InstallDialog, self).__init__(parent) self.load_stylesheet() self.setup_ui() def load_stylesheet(self): """加载 QSS 样式文件""" try: style_file = data.TOOL_STYLE_FILE if os.path.exists(style_file): with open(style_file, 'r') as f: self.setStyleSheet(f.read()) else: print(f"Warning: Style file not found: {style_file}") except Exception as e: print(f"Error loading stylesheet: {e}") def setup_ui(self): """Initialize and setup UI components""" self.setWindowTitle(f"{TOOL_NAME} Installation") self.setFixedSize(220, 120) self.setup_window_icon() self.create_widgets() self.create_layouts() self.create_connections() def setup_window_icon(self): """Setup window icon if available""" if os.path.exists(TOOL_ICON): self.setWindowIcon(QtGui.QIcon(TOOL_ICON)) else: print(f"Warning: Icon file not found: {TOOL_ICON}") #----------------- 5.1 UI Methods ----------------- def create_widgets(self): self.new_shelf_toggle = QtWidgets.QCheckBox(f"{TOOL_NAME} Installation") self.install_button = SetButton("Install " + TOOL_NAME) self.uninstall_button = SetButton("Uninstall " + TOOL_NAME) def create_layouts(self): main_layout = QtWidgets.QVBoxLayout(self) main_layout.setContentsMargins(10, 2, 10, 5) main_layout.setSpacing(5) header_layout = QtWidgets.QHBoxLayout() header_layout.setSpacing(5) welcome_label = QtWidgets.QLabel("Welcome to " + TOOL_NAME + "!") welcome_label.setStyleSheet("font-size: 11px; padding: 0px; margin: 0px;") header_layout.addWidget(welcome_label) header_layout.addStretch() main_layout.addLayout(header_layout) main_layout.addWidget(self.install_button) main_layout.addWidget(self.uninstall_button) self.install_button.setFixedHeight(30) self.uninstall_button.setFixedHeight(30) def create_connections(self): self.install_button.clicked.connect(self.install) self.uninstall_button.clicked.connect(self.uninstall) def create_styled_message_box(self, title, text): msg_box = QtWidgets.QMessageBox(self) msg_box.setWindowTitle(title) msg_box.setText(text) msg_box.setStandardButtons(QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No) msg_box.setStyleSheet(self.styleSheet()) return msg_box #----------------- 5.2 Event Handler Methods ----------------- def event(self, event): if event.type() == QtCore.QEvent.EnterWhatsThisMode: QtWidgets.QWhatsThis.leaveWhatsThisMode() self.open_help_url() return True return QtWidgets.QDialog.event(self, event) def closeEvent(self, event): """Handle window close event""" try: super(InstallDialog, self).closeEvent(event) except Exception as e: print(f"Error closing window: {e}") event.accept() def helpEvent(self, event): self.open_help_url() event.accept() #----------------- 5.3 Utility Methods ----------------- def open_help_url(self): webbrowser.open(TOOL_HELP_URL) QtWidgets.QApplication.restoreOverrideCursor() def get_script_path(self): maya_script = mel.eval('getenv("MAYA_SCRIPT_NAME")') if maya_script and os.path.exists(maya_script): return os.path.dirname(maya_script) for sys_path in sys.path: install_path = os.path.join(sys_path, "install.py") if os.path.exists(install_path): return os.path.dirname(install_path) return os.getcwd() #----------------- 5.4 Installation Methods ----------------- def install(self): """Handle install request with error handling""" if not self._validate_paths(): return msg_box = self.create_styled_message_box( "Confirm Installation", f"Are you sure you want to install {TOOL_NAME}?" ) if msg_box.exec_() == QtWidgets.QMessageBox.Yes: try: self.install_tool() self.close() except Exception as e: error_msg = f"Error during installation: {e}" print(error_msg) QtWidgets.QMessageBox.critical(self, "Error", error_msg) def uninstall(self, *args): """Handle uninstall request""" msg_box = self.create_styled_message_box( "Confirm Uninstallation", f"Are you sure you want to uninstall {TOOL_NAME}?" ) if msg_box.exec_() == QtWidgets.QMessageBox.Yes: try: self.uninstall_tool() self.close() except Exception as e: error_msg = f"Error during uninstallation: {e}" print(error_msg) QtWidgets.QMessageBox.critical(self, "Error", error_msg) else: print("Uninstallation cancelled") def create_mod_file(self): """Create or update the .mod file for Maya""" modules_dir = get_maya_modules_dir() mod_content = f"""+ {TOOL_NAME} {TOOL_VERSION} {ROOT_PATH} scripts: {SCRIPTS_PATH} """ mod_file_path = os.path.join(modules_dir, TOOL_MOD_FILENAME) self._write_mod_file(mod_file_path, mod_content) def _write_mod_file(self, file_path, content): """Helper method to write .mod file""" try: with open(file_path, "w") as f: f.write(content) print(f"Successfully created/updated: {file_path}") except Exception as e: error_msg = f"Error writing .mod file: {e}" print(error_msg) QtWidgets.QMessageBox.critical(self, "Error", error_msg) def uninstall_mod_file(self): modules_dir = get_maya_modules_dir() mod_file_path = os.path.join(modules_dir, TOOL_MOD_FILENAME) if os.path.exists(mod_file_path): try: os.remove(mod_file_path) print(f"{TOOL_NAME}.mod file deleted") except Exception as e: print(f"Error deleting {TOOL_NAME}.mod file: {e}") def clean_existing_buttons(self): if cmds.shelfLayout(TOOL_NAME, exists=True): buttons = cmds.shelfLayout(TOOL_NAME, query=True, childArray=True) or [] for btn in buttons: if cmds.shelfButton(btn, query=True, exists=True): label = cmds.shelfButton(btn, query=True, label=True) if label == TOOL_NAME: cmds.deleteUI(btn) print(f"Deleted existing {TOOL_NAME} button: {btn}") def install_tool(self): """Install the tool to Maya""" if not os.path.exists(SCRIPTS_PATH): print(f"Error: Scripts path does not exist: {SCRIPTS_PATH}") return if not os.path.exists(TOOL_MAIN_SCRIPT): print(f"Error: Main script file not found: {TOOL_MAIN_SCRIPT}") return # Add scripts path to Python path if SCRIPTS_PATH not in sys.path: sys.path.insert(0, SCRIPTS_PATH) # Create shelf and button self._create_shelf_button() self.create_mod_file() # Switch to the newly created shelf try: cmds.shelfTabLayout("ShelfLayout", edit=True, selectTab=TOOL_NAME) print(f"Switched to {TOOL_NAME} shelf") except Exception as e: print(f"Error switching to {TOOL_NAME} shelf: {e}") self._show_install_success_message() def _create_shelf_button(self): """Create shelf button for the tool""" shelf_layout = mel.eval('$tmpVar=$gShelfTopLevel') # Create shelf if not exists if not cmds.shelfLayout(TOOL_NAME, exists=True): cmds.shelfLayout(TOOL_NAME, parent=shelf_layout) # Clean existing buttons self.clean_existing_buttons() # Create new button icon_path = TOOL_ICON if os.path.exists(TOOL_ICON) else TOOL_COMMAND_ICON command = self._get_shelf_button_command() cmds.shelfButton( parent=TOOL_NAME, image1=icon_path, label=TOOL_NAME, command=command, sourceType="python", annotation=f"{TOOL_NAME} {TOOL_VERSION}", noDefaultPopup=True, style="iconOnly" ) def _get_shelf_button_command(self): """Get the command string for shelf button""" return f""" import sys import os SCRIPTS_PATH = r'{SCRIPTS_PATH}' if SCRIPTS_PATH not in sys.path: sys.path.insert(0, SCRIPTS_PATH) os.chdir(SCRIPTS_PATH) try: import {TOOL_NAME} {TOOL_NAME}.show() except ImportError as e: print("Error importing {TOOL_NAME}:", str(e)) print("Scripts path:", SCRIPTS_PATH) print("sys.path:", sys.path) print("Contents of Scripts folder:", os.listdir(SCRIPTS_PATH)) """ def uninstall_tool(self): """Uninstall the tool from Maya""" window_name = f"{TOOL_NAME}Window" dock_name = f"{TOOL_NAME}WindowDock" shelf_file = f"shelf_{TOOL_NAME}.mel" if cmds.window(window_name, exists=True): try: cmds.deleteUI(window_name) except Exception as e: print(f"Error closing {TOOL_NAME} window: {e}") if cmds.dockControl(dock_name, exists=True): try: cmds.deleteUI(dock_name) except Exception as e: print(f"Error closing docked {TOOL_NAME} window: {e}") self.uninstall_mod_file() # Get the current shelf before removing it current_shelf = cmds.shelfTabLayout("ShelfLayout", query=True, selectTab=True) # Delete Shelves and Buttons if cmds.shelfLayout(TOOL_NAME, exists=True): try: cmds.deleteUI(TOOL_NAME, layout=True) except Exception as e: print(f"Error deleting {TOOL_NAME} shelf: {e}") self._clean_all_shelf_buttons() # Remove from Python path if SCRIPTS_PATH in sys.path: sys.path.remove(SCRIPTS_PATH) # Deleting Shelf Files shelf_path = os.path.join( cmds.internalVar(userAppDir=True), cmds.about(version=True), "prefs", "shelves", f"shelf_{TOOL_NAME}.mel" ) if os.path.exists(shelf_path): try: os.remove(shelf_path) except Exception as e: print(f"Error deleting shelf file: {e}") # If the current tool shelf is a deleted tool shelf, switch to another tool shelf if current_shelf == TOOL_NAME: shelves = cmds.shelfTabLayout("ShelfLayout", query=True, childArray=True) if shelves and len(shelves) > 0: cmds.shelfTabLayout("ShelfLayout", edit=True, selectTab=shelves[0]) self._show_uninstall_success_message() def _clean_all_shelf_buttons(self): """Clean up all shelf buttons related to the tool""" all_shelves = cmds.shelfTabLayout("ShelfLayout", query=True, childArray=True) or [] for shelf in all_shelves: shelf_buttons = cmds.shelfLayout(shelf, query=True, childArray=True) or [] for btn in shelf_buttons: if cmds.shelfButton(btn, query=True, exists=True): if cmds.shelfButton(btn, query=True, label=True) == TOOL_NAME: cmds.deleteUI(btn) def _show_uninstall_success_message(self): """Show uninstallation success message""" msg_box = QtWidgets.QMessageBox() msg_box.setWindowTitle("Uninstallation Successful") msg_box.setText(f"{TOOL_NAME} has been successfully uninstalled!") msg_box.setStandardButtons(QtWidgets.QMessageBox.Ok) msg_box.setWindowIcon(QtGui.QIcon(TOOL_ICON)) msg_box.setStyleSheet(self.styleSheet()) msg_box.exec_() def _show_install_success_message(self): msg_box = QtWidgets.QMessageBox() msg_box.setWindowTitle("Installation Successful") msg_box.setText(f"{TOOL_NAME} has been successfully installed!") msg_box.setStandardButtons(QtWidgets.QMessageBox.Ok) msg_box.setWindowIcon(QtGui.QIcon(TOOL_ICON)) msg_box.setStyleSheet(self.styleSheet()) msg_box.exec_() def _validate_paths(self): """Validate all required paths exist""" paths = { "Root": ROOT_PATH, "Scripts": SCRIPTS_PATH, "Icons": ICONS_PATH } for name, path in paths.items(): if not os.path.exists(path): error_msg = f"Error: {name} path does not exist: {path}" print(error_msg) QtWidgets.QMessageBox.critical(self, "Error", error_msg) return False return True def _log(self, message, error=False): """Log messages with timestamp""" from datetime import datetime timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") log_message = f"[{timestamp}] {message}" print(log_message) if error: QtWidgets.QMessageBox.critical(self, "Error", message) def _load_mel_shelf(self): """Load mel shelf file with error handling""" try: mel.eval(f'loadNewShelf "shelf_{TOOL_NAME}.mel"') except Exception as e: self._log(f"Error loading shelf file: {e}", error=True) #===================================== 6. Main Function ===================================== def main(): """Main entry point for the installer""" try: dialog = InstallDialog() dialog.show() except Exception as e: print(f"Error launching installer: {e}") return -1 return dialog if __name__ == "__main__": main() ``` │ ├── styles │ │ ├── style.qss ```Python /* 全局 QPushButton 样式 */ QPushButton { background-color: #D0D0D0; color: #303030; border-radius: 10px; padding: 5px; font-weight: bold; min-width: 80px; } QPushButton:hover { background-color: #E0E0E0; } QPushButton:pressed { background-color: #C0C0C0; } /* 单独的消息按钮样式(可选) */ .messageButton { background-color: #B0B0B0; color: #303030; border-radius: 10px; padding: 5px; font-weight: bold; min-width: 80px; } .messageButton:hover { background-color: #C0C0C0; } .messageButton:pressed { background-color: #A0A0A0; } /* MetaFusion 深色主题样式 */ /* 主窗口样式 */ QMainWindow { background-color: #333333; color: #CCCCCC; } /* 菜单栏样式 */ QMenuBar { background-color: #333333; color: #CCCCCC; border-bottom: 1px solid #222222; } QMenuBar::item { background-color: transparent; padding: 4px 8px; } QMenuBar::item:selected { background-color: #444444; } /* 工具栏样式 */ QToolBar { background-color: #333333; border: none; padding: 3px; } QToolButton { background-color: transparent; border: 1px solid transparent; border-radius: 2px; padding: 4px; margin: 1px; } QToolButton:hover { background-color: #444444; border: 1px solid #555555; } /* 标签页样式 */ QTabWidget::pane { border: 1px solid #222222; background-color: #333333; } QTabBar::tab { background-color: #2A2A2A; color: #CCCCCC; padding: 5px 10px; border: 1px solid #222222; min-width: 80px; } QTabBar::tab:selected { background-color: #333333; border-bottom: none; } QTabBar::tab:hover:not(:selected) { background-color: #3A3A3A; } /* 列表和树形控件样式 */ QTreeView, QListView { background-color: #2A2A2A; border: 1px solid #222222; color: #CCCCCC; } QTreeView::item:hover, QListView::item:hover { background-color: #3A3A3A; } QTreeView::item:selected, QListView::item:selected { background-color: #444444; } /* 输入框样式 */ QLineEdit { background-color: #2A2A2A; border: 1px solid #222222; border-radius: 2px; color: #CCCCCC; padding: 3px; } /* 下拉框样式 */ QComboBox { background-color: #2A2A2A; border: 1px solid #222222; border-radius: 2px; color: #CCCCCC; padding: 3px; min-width: 100px; } QComboBox::drop-down { border: none; width: 20px; } QComboBox::down-arrow { border-image: url(:/resources/icons/down_arrow.png); width: 12px; height: 12px; } /* 按钮样式 */ QPushButton { background-color: #2A2A2A; border: 1px solid #222222; border-radius: 2px; color: #CCCCCC; padding: 5px 15px; min-width: 80px; } QPushButton:hover { background-color: #3A3A3A; border: 1px solid #444444; } QPushButton:pressed { background-color: #222222; } /* 滚动条样式 */ QScrollBar:vertical { background: #2A2A2A; width: 10px; margin: 0; } QScrollBar::handle:vertical { background: #444444; min-height: 20px; border-radius: 5px; } QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical { height: 0px; } QScrollBar:horizontal { background: #2A2A2A; height: 10px; margin: 0; } QScrollBar::handle:horizontal { background: #444444; min-width: 20px; border-radius: 5px; } QScrollBar::add-line:horizontal, QScrollBar::sub-line:horizontal { width: 0px; } /* 分组框样式 */ QGroupBox { border: 1px solid #222222; border-radius: 3px; margin-top: 6px; padding-top: 6px; color: #CCCCCC; } QGroupBox::title { left: 7px; padding: 0px 3px; } /* 状态栏样式 */ QStatusBar { background-color: #333333; color: #CCCCCC; } /* 工具提示样式 */ QToolTip { background-color: #2A2A2A; border: 1px solid #222222; color: #CCCCCC; padding: 3px; } ``` ├── config │ └── \_\_init\_\_.py ```Python #!/usr/bin/env python # -*- coding: utf-8 -*- from . import * ``` │ └── data.py ```Python #!/usr/bin/env python # -*- coding: utf-8 -*- import os import sys import maya.cmds as cmds #===================================== 2. Global Variables ===================================== try: ROOT_PATH = os.path.dirname(INSTALL_PATH).replace("\\", "/") except NameError: # __file__ 在 config 中,所以返回上一级目录即项目根目录 ROOT_PATH = os.path.dirname(os.path.dirname(os.path.abspath(__file__))).replace("\\", "/") TOOL_NAME = "MetaFusion" TOOL_VERSION = "Beta v1.0.0" TOOL_AUTHOR = "Virtuos" TOOL_LANG = 'en_US' TOOL_WSCL_NAME = f"{TOOL_NAME}WorkSpaceControl" TOOL_HELP_URL = f"https://gitea.cgnico.com/CGNICO/{TOOL_NAME}/wiki" #===================================== 3. Paths ===================================== # PATHS SCRIPTS_PATH = os.path.join(ROOT_PATH, "scripts").replace("\\", "/") ICONS_PATH = os.path.join(ROOT_PATH, "resources", "icons").replace("\\", "/") STYLES_PATH = os.path.join(ROOT_PATH, "resources", "styles").replace("\\", "/") MAYA_VERSION = cmds.about(version=True) SYSTEM_OS = cmds.about(os=True) if MAYA_VERSION in ["2022", "2023", "2024", "2025"]: PLUGIN_PATH = os.path.join(ROOT_PATH, "plugins", SYSTEM_OS, MAYA_VERSION).replace("\\", "/") else: print(f"MetaFusion is not supported on Maya {MAYA_VERSION}") PYTHON_VERSION = sys.version_info.major if PYTHON_VERSION == 3: PYDNA_PATH = os.path.join(ROOT_PATH, "plugins", SYSTEM_OS, "python3").replace("\\", "/") elif PYTHON_VERSION == 3.11: PYDNA_PATH = os.path.join(ROOT_PATH, "plugins", SYSTEM_OS, "python311").replace("\\", "/") elif PYTHON_VERSION == 3.9: PYDNA_PATH = os.path.join(ROOT_PATH, "plugins", SYSTEM_OS, "python397").replace("\\", "/") elif PYTHON_VERSION == 3.10: PYDNA_PATH = os.path.join(ROOT_PATH, "plugins", SYSTEM_OS, "python3108").replace("\\", "/") else: print(f"MetaFusion is not supported on Python {PYTHON_VERSION}") #===================================== 3. Files ===================================== # FILES TOOL_MAIN_SCRIPT = os.path.join(SCRIPTS_PATH, f"{TOOL_NAME}.py").replace("\\", "/") TOOL_STYLE_FILE = os.path.join(STYLES_PATH, "style.qss").replace("\\", "/") TOOL_ICON = os.path.join(ICONS_PATH, f"{TOOL_NAME}Logo.png").replace("\\", "/") TOOL_COMMAND_ICON = os.path.join(ICONS_PATH, "CommandButton.png").replace("\\", "/") TOOL_MOD_FILENAME = f"{TOOL_NAME}.mod" #===================================== 4. Qt ===================================== # Qt def Qt(): try: from PySide import QtCore, QtGui, QtWidgets return QtCore, QtGui, QtWidgets except ImportError: try: from PySide2 import QtCore, QtGui, QtWidgets return QtCore, QtGui, QtWidgets except ImportError: try: from PySide3 import QtCore, QtGui, QtWidgets return QtCore, QtGui, QtWidgets except ImportError: try: from PySide4 import QtCore, QtGui, QtWidgets return QtCore, QtGui, QtWidgets except ImportError: try: from PySide5 import QtCore, QtGui, QtWidgets return QtCore, QtGui, QtWidgets except ImportError: try: from PySide6 import QtCore, QtGui, QtWidgets return QtCore, QtGui, QtWidgets except ImportError: print("未找到 Qt 模块") return None, None, None ``` ├── scripts │ ├── MetaFusion.py ```Python #!/usr/bin/env python # -*- coding: utf-8 -*- import os import sys from PySide2 import QtCore, QtGui, QtWidgets # 添加项目根目录到 Python 路径 ROOT_DIR = os.path.dirname(os.path.dirname(__file__)) if ROOT_DIR not in sys.path: sys.path.insert(0, ROOT_DIR) from config import data QtCore, QtGui, QtWidgets = data.Qt() #===================================== 2. Global Variables ===================================== TOOL_NAME = data.TOOL_NAME TOOL_VERSION = data.TOOL_VERSION TOOL_AUTHOR = data.TOOL_AUTHOR TOOL_LANG = data.TOOL_LANG TOOL_WSCL_NAME = data.TOOL_WSCL_NAME TOOL_HELP_URL = data.TOOL_HELP_URL SCRIPTS_PATH = data.SCRIPTS_PATH ICONS_PATH = data.ICONS_PATH TOOL_MAIN_SCRIPT = data.TOOL_MAIN_SCRIPT TOOL_MOD_FILENAME = data.TOOL_MOD_FILENAME TOOL_ICON = data.TOOL_ICON TOOL_COMMAND_ICON = data.TOOL_COMMAND_ICON main_window = None class MetaFusionWindow(QtWidgets.QMainWindow): def __init__(self, parent=None): super(MetaFusionWindow, self).__init__(parent) self.setWindowTitle("MetaFusion") self.resize(800, 600) # 加载样式表 self.load_stylesheet() # 创建UI self.setup_ui() def load_stylesheet(self): """加载QSS样式表""" style_file = os.path.join(os.path.dirname(os.path.dirname(__file__)), "resources", "styles", "style.qss") if os.path.exists(style_file): with open(style_file, 'r', encoding='utf-8') as f: self.setStyleSheet(f.read()) def setup_ui(self): """设置UI结构""" # 创建中心部件 central_widget = QtWidgets.QWidget() self.setCentralWidget(central_widget) # 创建主布局 main_layout = QtWidgets.QVBoxLayout(central_widget) # 创建菜单栏 self.create_menu_bar() # 创建工具栏 self.create_tool_bar() # 创建标签页 self.create_tabs() def create_menu_bar(self): """创建菜单栏""" menubar = self.menuBar() # 文件菜单 file_menu = menubar.addMenu("文件") file_menu.addAction("新建") file_menu.addAction("打开") file_menu.addAction("保存") file_menu.addSeparator() file_menu.addAction("退出") # 编辑菜单 edit_menu = menubar.addMenu("编辑") edit_menu.addAction("撤销") edit_menu.addAction("重做") # 帮助菜单 help_menu = menubar.addMenu("帮助") help_menu.addAction("关于") def create_tool_bar(self): """创建工具栏""" toolbar = self.addToolBar("主工具栏") toolbar.setMovable(False) # 添加工具栏按钮 toolbar.addAction(QtGui.QIcon(":/icons/new.png"), "新建") toolbar.addAction(QtGui.QIcon(":/icons/open.png"), "打开") toolbar.addAction(QtGui.QIcon(":/icons/save.png"), "保存") def create_tabs(self): """创建标签页""" self.tab_widget = QtWidgets.QTabWidget() self.centralWidget().layout().addWidget(self.tab_widget) # 创建四个主要标签页 self.model_tab = QtWidgets.QWidget() self.rig_tab = QtWidgets.QWidget() self.adjust_tab = QtWidgets.QWidget() self.define_tab = QtWidgets.QWidget() # 添加标签页到标签页控件 self.tab_widget.addTab(self.model_tab, "模型") self.tab_widget.addTab(self.rig_tab, "绑定") self.tab_widget.addTab(self.adjust_tab, "调整") self.tab_widget.addTab(self.define_tab, "定义") # 设置各个标签页的内容 self.setup_model_tab() self.setup_rig_tab() self.setup_adjust_tab() self.setup_define_tab() def setup_model_tab(self): """设置模型标签页内容""" layout = QtWidgets.QVBoxLayout(self.model_tab) # 在这里添加模型标签页的具体控件 def setup_rig_tab(self): """设置绑定标签页内容""" layout = QtWidgets.QVBoxLayout(self.rig_tab) # 在这里添加绑定标签页的具体控件 def setup_adjust_tab(self): """设置调整标签页内容""" layout = QtWidgets.QVBoxLayout(self.adjust_tab) # 在这里添加调整标签页的具体控件 def setup_define_tab(self): """设置定义标签页内容""" layout = QtWidgets.QVBoxLayout(self.define_tab) # 在这里添加定义标签页的具体控件 def show(): """显示主窗口""" global main_window try: main_window.close() except: pass main_window = MetaFusionWindow() main_window.show() return main_window if __name__ == "__main__": app = QtWidgets.QApplication([]) window = show() app.exec_() ```