deleting workspaceControl/running uiScript, maya/pyside - python

I’m writing a thing in maya and have run in to trouble. Really don’t know what I did, I was going to adress something else when this happened, the last thing I did was add a button to the layout.
I have been messing around with this for a long time now and as far as I can tell, the uiScript flag doesn’t like arguments passed in the method call…? what happens is, it never sets the restore flag to True so that bit never hits. resulting in it spawning multiple windows in maya. I’m also trying to figure out where the cmds.deleteUI try clause should go, not entrirely sure where I had it before this happened. If anyone could offer any insight I would be most grateful, cheers /S
in the script below, I have replaced an instance of my ui with just a button, it makes no difference on the behaviour.
code:
//////////
from PySide2 import QtWidgets, QtCore
from maya.app.general.mayaMixin import MayaQWidgetDockableMixin
import maya.OpenMayaUI as mui
import maya.cmds as cmds
import weakref
if not 'customMixinWindow' in globals():
customMixinWindow = None
class DockableWidget(MayaQWidgetDockableMixin, QtWidgets.QWidget):
instances = list()
CONTROL_NAME = 'customMixinWindow'
def __init__(self, parent=None):
super(DockableWidget, self).__init__(parent=parent)
DockableWidget.delete_instances()
self.__class__.instances.append(weakref.proxy(self))
self.main_layout = QtWidgets.QVBoxLayout()
self.button = QtWidgets.QPushButton()
self.main_layout.addWidget(self.button)
self.setLayout(self.main_layout)
self.setAttribute(QtCore.Qt.WA_DeleteOnClose)
#staticmethod
def delete_instances():
print "deleting"
for ins in DockableWidget.instances:
try:
ins.setParent(None)
ins.deleteLater()
except:
pass
DockableWidget.instances.remove(ins)
del ins
def DockableWidgetUIScript(restore=False):
global customMixinWindow
if restore == True:
restoredControl = mui.MQtUtil.getCurrentParent()
customMixinWindow = DockableWidget()
if customMixinWindow is None:
#customMixinWindow = DockableWidget()
customMixinWindow.setObjectName('customMayaMixinWindow')
if restore == True:
mixinPtr = mui.MQtUtil.findControl(customMixinWindow.objectName())
mui.MQtUtil.addWidgetToMayaLayout(long(mixinPtr), long(restoredControl))
else:
try:
cmds.workspaceControl('customMayaMixinWindowWorkspaceControl', e=True, close=True)
cmds.deleteUI('customMayaMixinWindowWorkspaceControl')
except:
pass
customMixinWindow.show(dockable=True, restore=True, height=400, width=400, uiScript='import dockWin; dockWin.DockableWidgetUIScript(restore=True)')
def main():
ui = DockableWidgetUIScript()
return ui
if __name__ == 'dockWin':
main()

ok, problem solved.
after a lot of troubleshooting, it turned out to be a QSpacerItem in my ui that caused the window to open once and then crash maya the second time. which made me think it had something to do with the deleteUI stuff.
I had the QSpacerItem added like so:
self.spacerFive = QtWidgets.QSpacerItem(5 , 5)
self.myLayout.addSpacerItem(self.spacerFive)
this made maya crash out completely when deleteing the UI/workspaceControl…
no idea why, garbage collection?
this fixed it:
self.myLayout.addSpacerItem(QtWidgets.QSpacerItem(5, 5))
alright, everything working as expected again. cheers /S

Related

Issue when executing code snippet with "exec" and inheritance

