在场景中执行修改并从 Maya 中的 QThread 更新自定义 window

Perform modifications in the scene and update custom window from a QThread in Maya

上下文

我正在 Maya 中创建 PySide2 工具 运行。该工具正在执行很多长任务,一些修改场景(清理任务),一些创建文件(导出任务)。

因为这是一项长期任务,我想在 运行 时显示反馈(进度条)。

问题

示例代码

这是一段简化的代码,显示了我目前的进展情况。这是使用 QThread 的正确方法吗?我来自 CG 艺术家背景,不是专业程序员,所以我可能误用或误解了我尝试使用的概念(线程、PySide...)

import time

from PySide2.QtGui import *
from PySide2.QtCore import *
from PySide2.QtWidgets import *

import maya.cmds as cmds


class Application(object):
    def __init__(self):
        self.view = View(self)

    def do_something(self, callback):
        start = int(cmds.playbackOptions(q=True, min=True))
        end = int(cmds.playbackOptions(q=True, max=True))

        # First operation
        for frame in xrange(start, end + 1):
            cmds.currentTime(frame, edit=True)
            # Export ...
        callback(33)
        time.sleep(1)

        # Second operation
        for frame in xrange(start, end + 1):
            cmds.currentTime(frame, edit=True)
            # Export ...
        callback(66)
        time.sleep(1)

        # Third operation
        for frame in xrange(start, end + 1):
            cmds.currentTime(frame, edit=True)
            # Export ...
        callback(100)
        time.sleep(1)


class View(QWidget):
    def __init__(self, controller):
        super(View, self).__init__()
        self.controller = controller
        self.thread = None
        
        self.setLayout(QVBoxLayout())
        
        self.progress = QLabel()
        self.layout().addWidget(self.progress)

        self.button = QPushButton('Do something')
        self.layout().addWidget(self.button)
        
        self.button.clicked.connect(self.do_something)
        
        self.show()
        
    def do_something(self):
        self.thread = DoSomethingThread(self.controller)
        self.thread.updated.connect(lambda progress: self.progress.setText(str(progress) + '%'))
        self.thread.run()
    
    
class DoSomethingThread(QThread):
    completed = Signal()
    updated = Signal(int)

    def __init__(self, controller, parent=None):
        super(DoSomethingThread, self).__init__(parent)
        self.controller = controller

    def run(self):
        self.controller.do_something(self.update_progress)
        self.completed.emit()
        
    def update_progress(self, progress):
        self.updated.emit(int(progress))
        
app = Application()

线程在Maya中很难正确使用Python(从列出的问题数量可以看出这一点here

通常有两个硬性规则需要遵守:

  1. 所有涉及 Maya 场景的工作(比如选择或移动对象)都必须在主线程中进行
  2. 所有涉及 Maya G 的工作UI 也必须在主线程中进行。

"main thread" here is the thread you get when you run a script from the listener, not on you're creating for yourself

这显然使很多事情变得困难。通常,解决方案将涉及主线程上的控制操作 运行,而其他不涉及 Maya GUI 或场景对象的工作在其他地方进行。线程安全容器(如 python Queue 可用于将完成的工作从工作线程移出到主线程可以安全到达的位置,或者您可以使用 QT 信号来在主线程中安全地触发工作....如果您的编程生涯不远,所有这些都会有点棘手。

好消息是——如果您想在 Maya 中完成的所有工作都在场景中,那么没有线程不会造成太大损失。除非工作基本上是非 Maya 工作——比如使用 HTTP 请求获取 Web 数据,或者将非 Maya 文件写入磁盘,或者其他不处理 Maya 特定数据的东西——添加线程不会”不会给您带来任何额外的性能。看起来你的例子正在推进时间线,做工作,然后尝试更新 PySide GUI。为此,您根本不需要线程(您也不需要单独的 QApplication——Maya 已经是 QApplication)

这是一个非常愚蠢的例子。

from PySide2.QtCore import *
from PySide2.QtGui import *
from PySide2.QtWidgets import *
import maya.cmds as cmds

class DumbWindow(QWidget):

    def __init__(self):
        super(DumbWindow, self).__init__()
        
        #get the maya app
        maya_app = QCoreApplication.instance()
        
        # find the main window for a parent
        for widget in maya_app.topLevelWidgets():
            if 'TmainWindow' in widget.metaObject().className():
                self.setParent(widget)
                break
                
        self.setWindowTitle("Hello World")
        self.setWindowFlags(Qt.Window)
        
        self.layout = QVBoxLayout()
        self.setLayout(self.layout)
        
        start_button = QPushButton('Start', self)
        stop_button = QPushButton('Stop', self)
        self.layout.addWidget(start_button)
        self.layout.addWidget(stop_button)
        
        self.should_cancel = False
        self.operation = None
        self.job = None

        # hook up the buttons
        start_button.clicked.connect(self.start)
        stop_button.clicked.connect(self.stop)


    def start(self):
        '''kicks off the work in 'this_is_the_work''' 
        self.operation = self.this_is_the_work()
        self.should_cancel = False
        self.job = cmds.scriptJob(ie=self.this_makes_it_tick)
            
        
    def stop(self):
        ''' cancel before the next step'''
        self.should_cancel = True

    def this_is_the_work(self):
        print "--- started ---"        
        for frame in range(100):
            cmds.currentTime(frame, edit=True)
            yield "advanced", frame
        
        print "--- DONE ----"

    def bail(self):
        self.operation = None
        def kill_my_job():
            cmds.scriptJob(k=self.job)
            print "job killed"
        
        cmds.scriptJob(ie = kill_my_job, runOnce=True)

    def this_makes_it_tick(self):
        '''
        this is called whenever Maya is idle and thie
        '''

        # not started yet
        if not self.operation:
            return

        # user asked to cancel
        if self.should_cancel:
            print "cancelling"
            self.bail()
            return            

        try:
            # do one step.  Here's where you can update the 
            # gui if you need to 
            result =   next(self.operation)
            print result
            # example GUI update
            self.setWindowTitle("frame %i" % result[-1])
        except StopIteration:
            # no more stpes, we're done
            print "completed"
            self.bail()
        except Exception as e:
            print "oops", e
            self.bail()
 
         

test = DumbWindow()
test.show()

点击 start 会创建一个 maya scriptJob,它将尝试 运行 函数 this_is_the_work() 中的任何操作。它将 运行 下一个 yield 语句,然后检查以确保用户没有要求取消作业。在 yield 之间 Maya 会很忙(就像您在侦听器中输入一些行一样)但是如果您在 yield 出现时与 Maya 交互,脚本将等待您。这允许在没有单独线程的情况下进行安全的用户交互,当然它也不像完全独立的线程那样流畅。

您会注意到,这会在 bail() 方法中启动第二个 scriptJob -- 那是因为 scriptJob 无法自行终止,所以我们创建另一个将 运行下一个空闲事件并杀死我们不想要的那个。

这个技巧基本上是大多数基于 MEL 的 Maya UI 在幕后工作的方式——如果你 运行 cmds.scriptJob(lj=True) 在听众中你通常会看到很多表示跟踪事物的 UI 元素的 scriptJobs。