在场景中执行修改并从 Maya 中的 QThread 更新自定义 window
Perform modifications in the scene and update custom window from a QThread in Maya
上下文
我正在 Maya 中创建 PySide2 工具 运行。该工具正在执行很多长任务,一些修改场景(清理任务),一些创建文件(导出任务)。
因为这是一项长期任务,我想在 运行 时显示反馈(进度条)。
问题
- 不幸的是,到目前为止,整个UI在执行过程中似乎没有更新。
- 另外,因为我在实际代码中有奇怪的行为(Maya 永远冻结),我猜这不是线程的安全使用。
示例代码
这是一段简化的代码,显示了我目前的进展情况。这是使用 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)
通常有两个硬性规则需要遵守:
- 所有涉及 Maya 场景的工作(比如选择或移动对象)都必须在主线程中进行
- 所有涉及 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。
上下文
我正在 Maya 中创建 PySide2 工具 运行。该工具正在执行很多长任务,一些修改场景(清理任务),一些创建文件(导出任务)。
因为这是一项长期任务,我想在 运行 时显示反馈(进度条)。
问题
- 不幸的是,到目前为止,整个UI在执行过程中似乎没有更新。
- 另外,因为我在实际代码中有奇怪的行为(Maya 永远冻结),我猜这不是线程的安全使用。
示例代码
这是一段简化的代码,显示了我目前的进展情况。这是使用 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)
通常有两个硬性规则需要遵守:
- 所有涉及 Maya 场景的工作(比如选择或移动对象)都必须在主线程中进行
- 所有涉及 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。