I'm having some issues when trying to execute a string/file from within a QPlainTextEdit, it appears to be some sort of scoping issues. What happens is that when the code EXECUTABLE_STRING is run from the global scope, it works fine. However, when it is run from a local scope, such as through the AbstractPythonCodeWidget, it either can't find the object to do inheritance TypeError: super(type, obj): obj must be an instance or subtype of type or runs into a name error NameError: name 'Test' is not defined. Which oddly changes based on whether or not the exec(EXECUTABLE_STRING) line is commented/uncommented when run. Any help would be greatly appreciated.
import sys
from PyQt5.QtWidgets import QApplication, QPlainTextEdit
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QCursor
app = QApplication(sys.argv)
EXECUTABLE_STRING = """
from PyQt5.QtWidgets import QLabel, QApplication
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QCursor
class Test(QLabel):
def __init__(self, parent=None):
super(Test, self).__init__(parent)
self.setText("Test")
a = Test()
a.show()
a.move(QCursor.pos())
"""
class AbstractPythonCodeWidget(QPlainTextEdit):
def __init__(self, parent=None):
super(AbstractPythonCodeWidget, self).__init__(parent)
self.setPlainText(EXECUTABLE_STRING)
def keyPressEvent(self, event):
if event.modifiers() == Qt.ControlModifier:
if event.key() == Qt.Key_Return:
# this does not work
#exec(compile(self.toPlainText(), "script", "exec"), globals(), locals())
exec(self.toPlainText())
return QPlainTextEdit.keyPressEvent(self, event)
w = AbstractPythonCodeWidget()
w.show()
w.move(QCursor.pos())
w.resize(512, 512)
# this works when run here, but not when run on the keypress event
# exec(EXECUTABLE_STRING)
sys.exit(app.exec_())
First of all, running exec based on user input can be a security issue, but most importantly usually leads to fatal crash unless lots of precautions are taken, since you're using the same interpreter for both your program and the user code: basically, if the code of the user fails, your program fails, but that's not the only problem.
The reason for which your code doesn't run properly is actually a bit complex, and it's related to the scope of the class name, which becomes a bit complex when running exec along with super().[1]
An interesting aspect is that if you remove the arguments of super (and you should, since Python 3), the program won't raise any error.
But that won't be enough: a is a local variable, and it will be garbage collected as soon as exec is finished, and since the label is assigned to that variable, it will be destroyed along with it.
A possible solution would be to make the reference persistent, for example by assigning it to self (since self exists in the scope of the executed script). This is a working example of the EXECUTABLE_STRING:
from PyQt5.QtWidgets import QLabel
from PyQt5.QtGui import QCursor
class Test(QLabel):
def __init__(self, parent=None):
super().__init__(parent)
self.setText("Test")
self.a = Test()
self.a.show()
self.a.move(QCursor.pos())
As you can see, we don't need to import everything again anymore, we only import QLabel, since it wasn't imported in the main script, but everything else already exists in the scope of the script, including the current QApplication instance.
That said, all the above is only for knowledge purposes, as you should NOT use exec to run user code.
For instance, try to paste the following in the text edit, and run it:
self.document().setHtml('This should <b>NOT</b> happen!!!<br/><br/>Bye!')
self.setReadOnly(True)
from PyQt5.QtCore import QTimer
from PyQt5.QtWidgets import QApplication
QTimer.singleShot(2000, QApplication.quit)
As you can see, not only the above code is able to change the input, but also take complete control of the whole application.
You could prevent that by calling a function that would basically limit the scope of the execution:
def runCode(code):
try:
exec(code)
except Exception as e:
return e
class AbstractPythonCodeWidget(QPlainTextEdit):
# ...
def keyPressEvent(self, event):
if event.modifiers() == Qt.ControlModifier:
if event.key() == Qt.Key_Return:
error = runCode(self.toPlainText())
if error:
QMessageBox.critical(self, 'Script crash!', str(error))
return QPlainTextEdit.keyPressEvent(self, event)
But that's just because no self is involved: you could still use w.a = Test() with the example above.
So, if you want to run user made scripts in your program, exec is probably not an acceptable solution, unless you take enough precautions.
If you don't need direct interaction between your program and the user script, a possibility could be to use the subprocess module, and run the script with another python interpreter instance.
[1] If anybody has a valid resource/answer that might shed some light on the topic, please comment.
Found a similar issue that goes into more depth about how the scope works with Globals/Locals in exec here:
globals and locals in python exec()
Don't want to copy/paste the entire thread, but the answer that worked for me in this post was:
d = dict(locals(), **globals())
exec (code, d, d)

Load a web page

