While it is rather easy to create a composite widget using Python and/or a true custom widget using Python (see examples in PyQt distribution /examples/designer/plugins/widgets), it becomes very unintuitive when you use C++ :(
For this post we will be using a true custom Widget and go step by step on what you need to do to get it working and exposed in Maya. For this we will be using a clock (with seconds)
The first thing is to create the Widget itself. That step is pretty straight forward. Create your .h and .cpp file to make up your control.
//----------------------------------------------------------------------------- #pragma once #include <QWidget> #include <QtDesigner/QDesignerExportWidget> class QDESIGNER_WIDGET_EXPORT FullAnalogClock : public QWidget { Q_OBJECT public: FullAnalogClock (QWidget *parent =NULL) ; protected: virtual void paintEvent (QPaintEvent *event) ; } ;
What is important here is to note the presence of
QDESIGNER_WIDGET_EXPORT
This one is important if you want your control to be available and usable in the QtDesigner tool.
Because I prefer to avoid using qmake, I do include the metadata moc file and the end of my implementation file and generate it as a step rule in Visual Studio. But at this stage, it is perfectly ok to build your C++ control the Qt way...
//----------------------------------------------------------------------------- #include "StdAfx.h" #include "FullAnalogClock.h" FullAnalogClock::FullAnalogClock (QWidget *parent) : QWidget(parent) { QTimer *timer =new QTimer (this) ; connect (timer, SIGNAL(timeout ()), this, SLOT(update ())) ; timer->start (1000) ; setWindowTitle (tr("Analog Clock")) ; resize (200, 200) ; setVisible (true) ; setGeometry (100, 100, 200, 200); } void FullAnalogClock::paintEvent (QPaintEvent *) { static const QPoint hourHand [3] ={ QPoint (7, 8), QPoint (-7, 8), QPoint (0, -40) } ; static const QPoint minuteHand [3] ={ QPoint (7, 8), QPoint (-7, 8), QPoint (0, -70) } ; static const QPoint secondHand [2] ={ QPoint (0, 8), QPoint (0, -70) } ; QColor hourColor (127, 0, 127) ; QColor minuteColor (0, 127, 127, 191) ; QColor secondColor (127, 127, 0, 191) ; int side =qMin (width (), height ()) ; QTime time =QTime::currentTime () ; QPainter painter (this) ; painter.setRenderHint (QPainter::Antialiasing) ; painter.translate (width () / 2, height () / 2) ; painter.scale (side / 200.0, side / 200.0) ; // Hour painter.setPen (Qt::NoPen) ; painter.setBrush (hourColor) ; painter.save () ; painter.rotate (30.0 * ((time.hour () + time.minute () / 60.0))) ; painter.drawConvexPolygon (hourHand, 3) ; painter.restore () ; painter.setPen (hourColor) ; for ( int i =0 ; i < 12 ; ++i ) { painter.drawLine (88, 0, 96, 0) ; painter.rotate (30.0) ; } // Minute painter.setPen (Qt::NoPen) ; painter.setBrush (minuteColor) ; painter.save () ; painter.rotate (6.0 * (time.minute () + time.second () / 60.0)) ; painter.drawConvexPolygon (minuteHand, 3) ; painter.restore () ; painter.setPen (minuteColor) ; for ( int j =0 ; j < 60 ; ++j ) { if ( (j % 5) != 0 ) painter.drawLine (92, 0, 96, 0) ; painter.rotate (6.0) ; } // Second painter.setPen (secondColor) ; //painter.setBrush (secondColor) ; painter.setBrush (Qt::NoBrush) ; painter.save () ; painter.rotate (6.0 * time.second ()) ; painter.drawLine (secondHand [0], secondHand [1]) ; //painter.drawConvexPolygon (minuteHand, 3) ; painter.restore () ; } #include "FullAnalogClock.moc"
Now, the first thing that we prepared for, is to support the QtDesigner in case you want to be able to add, edit your control in a GUI environment. To do this, we need to make your DLL a Qt Plug-in. To do that, you need to have these symbols defined.
#define QT_PLUGIN #define QT_SHARED #define QT_NO_DEBUG 1 #define QDESIGNER_EXPORT_WIDGETS
and have a Qt plug-in class defined
//----------------------------------------------------------------------------- #pragma once #include <QtDesigner/QDesignerCustomWidgetInterface> class FullAnalogClockPlugin : public QObject, public QDesignerCustomWidgetInterface { Q_OBJECT Q_INTERFACES(QDesignerCustomWidgetInterface) private: bool initialized ; public: FullAnalogClockPlugin (QObject *parent =NULL) ; bool isContainer () const ; bool isInitialized () const ; QIcon icon () const ; QString domXml () const ; QString group () const ; QString includeFile () const ; QString name () const ; QString toolTip () const ; QString whatsThis () const ; QWidget *createWidget (QWidget *parent) ; void initialize (QDesignerFormEditorInterface *core) ; } ;
While the implementation of that class isn't very difficult there are few thing important like the name(), group() members which will tell the QtDesigner how to categorize your control in its UI. And the macro Q_EXPORT_PLUGIN2 to make your DLL a Qt plug-in.
//----------------------------------------------------------------------------- #include "StdAfx.h" #include "FullAnalogClock.h" #include "FullAnalogClockPlugin.h" #include <QtPlugin%gt; //----------------------------------------------------------------------------- FullAnalogClockPlugin::FullAnalogClockPlugin (QObject *parent) : QObject(parent) { initialized =false ; } void FullAnalogClockPlugin::initialize (QDesignerFormEditorInterface * /* core */) { if ( initialized ) return ; initialized =true ; } bool FullAnalogClockPlugin::isInitialized () const { return initialized ; } QWidget *FullAnalogClockPlugin::createWidget (QWidget *parent) { return new FullAnalogClock (parent) ; } QString FullAnalogClockPlugin::name () const { return "FullAnalogClock" ; } QString FullAnalogClockPlugin::group () const { return "Display Widgets" ; } QIcon FullAnalogClockPlugin::icon () const { return QIcon () ; } QString FullAnalogClockPlugin::toolTip () const { return "FullAnalogClock clock" ; } QString FullAnalogClockPlugin::whatsThis () const { return "FullAnalogClock clock" ; } bool FullAnalogClockPlugin::isContainer () const { return false ; } QString FullAnalogClockPlugin::domXml () const { return "<ui language=\"c++\">\n" " <widget class=\"FullAnalogClock\" name=\"FullAnalogClock\">\n" " <property name=\"geometry\">\n" " <rect>\n" " <x>0</x>\n" " <y>0</y>\n" " <width>100</width>\n" " <height>100</height>\n" " </rect>\n" " </property>\n" " </widget>\n" "</ui>"; } QString FullAnalogClockPlugin::includeFile () const { return "FullAnalogClock.h" ; } Q_EXPORT_PLUGIN2(FullAnalogClockPlugin, FullAnalogClockPlugin) #include "FullAnalogClockPlugin.moc"
Build and copy your resulting dll into the Maya or your Qt 'qt-plugins\designer' directory. And it should appear and work into your QtDesigner. It will also work if you create a .ui file using QtDesginer and load that .ui in Maya using the loadUI MEL command, I.e.:
$uiName= `loadUI -uiFile "e:/clock.ui"`; showWindow $uiName;
and/or
$uiName= `loadUI -uiFile "e:/clock.ui"`; dockControl -allowedArea "all" -area "right" -floating off -content $uiName -label "Custom Dock1";
Now this is where it becomes tricky. Ok, but what about Python? if you use the Maya command loadUI, no change, but if you decide to use PyQt for example - that won't work as is. This is because PyQt needs a sip wrapper to work with Qt Widget written in C++. See this post for more details.
The first thing is to create a .sip file
// Define the SIP wrapper to the FullAnalogClock library. %Module(name=FullAnalogClock, keyword_arguments="Optional") %Import QtGui/QtGuimod.sip %If (Qt_4_7_1 -) class FullAnalogClock : public QWidget { %TypeHeaderCode #include <FullAnalogClock.h> %End public: FullAnalogClock(QWidget *parent /TransferThis/ = 0); protected: virtual void resizeEvent(QResizeEvent *); virtual void paintEvent(QPaintEvent *e); }; %End
Beyond the %Module, %Import - what is really important is to have the 2 virtuals resizeEvents() and paintEvents() present. Otherwise your control will not appear as SIP won't be able to make the connection from PyQt.
Next is to create a config file to build the SIP wrapper.
import os import sipconfig from PyQt4 import pyqtconfig # The name of the SIP build file generated by SIP and used by the build # system. build_file = "FullAnalogClock.sbf" # Get the PyQt configuration information. config = pyqtconfig.Configuration() # Get the extra SIP flags needed by the imported PyQt modules. Note that # this normally only includes those flags (-x and -t) that relate to SIP's # versioning system. pyqt_sip_flags = config.pyqt_sip_flags # Run SIP to generate the code. Note that we tell SIP where to find the qt # module's specification files using the -I flag. ##os.system(" ".join([config.sip_bin, "-c", ".", "-b", build_file, "-I", config.pyqt_sip_dir, pyqt_sip_flags, "FullAnalogClock.sip"])) config.sip_bin ="s:\\Python\\sip" test =" " . join([config.sip_bin, "-c", ".", "-b", build_file, "-I", "\"" + config.pyqt_sip_dir + "\"", pyqt_sip_flags, "FullAnalogClock.sip"]) os.system (test) # We are going to install the SIP specification file for this module and # its configuration module. installs = [] installs.append(["FullAnalogClock.sip", os.path.join(config.default_sip_dir, "FullAnalogClock")]) installs.append(["FullAnalogClockconfig.py", config.default_mod_dir]) # Create the Makefile. The QtGuiModuleMakefile class provided by the # pyqtconfig module takes care of all the extra preprocessor, compiler and # linker flags needed by the Qt library. makefile = pyqtconfig.QtGuiModuleMakefile( configuration=config, build_file=build_file, installs=installs ) # Add the library we are wrapping. The name doesn't include any platform # specific prefixes or extensions (e.g. the "lib" prefix on UNIX, or the # ".dll" extension on Windows). makefile.extra_libs = ["FullAnalogClock"] #makefile.CFLAGS.append("-l..") #makefile.LFLAGS.append("-L../x64/Release") # Generate the Makefile itself. makefile.generate() # Now we create the configuration module. This is done by merging a Python # dictionary (whose values are normally determined dynamically) with a # (static) template. content = { # Publish where the SIP specifications for this module will be # installed. "FullAnalogClock_sip_dir": config.default_sip_dir, # Publish the set of SIP flags needed by this module. As these are the # same flags needed by the qt module we could leave it out, but this # allows us to change the flags at a later date without breaking # scripts that import the configuration module. "FullAnalogClock_sip_flags": pyqt_sip_flags } # This creates the FullAnalogClockconfig.py module from the FullAnalogClockconfig.py.in # template and the dictionary. sipconfig.create_config_module("FullAnalogClockconfig.py", "FullAnalogClockconfig.py.in", content)
You also need a FullAnalogClockconfig.py.in, but this one can be empty or contains the following:
from PyQt4 import pyqtconfig # These are installation specific values created when FullAnalogClock was configured. # The following line will be replaced when this template is used to create # the final configuration module. # @SIP_CONFIGURATION@ class Configuration(pyqtconfig.Configuration): """The class that represents FullAnalogClock configuration values. """ def __init__(self, sub_cfg=None): """Initialise an instance of the class. sub_cfg is the list of sub-class configurations. It should be None when called normally. """ # This is all standard code to be copied verbatim except for the # name of the module containing the super-class. if sub_cfg: cfg = sub_cfg else: cfg = [] cfg.append(_pkg_config) pyqtconfig.Configuration.__init__(self, cfg) class FullAnalogClockModuleMakefile(pyqtconfig.QtGuiModuleMakefile): """The Makefile class for modules that %Import FullAnalogClock. """ def finalise(self): """Finalise the macros. """ # Make sure our C++ library is linked. self.extra_libs.append("FullAnalogClock") # Let the super-class do what it needs to. pyqtconfig.QtGuiModuleMakefile.finalise(self)
and last do the build
@echo off set MAYA_LOCATION=C:\Program Files\Autodesk\Maya2013.5 set SIP=E:\sip-4.13.3 set PYQT=E:\PyQt-win-gpl-4.9.4 set MSVC_DIR=C:\Program Files (x86)\Microsoft Visual Studio 10.0 set MSVC_VERSION=2010 set QTDIR=v:\qt-adsk-4.7.1 set QMAKESPEC=win32-msvc%MSVC_VERSION% subst s: /d subst s: "%MAYA_LOCATION%" set MAYA_LOCATION=s: subst v: /d subst v: "E:\__sdkext\_Maya2013-5 Scripts" call "%MSVC_DIR%\VC\vcvarsall.bat" amd64 set INCLUDE=%INCLUDE%;%MAYA_LOCATION%\include\python2.6;%MAYA_LOCATION%\Python\include;%MAYA_LOCATION%\include\Qt;%MAYA_LOCATION%\include\QtCore;%MAYA_LOCATION%\include\QtGui;.. set LIB=%LIB%;%MAYA_LOCATION%\lib;..\..\Movie\x64\Release e: cd "E:\Projects\CP2520\AnalogClock\sip" "%MAYA_LOCATION%\bin\mayapy" ./configure.py nmake nmake install
Now go and run in Maya, you're done. The FullAnalogClock.pyd and FullAnalogClockconfig.py should have been copied into the Maya 'Python/Lib/site-packages' directory. One little thing when you in Maya, you still need to teach Maya where to the find the dll. The following code should so it simply:
import sys, os sys.path.append("C:/Program Files/Autodesk/Maya2013.5/qt-plugins/designer") os.environ['PATH'] =os.environ['PATH'] + ";C:/Program Files/Autodesk/Maya2013.5/qt-plugins/designer"
The full code:
import sys, os sys.path.append("C:/Program Files/Autodesk/Maya2013.5/qt-plugins/designer") os.environ['PATH'] =os.environ['PATH'] + ";C:/Program Files/Autodesk/Maya2013.5/qt-plugins/designer" import sip from PyQt4 import QtCore, QtGui import FullAnalogClock import os, re, sys import maya.cmds as cmds import maya.OpenMayaUI as mui global app global videop class videoPlayer(QtGui.QDialog): def __init__(self, parent = None): QtGui.QWidget.__init__( self, parent ) self.parent = parent self.setSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Preferred) self.resize(500,500) self.clock = FullAnalogClock.FullAnalogClock(self) self.clock.resize(100, 100) self.clock.setVisible(True) layout = QtGui.QHBoxLayout(self) layout.addWidget(self.clock) self.setLayout(layout) def test(): global app app = QtGui.QApplication.instance() ptr = mui.MQtUtil.mainWindow() win = sip.wrapinstance(long(ptr), QtCore.QObject) global videop videop = videoPlayer(win) videop.show() return videop test()
And you'll get the following :)
Result with a a clock created using SIP/PyQt code (top)
and using the .ui & MEL command loadUI (bottom)
Comments
You can follow this conversation by subscribing to the comment feed for this post.