Why use PyQt/PySide at all in Maya?
- A larger array of available GUI elements to work with.
- A visual editor(Qt Designer).
- Object Oriented design.
- Signal/Slot mechanism, combined with an event driven system.
- Access to low level components such as the base QWidget, QObject, QAction, and QPainter.
- Much more! We are going to go over many things that are simply impossible within the context of Maya’s default toolkit.
Some examples of cool things!
A proper tree widget, and full control over it.
Mounds of random GUI’s as you can see, icon support in PyQt is just about everywhere versus Maya UI where only a few specific controls support images (And never dynamic resizing of them).
PyQt/PySide, lets get this out of the way…
- PyQt and PySide are nearly identical as of right now. PySide is newer and is being sponsored and partially developed by the people who make Qt itself, Nokia.
- PyQt website (Made by Riverbank software)
- PySide website (Made by Nokia)
- For this article I will be covering primarily PyQt, but PySide is pretty much the same so just swap PyQt for PySide and you should be fine.
- PySide is available as LGPL as well as GPL. PyQt is GPL only.
- I will make a note if something is specifically different between the two.
Setting up PyQt
Hopefully by now you are saying “Gee, that sounds great, but how do I get it installed and working!”, fear no more! I am also going to go over some basic deployment information, in case you want to roll out PyQt to your team.
Setting up PyQt – Getting it!
Here are the versions of Maya, and their corresponding installers for PyQt. All have been tested in production env. and are verified working. Download the correct installer for your version of Maya below. (If you are on OS-X/Linux you will have to acquire these yourself sadly. If anyone wants to do a write up and provide links let me know.) Credit goes to Robert Kist (http://blarg.robertkist.com/) for the 2012 X86 build. Thanks Robert!
Setting up PyQt – Installing it
I have always found that it’s easiest to install new packages into a clean python install before extracting them out into their own folder, or moving them into Maya’s site-packages directory. Here’s the installers for the versions of Python that Maya uses (And that the above PyQt installers will be looking for).
Now install PyQt into that clean Python install.
Setting up PyQt – Making Maya aware of the PyQt and sip packages
There are multiple ways of doing this, and which works best depends on how your pipeline/deployment works. There are probably more ways as well, but these are the ones that I know of.
The last two of these, the .pth method and the site.addsitedir method are both great for deploying PyQt via a network location. With these you can just drop a single copy somewhere everyone has access to, and then during Maya startup (Or python in general with site-customize) add the correct path and voila PyQt is working.
Copying to Maya’s site packages
- Probably the easiest method for a single user who only wants to use PyQt from inside Maya.
- Has to be done per-user and involves having access to a copy of the files to copy over.
- Updates are somewhat difficult only because they need to be done per user (Unless you have a versioned Maya install)
- Copy the PyQt4 folder, and the sip.pyd file from your python26/lib/site-packages directory into the Maya install /python/lib/site-packages directory.
Creating a .pth file
- Still requires a file per-user, but not a full copy of the packages
- Updates are easy, just update the network/Perforced location
- Extremely simple file, and can be created after python startup but before pyqt import if need be.
- Create an empty text file in your Maya install /python/lib/site-packages directory and rename it to something like pyqt.pth
- Open that file in a text editor and paste in the full path to your PyQt4/sip site-packages folder ( python26/lib/site-packages)
- No files in the users Maya directory
- Requires code to be run before importing PyQt (Can easily be done in sitecustomize.py, or userSetup.py, or at head of script)
- Before trying to import PyQt or sip, use something like the following code
- import site
Setting up PyQt – Test it!
In Maya, try the following code
import sip sip.setapi('QString', 2) sip.setapi('QVariant', 2) from PyQt4 import QtGui, QtCore btn = QtGui.QPushButton(QtGui.__file__) btn.show()
If you don’t get any errors and a button is created (Not parented to Maya though) then it worked! Otherwise, leave me a comment with the error message from Maya, and I will try to help you out.
A quick note on sip
PySide uses the equivalent of the PyQt version 2 API, while PyQt has two versions that can be picked via sip.setapi(). To maintain code compatibility between the two and for the benefit of the improved API you will see “sip.setapi(‘blah’, 2) on occasion. sip only permits the API to be set before PyQt is loaded, so be aware that you cannot change API’s mid-run. Maya MUST be restarted for code using the alternate API to be run, and only if the api version is set properly before PyQt is imported. For this reason, try to pick an API version and stick with it (I highly recommend version 2…)
#Source code for some common Maya/PyQt functions we will be using import sip sip.setapi('QString', 2) sip.setapi('QVariant', 2) from PyQt4 import QtGui, QtCore import maya.OpenMayaUI as apiUI def getMayaWindow(): """ Get the main Maya window as a QtGui.QMainWindow instance @return: QtGui.QMainWindow instance of the top level Maya windows """ ptr = apiUI.MQtUtil.mainWindow() if ptr is not None: return sip.wrapinstance(long(ptr), QtCore.QObject) def toQtObject(mayaName): """ Convert a Maya ui path to a Qt object @param mayaName: Maya UI Path to convert (Ex: "scriptEditorPanel1Window|TearOffPane|scriptEditorPanel1|testButton" ) @return: PyQt representation of that object """ ptr = apiUI.MQtUtil.findControl(mayaName) if ptr is None: ptr = apiUI.MQtUtil.findLayout(mayaName) if ptr is None: ptr = apiUI.MQtUtil.findMenuItem(mayaName) if ptr is not None: return sip.wrapinstance(long(ptr), QtCore.QObject)
PyQt and Maya – Parenting to the Maya Window
The process for parenting your window to the main Maya window is fairly simple. First you get the main Maya window as a QMainWindow instance using the getMayaWindow() function and then pass that instance in as the parent object for your new top level PyQt object.
PyQt and Maya – Parenting to a specific Maya Widget
Similar to the main Maya window, here the path for our Maya UI control is passed to the toQtObject() function to get a PyQt object. Next just parent the new object to the Maya PyQt one. The main issue here is going to be that Maya uses “invisible” layouts to control how objects are placed. I would generally recommend avoiding this use scenario unless absolutely necessary, navigating the Maya widget hierarchy can be a pain. Look for info on these invisible widgets in the Maya API docs for MScriptUtil, it shows how to query the internal Qt structure for the Maya widgets.
PyQt and Maya – Parenting a Maya widget to PyQt
First convert the Maya UI control name to a PyQt object then call .setParent() on that object and provide the new parent widget. You will probably need to assign it a layout in PyQt as well, depending on the structure of your UI.
PyQt and Maya – Customizing a Maya widget with PyQt
Convert your Maya UI control to a PyQt object. Customize as you normally would for that object type (Ex: assign a Qt style sheet, or add an image to a QPushButton)
PyQt general – Garbage collection
Python will garbage collect any locally scoped objects when a function has finished executing if their reference count drops to zero. The effect on us is that a window declared in a function will be immediately destroyed when that function ends, if that window is not stored elsewhere. The two most common ways to store it are either by declaring the window variable global, or by assigning it into the __main__ module (import __main__).
Qt Designer and you!
Why use Qt Designer?
- Esablishing layouts and getting everything properly positioned is more easily achieved visually (IMO)
- The designer files offload what is normally the largest portion of your code to a seperate file.
- XML is nice and easy to parse, and modify at run-time.
- It’s possible for non-programmers to tweak UI’s to some degree. (Customize icon sets, change tooltips, etc..)
- Utilize the Qt resource system (Although some modifications are necesarry at run-time or as a “build” step depending.
Pre-compiled designer files versus run-time
- Both PyQt and PySide have a utility for generating a python file based on the designer .ui file.
- Both PyQt and PySide have the ability to parse and convert the designer file to python objects on the fly.
- Unless you are having a huge hit startup time wise for your UI (Due to the parse step of the UI xml data) I would highly recommend converting on the fly.
- On the fly is much more dynamic, and you wont have to constantly maintain the intermediate .py file.
- Also there’s still an import step involved for the .py file, so it’s not going to save a substantial amount of time in most cases.
- If the UI file is load on the fly, it’s easy to modify the incoming xml file before loading it. This is most useful for finding resource paths and either compiling them, or handling them in a custom way.
How to load a designer file
- DO NOT USE cmds.loadUi!!!
- seriously, don’t.. forget it exists, now.
- Who ever wrote it needs to be punched in the mouth.
- Use uic/pysideuic (PySide has an alternate setup here), it’s awesome!
- Note: pysideuic is lacking a loadUiType function, I have a workaround that I will try to mention in the example code. (Edit: here’s some PySide loadUiType code)
- The uic.loadUiType function is the one that should be used for loading in a PyQt UI from a designer file.
- Depending on how your resources are set up you may wish to alter the UI file before passing it to the loadUiType command. (See resources section below)
from PyQt4 import uic form_class, base_class = uic.loadUiType('path/to/uifile.ui')
The form_class returned is your customized python class (Based on object) and base_class is the super class of the window (Ex: QMainWindow, QDialog, etc..). Now to begin using them simply subclass from both (multiple inheritance). In your __init__ method you need to use super() to call the proper parent __init__ functions, and then setupUi() on self to instantiate the designer widgets onto self. One important thing to note here, is that the form_class and base_class attributes need to be unique variables for each UI at the top level, or you will have issues when you go to create your window instances.
class Window(form_class, base_class): def __init__(self, parent=None): super(Window, self).__init__(parent) self.setupUi(self)
Here’s a more specific example showing how to use a designer file as a base, and extend it in code. We are going to create a simple list, where items can be added, and removed via a button. The layout portion of this is defined in Qt designer, which I’ve provided the file for (example1.ui file). Now let’s dig into some code! (Full source code)
#This relies on the earlier getMayaWindow() function, so make sure that this code has access to that. from PyQt4 import uic #If you put the .ui file for this example elsewhere, just change this path. listExample_form, listExample_base = uic.loadUiType('c:/example1.ui') class ListExample(listExample_form, listExample_base): def __init__(self, parent=getMayaWindow()): super(ListExample, self).__init__(parent) self.setupUi(self) #The names "addItemBtn" and "removeItemBtn" #come from the "objectName" attribute in Qt Designer #the attributes to access them are automatically created #for us when we call setupUi() #Designer ensures that the names are unique for us. self.addItemBtn.clicked.connect(self.addItem) self.removeItemBtn.clicked.connect(self.removeItem) def addItem(self): """ Add a new item to the end of the listWidget """ item = QtGui.QListWidgetItem(self.listWidget) item.setText('Item #%s!'%self.listWidget.count()) def removeItem(self): """ Remove the last item from the listWidget """ count = self.listWidget.count() if count: self.listWidget.takeItem(count-1)
I have allot more planned, but Thanksgiving is coming up so I have decided to split this up into parts (probably at least 3, judging by how much of my overview I covered in this intro)