I am trying to load a web page using PySide's QtWebKit module. According to the documentation (Elements of QWebView; QWebFrame::toHtml()), the following script should print the HTML of the Google Search Page:
from PySide import QtCore
from PySide import QtGui
from PySide import QtWebKit
# Needed if we want to display the webpage in a widget.
app = QtGui.QApplication([])
view = QtWebKit.QWebView(None)
view.setUrl(QtCore.QUrl("http://www.google.com/"))
frame = view.page().mainFrame()
print(frame.toHtml())
But alas it does not. All that is printed is the method's equivalent of a null response:
<html><head></head><body></body></html>
So I took a closer look at the setUrl documentation:
The view remains the same until enough data has arrived to display the new url.
This made me think that maybe I was calling the toHtml() method too soon, before a response has been received from the server. So I wrote a class that overrides the setUrl method, blocking until the loadFinished signal is triggered:
import time
class View(QtWebKit.QWebView):
def __init__(self, *args, **kwargs):
super(View, self).__init__(*args, **kwargs)
self.completed = True
self.loadFinished.connect(self.setCompleted)
def setCompleted(self):
self.completed = True
def setUrl(self, url):
self.completed = False
super(View, self).setUrl(url)
while not self.completed:
time.sleep(0.2)
view = View(None)
view.setUrl(QtCore.QUrl("http://www.google.com/"))
frame = view.page().mainFrame()
print(frame.toHtml())
That made no difference at all. What am I missing here?
EDIT: Merely getting the HTML of a page is not my end game here. This is a simplified example of code that was not working the way I expected it to. Credit to Oleh for suggesting replacing time.sleep() with app.processEvents()
Copied from my other answer:
from PySide.QtCore import QObject, QUrl, Slot
from PySide.QtGui import QApplication
from PySide.QtWebKit import QWebPage, QWebSettings
qapp = QApplication([])
def load_source(url):
page = QWebPage()
page.settings().setAttribute(QWebSettings.AutoLoadImages, False)
page.mainFrame().setUrl(QUrl(url))
class State(QObject):
src = None
finished = False
#Slot()
def loaded(self, success=True):
self.finished = True
if self.src is None:
self.src = page.mainFrame().toHtml()
state = State()
# Optional; reacts to DOM ready, which happens before a full load
def js():
page.mainFrame().addToJavaScriptWindowObject('qstate$', state)
page.mainFrame().evaluateJavaScript('''
document.addEventListener('DOMContentLoaded', qstate$.loaded);
''')
page.mainFrame().javaScriptWindowObjectCleared.connect(js)
page.mainFrame().loadFinished.connect(state.loaded)
while not state.finished:
qapp.processEvents()
return state.src
load_source downloads the data from an URL and returns the HTML after modification by WebKit. It wraps Qt's event loop with its asynchronous events, and is a blocking function.
But you really should think what you're doing. Do you actually need to invoke the engine and get the modified HTML? If you just want to download HTML of some webpage, there are much, much simpler ways to do this.
Now, the problem with the code in your answer is you don't let Qt do anything. There is no magic happening, no code running in background. Qt is based on an event loop, and you never let it enter that loop. This is usually achieved by calling QApplication.exec_ or with a workaround processEvents as shown in my code. You can replace time.sleep(0.2) with app.processEvents() and it might just work.

Qt Signals/Slots in threaded Python

