使用 PySide2 开发 Maya 插件系列二:继承 uic 转换出来的 py 文件中的类 Ui_Form
开发环境:
Wing IDE 6.1
步骤1:
打开 Wing IDE,创建一个新的 project,保存这个 project 到某个路径下,把之前生产的 py 文件所在的文件夹添加到该 project 中,然后在文件夹下新建一个 py 文件,我这里命名为 PySideTest.py
图中 PySide2ToPySide.py 是一个 PySide2 兼容 PySide 的一个补丁代码,出处链接:http://www.cnblogs.com/hksac/p/9502236.html,如果要用需要把不必要的测试代码删除,修改后的代码如下:
1 from __future__ import with_statement 2 3 import os 4 import functools 5 import imp 6 import subprocess 7 import sys 8 import webbrowser 9 10 11 class PySide2Patcher(object): 12 _core_to_qtgui = set([ 13 "QAbstractProxyModel", 14 "QItemSelection", 15 "QItemSelectionModel", 16 "QItemSelectionRange", 17 "QSortFilterProxyModel", 18 "QStringListModel" 19 ]) 20 21 22 @classmethod 23 def _move_attributes(cls, dst, src, names): 24 """ 25 Moves a list of attributes from one package to another. 26 27 :param names: Names of the attributes to move. 28 """ 29 for name in names: 30 if not hasattr(dst, name): 31 setattr(dst, name, getattr(src, name)) 32 33 @classmethod 34 def _patch_QTextCodec(cls, QtCore): 35 """ 36 Patches in QTextCodec. 37 38 :param QTextCodec: The QTextCodec class. 39 """ 40 original_QTextCodec = QtCore.QTextCodec 41 42 class QTextCodec(original_QTextCodec): 43 @staticmethod 44 def setCodecForCStrings(codec): 45 pass 46 47 QtCore.QTextCodec = QTextCodec 48 49 @classmethod 50 def _fix_QCoreApplication_api(cls, wrapper_class, original_class): 51 52 wrapper_class.CodecForTr = 0 53 wrapper_class.UnicodeUTF8 = 1 54 wrapper_class.DefaultCodec = wrapper_class.CodecForTr 55 56 @staticmethod 57 def translate(context, source_text, disambiguation=None, encoding=None, n=None): 58 59 if n is not None: 60 return original_class.translate(context, source_text, disambiguation, n) 61 else: 62 return original_class.translate(context, source_text, disambiguation) 63 64 wrapper_class.translate = translate 65 66 @classmethod 67 def _patch_QCoreApplication(cls, QtCore): 68 69 original_QCoreApplication = QtCore.QCoreApplication 70 71 class QCoreApplication(original_QCoreApplication): 72 pass 73 cls._fix_QCoreApplication_api(QCoreApplication, original_QCoreApplication) 74 QtCore.QCoreApplication = QCoreApplication 75 76 @classmethod 77 def _patch_QApplication(cls, QtGui): 78 79 original_QApplication = QtGui.QApplication 80 81 class QApplication(original_QApplication): 82 def __init__(self, *args): 83 original_QApplication.__init__(self, *args) 84 QtGui.qApp = self 85 86 @staticmethod 87 def palette(widget=None): 88 89 return original_QApplication.palette(widget) 90 91 cls._fix_QCoreApplication_api(QApplication, original_QApplication) 92 93 QtGui.QApplication = QApplication 94 95 @classmethod 96 def _patch_QAbstractItemView(cls, QtGui): 97 98 original_QAbstractItemView = QtGui.QAbstractItemView 99 100 class QAbstractItemView(original_QAbstractItemView): 101 def __init__(self, *args): 102 original_QAbstractItemView.__init__(self, *args) 103 104 if hasattr(self, "dataChanged"): 105 original_dataChanged = self.dataChanged 106 107 def dataChanged(tl, br, roles=None): 108 original_dataChanged(tl, br) 109 self.dataChanged = lambda tl, br, roles: dataChanged(tl, br) 110 111 QtGui.QAbstractItemView = QAbstractItemView 112 113 @classmethod 114 def _patch_QStandardItemModel(cls, QtGui): 115 116 original_QStandardItemModel = QtGui.QStandardItemModel 117 118 class SignalWrapper(object): 119 def __init__(self, signal): 120 self._signal = signal 121 122 def emit(self, tl, br): 123 self._signal.emit(tl, br, []) 124 125 def __getattr__(self, name): 126 return getattr(self._signal, name) 127 128 class QStandardItemModel(original_QStandardItemModel): 129 def __init__(self, *args): 130 original_QStandardItemModel.__init__(self, *args) 131 self.dataChanged = SignalWrapper(self.dataChanged) 132 133 QtGui.QStandardItemModel = QStandardItemModel 134 135 @classmethod 136 def _patch_QMessageBox(cls, QtGui): 137 138 button_list = [ 139 QtGui.QMessageBox.Ok, 140 QtGui.QMessageBox.Open, 141 QtGui.QMessageBox.Save, 142 QtGui.QMessageBox.Cancel, 143 QtGui.QMessageBox.Close, 144 QtGui.QMessageBox.Discard, 145 QtGui.QMessageBox.Apply, 146 QtGui.QMessageBox.Reset, 147 QtGui.QMessageBox.RestoreDefaults, 148 QtGui.QMessageBox.Help, 149 QtGui.QMessageBox.SaveAll, 150 QtGui.QMessageBox.Yes, 151 QtGui.QMessageBox.YesAll, 152 QtGui.QMessageBox.YesToAll, 153 QtGui.QMessageBox.No, 154 QtGui.QMessageBox.NoAll, 155 QtGui.QMessageBox.NoToAll, 156 QtGui.QMessageBox.Abort, 157 QtGui.QMessageBox.Retry, 158 QtGui.QMessageBox.Ignore 159 ] 160 161 162 def _method_factory(icon, original_method): 163 164 def patch(parent, title, text, buttons=QtGui.QMessageBox.Ok, defaultButton=QtGui.QMessageBox.NoButton): 165 166 msg_box = QtGui.QMessageBox(parent) 167 msg_box.setWindowTitle(title) 168 msg_box.setText(text) 169 msg_box.setIcon(icon) 170 for button in button_list: 171 if button & buttons: 172 msg_box.addButton(button) 173 msg_box.setDefaultButton(defaultButton) 174 msg_box.exec_() 175 return msg_box.standardButton(msg_box.clickedButton()) 176 177 functools.update_wrapper(patch, original_method) 178 179 return staticmethod(patch) 180 181 original_QMessageBox = QtGui.QMessageBox 182 183 class QMessageBox(original_QMessageBox): 184 185 critical = _method_factory(QtGui.QMessageBox.Critical, QtGui.QMessageBox.critical) 186 information = _method_factory(QtGui.QMessageBox.Information, QtGui.QMessageBox.information) 187 question = _method_factory(QtGui.QMessageBox.Question, QtGui.QMessageBox.question) 188 warning = _method_factory(QtGui.QMessageBox.Warning, QtGui.QMessageBox.warning) 189 190 QtGui.QMessageBox = QMessageBox 191 192 @classmethod 193 def _patch_QDesktopServices(cls, QtGui, QtCore): 194 195 if hasattr(QtGui, "QDesktopServices"): 196 return 197 198 class QDesktopServices(object): 199 200 @classmethod 201 def openUrl(cls, url): 202 if not isinstance(url, QtCore.QUrl): 203 url = QtCore.QUrl(url) 204 205 if url.isLocalFile(): 206 url = url.toLocalFile().encode("utf-8") 207 208 if sys.platform == "darwin": 209 return subprocess.call(["open", url]) == 0 210 elif sys.platform == "win32": 211 os.startfile(url) 212 return os.path.exists(url) 213 elif sys.platform.startswith("linux"): 214 return subprocess.call(["xdg-open", url]) == 0 215 else: 216 raise ValueError("Unknown platform: %s" % sys.platform) 217 else: 218 try: 219 return webbrowser.open_new_tab(url.toString().encode("utf-8")) 220 except: 221 return False 222 223 @classmethod 224 def displayName(cls, type): 225 cls.__not_implemented_error(cls.displayName) 226 227 @classmethod 228 def storageLocation(cls, type): 229 cls.__not_implemented_error(cls.storageLocation) 230 231 @classmethod 232 def setUrlHandler(cls, scheme, receiver, method_name=None): 233 cls.__not_implemented_error(cls.setUrlHandler) 234 235 @classmethod 236 def unsetUrlHandler(cls, scheme): 237 cls.__not_implemented_error(cls.unsetUrlHandler) 238 239 @classmethod 240 def __not_implemented_error(cls, method): 241 raise NotImplementedError( 242 "PySide2 and Toolkit don't support 'QDesktopServices.%s' yet. Please contact %s" % 243 (method.__func__, 'asdf@qq.com') 244 ) 245 246 QtGui.QDesktopServices = QDesktopServices 247 248 @classmethod 249 def patch(cls, QtCore, QtGui, QtWidgets, PySide2): 250 251 qt_core_shim = imp.new_module("PySide.QtCore") 252 qt_gui_shim = imp.new_module("PySide.QtGui") 253 254 255 cls._move_attributes(qt_gui_shim, QtWidgets, dir(QtWidgets)) 256 cls._move_attributes(qt_gui_shim, QtGui, dir(QtGui)) 257 258 259 cls._move_attributes(qt_gui_shim, QtCore, cls._core_to_qtgui) 260 cls._move_attributes(qt_core_shim, QtCore, set(dir(QtCore)) - cls._core_to_qtgui) 261 262 cls._patch_QTextCodec(qt_core_shim) 263 cls._patch_QCoreApplication(qt_core_shim) 264 cls._patch_QApplication(qt_gui_shim) 265 cls._patch_QAbstractItemView(qt_gui_shim) 266 cls._patch_QStandardItemModel(qt_gui_shim) 267 cls._patch_QMessageBox(qt_gui_shim) 268 cls._patch_QDesktopServices(qt_gui_shim, qt_core_shim) 269 270 return qt_core_shim, qt_gui_shim 271 272 273 274 275 import PySide2 276 from PySide2 import QtCore, QtGui, QtWidgets 277 278 def _import_module_by_name(parent_module_name, module_name): 279 280 module = None 281 try: 282 module = __import__(parent_module_name, globals(), locals(), [module_name]) 283 module = getattr(module, module_name) 284 except Exception as e: 285 pass 286 return module 287 288 289 QtCore, QtGui = PySide2Patcher.patch(QtCore, QtGui, QtWidgets, PySide2) 290 QtNetwork = _import_module_by_name("PySide2", "QtNetwork") 291 QtWebKit = _import_module_by_name("PySide2.QtWebKitWidgets", "QtWebKit")
PySideTest.py 代码如下:
1 # -*- coding: utf-8 -*- 2 import sys 3 try: 4 from PySide import QtCore, QtGui 5 import test_ui_pyside as ui 6 except: 7 from PySide2ToPySide import QtCore, QtGui #注意:不能确保完全兼容,但常用的基本兼容 8 import test_ui_pyside2 as ui #使用 pyside2-uic 生成 test_ui_pyside2 9 10 class MainWindow(QtGui.QWidget, ui.Ui_Form): # 如果 designer 新建的时候选的是 MainWindow,则要集成 QtGui.QMainWindow,其它的类型要对应好 11 def __init__(self, parent = None): 12 super(MainWindow, self).__init__(parent) # 执行父类的__init__() 13 self.setupUi(self) # 调用 ui.Ui_Form 的 setupUi() 14 15 def main(): 16 """ 和maya中的不一样 """ 17 app = QtGui.QApplication(sys.argv) # window是基于application的,所以在没有application的情况下要创建一个,如果在 maya 中,则不需要,因为maya本身就是一个 application 18 win = MainWindow() #实例一个window 19 win.show() # 显示window 20 sys.exit(app.exec_()) # 退出application,同时会释放win 21 22 if __name__ == '__main__': #对当前文件进行debug则会运行以下代码,import该文件不会运行,请了解模块默认属性 __name__ 的特点和用处 23 main()
这时候已经可以点击debug,运行结果:
步骤2:
设置 wing IDE 的 project 属性 Project->Project Properties...
这样的好处是可以让 IDE 有maya python 模块的命令补全。
新建一个 PySideTest_maya.py,这是提供给 maya 运行的:
1 # -*- coding: utf-8 -*- 2 import sys 3 4 import PySideTest 5 6 try: 7 from PySide import QtCore, QtGui 8 import shiboken 9 except: 10 from PySide2ToPySide import QtCore, QtGui 11 import shiboken2 as shiboken 12 13 import maya.OpenMayaUI as omui 14 def maya_main_window(): 15 main_window_ptr = omui.MQtUtil.mainWindow() #获得maya主窗口的指针,主要是为了让插件界面设置它为父窗口 16 return shiboken.wrapInstance(long(main_window_ptr), QtGui.QWidget) #把maya主窗口封装从QtGui对象 17 18 class MainWindow(PySideTest.MainWindow): 19 def __init__(self, parent = None): 20 super(MainWindow, self).__init__(parent) 21 22 self.setWindowTitle("TestWindow") #设置窗口标题 23 self.setWindowFlags(QtCore.Qt.Window) #设置窗口标志为window,这样会使得widget成为独立窗口,不然会附着在maya的左上角,如果UI是继承的是QMainWindow,则不需要设置 24 self.setAttribute(QtCore.Qt.WA_DeleteOnClose) #设置属性为关闭窗口则释放它的对象,窗口关闭,实例对象还存在,只要再次show即可,如果win再main中不断的新建MainWindow,则需要设置 25 26 def main(): 27 global win 28 try: 29 win.close() #为了不让窗口出现多个,因为第一次运行还没初始化,所以要try,在这里尝试先关闭,再重新新建一个窗口 30 except: 31 pass 32 win = MainWindow(maya_main_window()) #如果把win的初始化放在方法外,则不需要self.setAttribute(QtCore.Qt.WA_DeleteOnClose),同时关闭后再显示,还会保持上一次的窗口状态 33 win.show() 34 35 if __name__ == "__main__": 36 main()
分别在 maya2015 和 maya2017 的 Script Editor 的 python tab 里编写如下代码:
1 import sys 2 sys.path.append(r'E:WorksMayaScriptsPySideTest') #把代码所在的路径添加到环境变量PATH中,这样可以import它们 3 4 import PySideTest_maya 5 reload(PySideTest_maya) 6 PySideTest_maya.main()
选中需要运行的代码,Ctrl+Shift+Enter 运行:
运行结果: