最近在研究用PyQt写Maya插件的界面,遇到不少的疑难杂症,在这里汇总一下,便于日后查询。
与Maya界面融合
首先最主要的目标是想让PyQt写好的界面与Maya完美融合,不会在操作Maya界面的时候把我们自己的窗口压到后面,而网上传统的方法便是使用OpenMayaUI 库以及shiboken2 库中的wrapInstance 方法,将我们的窗口parent到已经存在的Maya窗口中。
官网的Maya开发人员帮助中也给了以下代码示例:
from maya import OpenMayaUI as omui
from PySide2.QtCore import *
from PySide2.QtGui import *
from PySide2.QtWidgets import *
from PySide2 import __version__
from shiboken2 import wrapInstance
mayaMainWindowPtr = omui.MQtUtil.mainWindow()
mayaMainWindow= wrapInstance(long(mayaMainWindowPtr), QWidget)
hello = QLabel("Hello, World", parent=mayaMainWindow)
hello.setObjectName('MyLabel')
hello.setWindowFlags(Qt.Window)
hello.show()
hello = None
hello = QLabel("Hello, World", parent=None)
hello.setObjectName('MyLabel')
hello.show()
hello = None
代码中对比了parent = mayaMainWindow 和parent = None 两种情况,后者在使用show()方法后窗口会由于Python的GC机制在创建后瞬间消失,但前者由于parent到了Maya的主窗口中,就会由Maya来维持其生命周期。另外,此时在操作Maya界面的时候,我们的窗口也会一直保持置顶不会被挡住。
Dock窗口
为了让我们的窗口能够dock在Maya的UI中,网上常见的方法是结合上面的wrapInstance 方法,再通过内置库的cmds.workspaceControl 来实现。(注:Maya2017之前的版本为 cmds.dockControl )
例如Dhruv Govil大神的Python For Maya: Artist Friendly Programming教程中的一个案例就是如此:
def getDock(name='LightingManagerDock'):
deleteDock(name)
ctrl = pm.workspaceControl(name,dockToMainWindow=('right',1),label="Lighting Manager")
qtCtrl = omui.MQtUtil.findControl(ctrl)
ptr = wrapInstance(long(qtCtrl),QtWidgets.QWidget)
return ptr
def deleteDock(name='LightingManagerDock'):
if pm.workspaceControl(name,query=True,exists=True) :
pm.deleteUI(name)
- 其中,大神这里用的是PyMel库
pm. ,也可替换成cmds. - 另外
omui.MQtUtil.findControl 方法的官方解释为:Auto-naming a widget so that it can be looked up as a string.
虽然以上方法可行,但根据教程还需要再额外添加多行代码才能实现dock的逻辑,比较麻烦,于是我又去调研了其他方案。
后来发现Maya提供了maya.app.general.mayaMixin 模块,其中包含的类可以方便将基于PyQt创建的控件融合进Maya UI中,其中用于dock窗口的就是MayaQWidgetDockableMixin 类。
官方示例:
from maya.app.general.mayaMixin import MayaQWidgetDockableMixin
from PySide.QtGui import QPushButton, QSizePolicy
class MyDockableButton(MayaQWidgetDockableMixin, QPushButton):
def __init__(self, parent=None):
super(MyDockableButton, self).__init__(parent=parent)
self.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Preferred )
self.setText('Push Me')
button = MyDockableButton()
button.show(dockable=False)
buttonName = button.objectName()
print('# ' + buttonName)
print('# ' + button.showRepr())
button.show(dockable=True)
print('# ' + button.showRepr())
- 在继承了
MayaQWidgetBaseMixin 类后,如果没有明确指定parent,则可以直接将我们的控件parent到main Maya window,无需再用wrapInstance 方法,非常方便。 - 另外要注意
MayaQWidgetBaseMixin 应作为第一继承,否则在使用show(dockable=True) 语句时会报错。 - 继承该类后,PyQt的
setWindowIcon 方法会失效,窗口无法自定义图标。
保持窗口唯一
当我们用上述方法写完窗口后,会发现重复执行show 方法时,会弹出多个窗口,这个原因就是我们每实例化一次,Maya都会自动给我们的窗口起一个独一无二的Object Name,如上面案例中的MyDockableButton_368fe1d8-5bc3-4942-a1bf-597d1b5d3b83 。
解决该问题的方法也很简单,可以手动用setObjectName 方法给我们的窗口命名,避免可以同时实例化多个窗口。当然我们在后续重复执行代码的时候,Maya会因为重名问题报错:Object’s name is not unique,所以应该在实例化之前先用cmds.deleteUI 删除已存在的窗口。
于是我们得到了一个很简单的方式来写Maya Dockable Window:
class MyDockableWindow(MayaQWidgetDockableMixin, QtWidgets.QWidget):
def __init__(self):
super(MyDockableWindow, self).__init__()
try:
cmds.deleteUI('MDWWorkspaceControl')
except RuntimeError:
pass
self.setWindowTitle('My Dockable Window')
self.resize(500, 400)
self.setObjectName('MDW')
self.show(dockable=True)
MyWin = MyDockableWindow()
界面尺寸问题
由于我写的界面宽高并不是Fixed,而是使用QSizePolicy.Expanding 允许用户随意缩放窗口,但我又希望每次用户打开界面的时候能复原我的初始布局,于是我在__init__ 时用了resize 方法来确保界面大小固定。
而当继承了MayaQWidgetDockableMixin 后,我发现我的界面经过用户手动大小调整后每次关闭再打开,Maya始终记得界面关闭前的大小,resize 方法失效了。
经过研究,我发现Maya会将每个界面布局(Workspace)存储在一个对应的JSON文件中,其中记录着各个控件(如Outliner、ToolBox、ArnoldRenderView)的布局信息,所以在每次打开某一个控件的时候,Maya都会记得它上次关闭前的一些属性,包括窗口大小。
这个JSON文件位于C:\Users\<用户名>\Documents\maya\<版本>\prefs\workspaces 文件夹中,内容参考如下:
"closedControls": [
{
"objectName": "UVToolkitDockControl",
"posX": 1901,
"posY": 697,
"controlHeight": 930,
"controlWidth": 315,
"widthProperty": "preferred",
"heightProperty": "free"
},
{
"objectName": "hyperShadePanel1Window",
"posX": 2610,
"posY": 333,
"controlHeight": 870,
"controlWidth": 1365,
"widthProperty": "free",
"heightProperty": "free"
},
{
"objectName": "ArnoldRenderView",
"posX": 765,
"posY": 758,
"controlHeight": 750,
"controlWidth": 1450,
"widthProperty": "free",
"heightProperty": "free"
},
]
- closedControls是没有dock在Maya界面中,已经被关闭的Workspace Control,如果我们的窗口在关闭Maya时没有dock,则可以在这里面找到。
- 其中controlHeight以及controlWidth就记录了这个窗口的宽高属性,会在下次打开时调用。
- 在关闭软件时,Maya会自动调用
saveShelf 命令来记录界面布局信息,该JSON文件也会被重写。
保存Dock窗口
如果我们在关闭Maya时,我们的窗口已经dock在了Maya界面布局上,会发现下次启动Maya后窗口消失。
如果想要创建一个能够跨会话永久保持的窗口,则可以参考Kaine van Gemert大神的方法,代价就是比较复杂。
原文链接:link
If you’re designing a tool where you would like a persistent interface across multiple sessions, that remembers its location within Maya such as the Outliner or Modelling Toolkit, we need to make use of the uiScript argument in our show method.
The uiScript argument takes a string representing Python code that will be executed to construct the UI when Maya is launched. This argument is part of the workspaceControl command and the show method in MayaQWidgetDockableMixin will pass this along when it creates the control.
To make use of this, we need to create a method for Maya that it can call to construct our UI. The one caveat is we will also need to handle parenting our UI to the Workspace Control ourselves.
The final self-restoring class looks like this:
import inspect
import maya.OpenMayaUI as omui
from maya.app.general.mayaMixin import MayaQWidgetDockableMixin
from PySide2 import QtWidgets
import shiboken2
mixinWindows = {}
class DockableBase(MayaQWidgetDockableMixin):
"""
Convenience class for creating dockable Maya windows.
"""
def __init__(self, controlName, **kwargs):
super(DockableBase, self).__init__(**kwargs)
self.setObjectName(controlName)
def show(self, *args, **kwargs):
"""
Show UI with generated uiScript argument
"""
modulePath = inspect.getmodule(self).__name__
className = self.__class__.__name__
super(DockableBase, self).show(dockable=True,
uiScript="import {0}; {0}.{1}._restoreUI()".format(modulePath, className), **kwargs)
@classmethod
def _restoreUI(cls):
"""
Internal method to restore the UI when Maya is opened.
"""
instance = cls()
workspaceControl = omui.MQtUtil.getCurrentParent()
mixinPtr = omui.MQtUtil.findControl(instance.objectName())
omui.MQtUtil.addWidgetToMayaLayout(long(mixinPtr), long(workspaceControl))
global mixinWindows
mixinWindows[instance.objectName()] = instance
To create a dockable window using our new MayaQWidgetDockableMixin wrapper class, inherit it along with the QWidget you wish to use. Remember, the inheritance order is still crucial!
class MyDockableWindow(DockableBase, QtWidgets.QDialog):
def __init__(self):
super(MyDockableWindow, self).__init__(controlName="MyWindow")
self.setWindowTitle("My Window")
self.pushButton = QtWidgets.QPushButton("Push me or else!", parent=self)
self.setLayout(QtWidgets.QVBoxLayout())
self.layout().addWidget(self.pushButton)
Due to the method of automatically generating the uiScript argument, if you execute this code directly from the Maya Script Editor, it won’t be able to determine the module where your class resides. You will need to make sure you have saved the file to one of Maya’s scripts directories, so it can be found when Maya launches, and then import your class.
For example, if you save your code to ‘scripts/dockableWindow.py’, then you would create an instance in the following manner:
from dockableWindow import MyDockableWindow
myWindow = MyDockableWindow()
myWindow.show()
You will now have a window that not only docks, but saves its location and restores itself successfully each time Maya is opened.
|