I'm having troubles using PyQt4 slots/signals.
I'm using PyLIRC and I'm listening for button presses on a remote. This part I have gotten to work outside of Qt. My problem comes when emitting the signal from the button listening thread and attempting to call a slot in the main thread.
My button listener is a QObject initialized like so:
buttonPressed = pyqtSignal(int)
def __init__(self):
super(ButtonEvent, self).__init__()
self.buttonPressed.connect(self.onButtonPressed)
def run(self):
print 'running'
while(self._isListening):
s = pylirc.nextcode()
if (s):
print 'emitting'
self.buttonPressed.emit(int(s[0]))
The onButtonPressed slot is internal to the button listener for testing purposes.
To move the button listener to another thread to do the work, I use the following:
event = ButtonEvent()
eventThread = QThread()
event.moveToThread(eventThread)
eventThread.started.connect(event.run)
Then in the main thread, I have my VideoTableController class that contains the slot in the main thread that doesn't get called. Inside of __init__ I have this:
class VideoTableController(QObject):
def __init__(self, buttonEvent):
buttonEvent.buttonPressed.connect(self.onButtonPressed)
Where onButtonPressed in this case is:
#pyqtSlot(int)
def onButtonPressed(self, bid):
print 'handling button press'
if bid not in listenButtons: return
{ ButtonEnum.KEY_LEFT : self.handleBack,
#...
So when I start the event thread, it starts listening properly. When I press a button on the remote, the onButtonPressed slot internal to the ButtonEvent class is properly called, but the slot within VideoTableController, which resides in the main thread, is not called. I started my listening thread after connecting the slot to the signal, and I tested doing it the other way around, but to no avail.
I have looked around, but I haven't been able to find anything. I changed over to using QObject after reading You're doing it wrong. Any help with this is greatly appreciated. Let me know if you need anything else.
EDIT: Thanks for the responses! Here is a big chunk of code for you guys:
ButtonEvent (This class uses singleton pattern, excuse the poor coding because I'm somewhat new to this territory of Python also):
import pylirc
from PyQt4.QtCore import QObject, pyqtSignal, QThread, pyqtSlot
from PyQt4 import QtCore
class ButtonEvent(QObject):
"""
A class used for firing button events
"""
_instance = None
_blocking = 0
_isListening = False
buttonPressed = pyqtSignal(int)
def __new__(cls, configFileName="~/.lircrc", blocking=0, *args, **kwargs):
if not cls._instance:
cls._instance = super(ButtonEvent, cls).__new__(cls, args, kwargs)
cls._blocking = blocking
if not pylirc.init("irexec", configFileName, blocking):
raise RuntimeError("Problem initilizing PyLIRC")
cls._isListening = True
return cls._instance
def __init__(self):
"""
Creates an instance of the ButtonEvent class
"""
super(ButtonEvent, self).__init__()
self.buttonPressed.connect(self.button)
### init
def run(self):
print 'running'
while(self._isListening):
s = pylirc.nextcode()
if (s):
print 'emitting'
self.buttonPressed.emit(int(s[0]))
def stopListening(self):
print 'stopping'
self._isListening = False
#pyqtSlot(int)
def button(self, bid):
print 'Got ' + str(bid)
def setupAndConnectButtonEvent(configFileName="~/.lircrc", blocking=0):
"""
Initializes the ButtonEvent and puts it on a QThread.
Returns the QThread it is running on.
Does not start the thread
"""
event = ButtonEvent().__new__(ButtonEvent, configFileName, blocking)
eventThread = QThread()
event.moveToThread(eventThread)
eventThread.started.connect(event.run)
return eventThread
Here is the VideoTableController:
from ControllerBase import ControllerBase
from ButtonEnum import ButtonEnum
from ButtonEvent import ButtonEvent
from PyQt4.QtCore import pyqtSlot
from PyQt4 import QtCore
class VideoTableController(ControllerBase):
listenButtons = [ ButtonEnum.KEY_LEFT,
ButtonEnum.KEY_UP,
ButtonEnum.KEY_OK,
ButtonEnum.KEY_RIGHT,
ButtonEnum.KEY_DOWN,
ButtonEnum.KEY_BACK ]
def __init__(self, model, view, parent=None):
super(VideoTableController, self).__init__(model, view, parent)
self._currentRow = 0
buttonEvent = ButtonEvent()
buttonEvent.buttonPressed.connect(self.onButtonPressed)
self.selectRow(self._currentRow)
#pyqtSlot(int)
def onButtonPressed(self, bid):
print 'handling button press'
if bid not in listenButtons: return
{ ButtonEnum.KEY_LEFT : self.handleBack,
ButtonEnum.KEY_UP : self.handleUp,
ButtonEnum.KEY_OK : self.handleOk,
ButtonEnum.KEY_RIGHT : self.handleRight,
ButtonEnum.KEY_DOWN : self.handleDown,
ButtonEnum.KEY_BACK : self.handleBack,
}.get(bid, None)()
And here is my startup script:
import sys
from PyQt4 import QtCore, QtGui
from ui_main import Ui_MainWindow
from VideoTableModel import VideoTableModel
from VideoTableController import VideoTableController
from ButtonEvent import *
class Main(QtGui.QMainWindow):
def __init__(self, parent=None):
QtGui.QWidget.__init__(self, parent)
self.ui = Ui_MainWindow()
self.ui.setupUi(self)
self.buttonEvent = ButtonEvent()
self.bEventThread = setupAndConnectButtonEvent()
model = VideoTableModel("/home/user/Videos")
self.ui.videoView.setModel(model)
controller = VideoTableController(model, self.ui.videoView)
self.bEventThread.start()
def closeEvent(self, event):
self.buttonEvent.stopListening()
self.bEventThread.quit()
event.accept()
if __name__ == '__main__':
app = QtGui.QApplication(sys.argv)
buttonEvent = ButtonEvent()
myapp = Main()
myapp.show()
sys.exit(app.exec_())
It turns out I was just making a foolish Python mistake. The signal was being emitted correctly, and the event loop was running properly in all threads. My problem was that in my Main.__init__ function I made a VideoTableController object, but I did not keep a copy in Main, so my controller did not persist, meaning the slot also left. When changing it to
self.controller = VideoTableController(model, self.ui.videoView)
Everything stayed around and the slots were called properly.
Moral of the story: it's not always a misuse of the library, it may be a misuse of the language.
It seems that the quickest workaround would be change your ButtonEvent code here:
...
def run(self):
print 'running'
while(self._isListening):
s = pylirc.nextcode()
if (s):
print 'emitting'
self.buttonPressed.emit(int(s[0]))
...
to this:
#pyqtSlot()
def run(self):
print 'running'
while(self._isListening):
s = pylirc.nextcode()
if (s):
print 'emitting'
self.buttonPressed.emit(int(s[0]))
The short explanation to this issue is that PyQt uses a proxy internally, and this way you can make sure to avoid that. After all, your method is supposed to be a slot based on the connect statement.
Right... Now, I would encourage you to give some consideration for your current software design though. It seems that you are using a class in a dedicated thread for handling Qt button events. It may be good idea, I am not sure, but I have not seen this before at least.
I think you could get rid of that class altogether in the future with a better approach where you connect from the push button signals directly to your handler slot. That would not be the run "slot" in your dedicated thread, however, but the cannonical handler.
It is not a good design practice to introduce more complexity, especially in multi-threaded applications, than needed. Hope this helps.
I haven't actually tested this (because I don't have access to your compiled UI file), but I'm fairly certain I'm right.
Your run method of your ButtonEvent (which is supposed to be running in a thread) is likely running in the mainthread (you can test this by importing the python threading module and adding the line print threading.current_thread().name. To solve this, decorate your run method with #pyqtSlot()
If that doesn't solve it, add the above print statement to various places until you find something running in the main thread that shouldn't be. The lined SO answer below will likely contain the answer to fix it.
For more details, see this answer: https://stackoverflow.com/a/20818401/1994235

Timer in a Child Thread in PySide

First off, I'm very new to Python and Pyside. In order to do a bit of self-improvement, I'm trying to get a QTimer to execute every second in a child thread of my PySide program (at the moment I just want it to print "hi!" to a terminal every second without freezing the main window).
I tried converting the example I found on the Qt Wiki from C++ to Python/PySide, but since I don't really know C++ I assume I converted it incorrectly and that's why it's not working properly.
At the moment, the doWork() function only seems to execute once and then never again. What am I doing wrong? Is there a better way to execute a function every second in PySide without freezing the main window?
Here's the code (I have removed some main window code to increase clarity):
from PySide import QtGui
from PySide import QtCore
from client_gui import Ui_MainWindow
statsThread = QtCore.QThread()
class MainWindow(QtGui.QMainWindow, Ui_MainWindow):
def __init__(self, parent=None):
super(MainWindow, self).__init__(parent)
#setup GUI
self.setupUi(self)
#start thread to update GUI
self.statsThread = updateStatsThread()
self.statsThread.start(QtCore.QThread.TimeCriticalPriority)
class updateGuiWithStats(QtCore.QObject):
def Worker(self):
timer = QtCore.QTimer()
timer.timeout.connect(self.doWork())
timer.start(1000)
def doWork(self):
print "hi!"
class updateStatsThread (QtCore.QThread):
def run(self):
updater = updateGuiWithStats()
updater.Worker()
self.exec_()
if __name__ == '__main__':
app = QtGui.QApplication(sys.argv)
frame = MainWindow()
frame.show()
sys.exit(app.exec_())
#Masci already pointed out the fix you needed for your timer.timeout.connect, but I see more issues than just that.
No need to create a global QThread that is never used:
statsThread = QtCore.QThread()
Your QTimer is being garbage collected right away because its created without a parent, and you aren't saving it within your class. This is why even after you fix your timer connection, it will still not work... Try:
class UpdateGuiWithStats(QtCore.QObject):
def startWorker(self):
self.timer = QtCore.QTimer()
self.timer.timeout.connect(self.doWork)
self.timer.start(1000)
Also, use UpperCase for the first letter of classes, and camelCase for methods. You are doing a mixture of both ways.
A couple of notes based on that link you provided, your example, and other comments on here... You can use just a QTimer as a solution if your doWork() is very light and will not block your main event loop with a bunch of data crunching, sleeping, etc. If it does, then doWork() will need to be moved to a QThread, as your example is doing. But at that point it is somewhat unnecessary to use an event loop, and a QTimer in a separate class that calls its own work. This all could be consolidated into a single class, something like:
class UpdateStatsThread(QtCore.QThread):
def __init__(self, parent=None):
super(UpdateStatsThread, self).__init__(parent)
self._running = False
def run(self):
self._running = True
while self._running:
self.doWork()
self.msleep(1000)
def stop(self, wait=False):
self._running = False
if wait:
self.wait()
def doWork(self):
print "hi!"
in updateGuiWithStats class, Worker method:
timer.timeout.connect(self.doWork())
should be
timer.timeout.connect(self.doWork)
You are connecting timeout signal to None (the return value of doWork() method), and I think this is why it is executed only once: doWork is called during the connection and nomore. When you make connections, remember to connect the function name (in Pythonics words, the callable object) and not the function call.
By the way, even if the above solved your problem, you should avoid using threads since QTimer already does by its own you need. In the docs you linked, the first answer to the When shouldn’t I use threads? question is: Timers.

pyQT QNetworkManager and ProgressBars

I'm trying to code something that downloads a file from a webserver and saves it, showing the download progress in a QProgressBar.
Now, there are ways to do this in regular Python and it's easy. Problem is that it locks the refresh of the progressBar. Solution is to use PyQT's QNetworkManager class. I can download stuff just fine with it, I just can't get the setup to show the progress on the progressBar. Here´s an example:
class Form(QDialog):
def __init__(self,parent=None):
super(Form,self).__init__(parent)
self.progressBar = QProgressBar()
self.reply = None
layout = QHBoxLayout()
layout.addWidget(self.progressBar)
self.setLayout(layout)
self.manager = QNetworkAccessManager(self)
self.connect(self.manager,SIGNAL("finished(QNetworkReply*)"),self.replyFinished)
self.Down()
def Down(self):
address = QUrl("http://stackoverflow.com") #URL from the remote file.
self.manager.get(QNetworkRequest(address))
def replyFinished(self, reply):
self.connect(reply,SIGNAL("downloadProgress(int,int)"),self.progressBar, SLOT("setValue(int)"))
self.reply = reply
self.progressBar.setMaximum(reply.size())
alltext = self.reply.readAll()
#print alltext
#print alltext
def updateBar(self, read,total):
print "read", read
print "total",total
#self.progressBar.setMinimum(0)
#self.progressBar.setMask(total)
#self.progressBar.setValue(read)
In this case, my method "updateBar" is never called... any ideas?
Well you haven't connected any of the signals to your updateBar() method.
change
def replyFinished(self, reply):
self.connect(reply,SIGNAL("downloadProgress(int,int)"),self.progressBar, SLOT("setValue(int)"))
to
def replyFinished(self, reply):
self.connect(reply,SIGNAL("downloadProgress(int,int)"),self.updateBar)
Note that in Python you don't have to explicitly use the SLOT() syntax; you can just pass the reference to your method or function.
Update:
I just wanted to point out that if you want to use a Progress bar in any situation where your GUI locks up during processing, one solution is to run your processing code in another thread so your GUI receives repaint events. Consider reading about the QThread class, in case you come across another reason for a progress bar that does not have a pre-built solution for you.

Categories

Resources