I'm creating a PySide2 tool running in Maya. The tool is executing a lot of long tasks, some modifying the scene (cleaning tasks), some creating files (exporting tasks).
Because this is a long task, I'd like to display feedback (progress bar) while it's running.
Here is a simplified bit of code showing where I am so far. Is this the right way to use QThread? I'm from a CG Artist background, not a professional programmer, so I'm probably misusing or misunderstanding the concepts I'm trying to use (threads, 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()
Threads are difficult to use correctly in Maya Python (you can see this from the number of questions listed here)
Generally there are two hard rules to observe:
"main thread" here is the thread you get when you run a script from the listener, not on you're creating for yourself
This obviously makes a lot of things hard to do. Generally a solution will involve the a controlling operation running on the main thread while other work that does not touch Maya GUI or scene objects is happening elsewhere. A thread-safe container (like a python Queue can be used to move completed work out of a worker thread into a place where the main thread can get to it safely, or you can use QT signals to safely trigger work in the main thread.... all of which is a bit tricky if you're not far along in your programming career.
The good news is -- if all the work you want to do in Maya is in the scene you aren't losing much by not having threads. Unless the work is basically non-Maya work -- like grabbing data of the web using an HTTP request, or writing a non-Maya file to disk, or something else that does not deal with Maya-specific data -- adding threads won't get you any additional performance. It looks like your example is advancing the time line, doing work, and then trying to update a PySide GUI. For that you don't really need threads at all (you also don't need a separate QApplication -- Maya is already a QApplication)
Here's a really dumb example.
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()
Hitting start
creates a maya scriptJob that will try to run whatever operation is in the function called this_is_the_work()
. It will run to the next yield
statement and then check to make sure the user hasn't asked to cancel the job. Between yields Maya will be busy (just as it would if you entered some lines in the listener) but if you're interacting with Maya when a yield comes up, the script will wait for you instead. This allows for safe user interaction without a separate thread, though of course it's not as smooth as a completely separate thread either.
You'll notice that this kicks off a second scriptJob in the bail()
method -- that's because a scriptJob can't kill itself, so we create another one which will run during the next idle event and kill the one we don't want.
This trick is basically how most of the Maya's MEL-based UI works under the hood -- if you run cmds.scriptJob(lj=True)
in the listener you'll usually see a lot of scriptJobs that represent UI elements keeping track of things.