前言

  2年前我还在华强的时候,就发掘了 Qt.py 内置了一个超棒的功能。
  可以动态加载 ui 文件生成界面,而不需要pyside-uic 进行代码组装。

  但是不知道什么时候开始,我就发现在 Maya 使用 loadUi 的方案不灵了。
  偶尔会遇到 C++ 被删除的错误。
  后来鉴于稳定性考虑,我写了一个 compile 脚本来实现自动生成 ui python 文件。
  下面我就来总结一下一路走来的情况。

问题再现

  去年的时候偶然发现 Maya 里面原来还有 mayaMixin 的库 链接
  可以继承一个类来实现 Maya 内置的 Qt 窗口,这样就不需要用以前那种将 Maya 窗口转为 Qt 再嵌入 UI 的奇怪方式了。 旧方案窗口生成 链接

  如果没记错的话,自从用上了 mayaMixin 的方式生成的 ui 在特定的情况下就会导致调用错误

1
2
3
4
# Traceback (most recent call last):
# File "G:\run_maya.py", line 85, in test_call
# print(self.Test_LE.text())
# RuntimeError: Internal C++ object (PySide2.QtWidgets.QLineEdit) already deleted.

alt

1
2
def test_call(self):
print(self.Test_LE.text())

  点击按钮只是触发 QLineEdit 的文本信息而已。
  这里只要是继承了 mayaMixin.MayaQWidgetBaseMixin 这个类
  然后使用 Qt.py 提供的 QtCompat.loadUi 方法显示窗口。
  就有可能出现这个问题。

  但是这出错并不是无法读取 ui 文件生成界面,而是点击按钮去读取一些组件数据的时候。
  比如通过 button 读取 QLineEdit 的 text 的时候就会有上面的出错提示。
  但是这个在 __init__ 方法中调用相关的 组件 数据是没有问题。
  于是我通过 QTimer.singleShot 进行测试,发现只要延时执行 50 毫秒就会出现 C++ 被删除的问题。
  于是很自然想到这些生成的 ui 可能没有被类或者全局变量挂载,所以被垃圾回收了。

  然而这个问题要如何解决呢?
  我最初推测是 Qt.py 的 load_ui 方法有问题。
  于是去研究了源码,源码就是 继承了 QUiLoader 这个类可以实现自定义的组件挂载功能 Qt文档链接
  于是我修改了 Qt.py 的源码,给每个生成的组件添加了 destroyed 打印的功能
  这样如果 QObject 被 垃圾回收 了 应该会输出删除的信息。
  然而奇葩就出现了,毕竟 UI 是能够显示出来的,说明它并没有删除,但是用 self 去调用相关的函数就会提示 C++ 删除了。
  这个问题让我百思不得其解,而且也只有 Maya 有这个问题,在 Unreal 的环境下并没有这个问题。

compile.py 自动生成 ui python 脚本

  因为问题的症结不是很清楚,至少还有出脚本的方案可以走。
  于是我就走了一遍 PySide2-uic 编译的流程 操作链接
  果然这个方案是可以用,于是我就写了一个 python 脚本自动将脚本目录下所有的 ui 文件自动编译生成 python 文件。 脚本链接

  当然脚本里面的 Qt.py 路径 和 Maya 路径都需要自行进行配置。
  最后经过对 Qt.py 源码的阅读,还发现它有命令行功能,可以将编译的 python 文件转成 Qt.py 下的规范。
  只是会额外生成 _backup 备份文件,属于 Qt.py 实验性的隐藏功能。

优势

  1. 提升启动效率
  2. 能提供代码提示
    缺点
  3. 需要手动触发一下编译才能更新 ui

mayaMixin 最后的挣扎

  后来我也查了 mayaMixin 的源码,查清楚到底是哪里的缺漏导致。
  于是发现官方的方案 和我以前自己想出来的第三种方案有很大的相似之处 链接
  也是将生成的窗口 parent 到 Maya 窗口的方案。
  但是当时我写文章的时候记得,parent 一定要在 __init__ 函数里面传入,只是调用 setParent 会引发 C++ 问题。
  于是我就转换思路,利用 QtWidgets.QApplication.activeWindow() 获取当前的窗口。
  然后传入到 __init__ ,虽然窗口会生成到左上角去了,不过的确解决了我的问题。

maya cmds.window 嵌入

  后来我再整合了以前使用嵌入方案。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
class MayaShowMixin(object):
@classmethod
def maya_show(cls, win_name=""):
from Qt.QtCompat import wrapInstance
from maya import cmds, OpenMayaUI

def maya_to_qt(name):
# Maya -> QWidget
ptr = OpenMayaUI.MQtUtil.findControl(name)
if ptr is None:
ptr = OpenMayaUI.MQtUtil.findLayout(name)
if ptr is None:
ptr = OpenMayaUI.MQtUtil.findMenuItem(name)
if ptr is not None:
return wrapInstance(long(ptr), QtWidgets.QWidget)

win_name = win_name if win_name else cls.__name__
# NOTE 如果变量存在 就检查窗口多开
if cmds.window(win_name, q=1, ex=1):
cmds.deleteUI(win_name)

instance = cls()
window = cmds.window(win_name, title=instance.windowTitle())

cmds.showWindow(window)
# NOTE 将Maya窗口转换成 Qt 组件
cls.__maya_window__ = maya_to_qt(window)
layout = QtWidgets.QVBoxLayout()
cls.__maya_window__.setLayout(layout)
layout.setContentsMargins(0, 0, 0, 0)
layout.addWidget(instance)

cls.__maya_window__.setMaximumSize(instance.maximumSize())
cls.__maya_window__.setMinimumSize(instance.minimumSize())
return cls.__maya_window__

  这样就可以实现 mayaMixin 类似的效果。
  需要注意这个方案,利用 maya_to_qt 转换的窗口需要有变量挂载。
  否则会招致上面的 C++ 删除的问题。
  这里我将它挂载到类里面。

  这个方法利用 cmds 来构建窗口。
  好处是可以记录窗口打开的位置和大小。

总结

  最后我还是采用了 compile.py 的方案,缺点就是没办法让 QtDesigner 在保存的时候给我自动编译。
  需要我手动触发一下,有需要的话也可以用 Qt 框架的 QFileSystemWatcher 来写个监听实现自动编译。

  说到做到,就结合 QFileSystemWatcher 整出个自动监听 ui 变化的功能出来了 脚本链接

2021-4-28 dayu_widgets 的相关问题

  今天发现 dayu_widgets 在 Maya2020 下生成 Python 代码运行会出错。

alt

  如上图所示在 Maya 2020 下运行出错,主要原因是 MPushButton(QWidget()) 传入 QWidget 类
  会导致触发 QPushButton.setText(QWidget()) 函数导致出错。
  但是如果使用 Qt.py 的 loadUi 方式就不会报错,也就是调用 QUiLoader 类可以兼容 Maya2020
  至于为何 Maya2020 为何如此特殊,主要原因是 Maya 2020 升级了 Qt 的版本,所以内核会有所不同。