-
Notifications
You must be signed in to change notification settings - Fork 252
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Implement wrapInstance and getCppPointer (#59) #190
Conversation
More ExamplesFor consideration into the main README.
Finding Widget Through MELshiboken from maya import mel, OpenMayaUI
from Qt import QtWidgets
import shiboken2
status_line = mel.eval('$temp=$gStatusLineForm')
ptr = OpenMayaUI.MQtUtil.findControl(status_line)
status_line = shiboken2.wrapInstance(long(ptr), QtWidgets.QWidget)
status_line = status_line.children()[1].children()[1]
status_line.setStyleSheet("QWidget {background: red}") Qt from maya import mel
from Qt import QtWidgets
app = QtWidgets.QApplication.instance()
window = {o.objectName(): o for o in app.topLevelWidgets()}["MayaWindow"]
status_line = mel.eval('$temp=$gStatusLineForm')
status_line = window.findChild(QtGui.QWidget, gStatusLine)
status_lne.setStyleSheet("QWidget {background: red}") Finding Widget through Object Nameshiboken import shiboken
import maya.OpenMayaUI as apiUI
from Qt import QtWidgets
channel_box = apiUI.MQtUtil.findControl("mainChannelBox")
channel_box = shiboken.wrapInstance(long(channel_box), QtWidgets.QTableView)
channel_box.setStyleSheet("QWidget {background: red}") Qt from Qt import QtWidgets
app = QtWidgets.QApplication.instance()
window = {o.objectName(): o for o in app.topLevelWidgets()}["MayaWindow"]
channel_box = window.findChild(QtWidgets.QTableView, "mainChannelBox")
channel_box.setStyleSheet("QWidget {background: green}") Custom Attribute Editor TemplateFor testing purposes, we'll create a custom node and associate an attribute editor template with it. The modification of the resulting template via Qt is what differs between Store the Notes
Boilerplate These two files are identical and cross-compatible. // AEMyNodeTemplate.mel
global proc AEMyNodeTemplate(string $nodeName)
{
editorTemplate -beginScrollLayout;
editorTemplate -beginLayout "My Attributes" -collapse 0;
editorTemplate -callCustom "MyNode_build_ui" "MyNode_update_ui" $nodeName;
editorTemplate -addControl "x";
editorTemplate -addControl "y";
editorTemplate -addControl "z";
editorTemplate -endLayout;
editorTemplate -addExtraControls;
editorTemplate -endScrollLayout;
}
global proc MyNode_build_ui( string $nodeName )
{
string $parent = `setParent -q`;
python("import myNodeUi");
python("myNodeUi.build_ui('" + $parent + "', '" + $nodeName + "')");
}
global proc MyNode_update_ui( string $nodeName )
{
string $parent = `setParent -q`;
python("myNodeUi.update_ui('" + $parent + "', '" + $nodeName + "')");
} # myNode.py
from maya import OpenMaya, OpenMayaMPx
kPluginNodeName = "MyNode"
MyNodeId = OpenMaya.MTypeId(524286)
class MyNode(OpenMayaMPx.MPxNode):
_x = OpenMaya.MObject()
_y = OpenMaya.MObject()
_z = OpenMaya.MObject()
def __init__(self):
OpenMayaMPx.MPxNode.__init__(self)
def compute(self, plug, data_block):
print("Computing..")
def MyNodeCreator():
return OpenMayaMPx.asMPxPtr(MyNode())
def MyNodeInit():
attr = OpenMaya.MFnNumericAttribute()
MyNode._x = attr.create("x", "x", OpenMaya.MFnNumericData.kFloat, 0.0)
attr.setKeyable(True)
MyNode._y = attr.create("y", "y", OpenMaya.MFnNumericData.kFloat, 0.0)
attr.setKeyable(True)
MyNode._z = attr.create("z", "z", OpenMaya.MFnNumericData.kFloat, 0.0)
attr.setKeyable(True)
MyNode.addAttribute(MyNode._x)
MyNode.addAttribute(MyNode._y)
MyNode.addAttribute(MyNode._z)
def initializePlugin(mobject):
mplugin = OpenMayaMPx.MFnPlugin(mobject)
mplugin.registerNode(
kPluginNodeName,
MyNodeId,
MyNodeCreator,
MyNodeInit,
OpenMayaMPx.MPxNode.kDependNode
)
def uninitializePlugin(mobject):
mplugin = OpenMayaMPx.MFnPlugin(mobject)
mplugin.deregisterNode(MyNodeId) shiboken Notice the OpenMayaUI and shiboken dependency. # myNodeUi.py
from maya import cmds, OpenMayaUI
from Qt import QtWidgets
if cmds.about(api=True) >= 201700:
from shiboken2 import wrapInstance
else:
from shiboken import wrapInstance
def build_ui(layout, node):
layout_ptr = OpenMayaUI.MQtUtil.findLayout(layout)
layout_obj = wrapInstance(long(layout_ptr), QtWidgets.QWidget)
layout_wid = layout_obj.findChild(QtWidgets.QBoxLayout) # Cast to QBoxLayout
widget = QtWidgets.QLabel("Hello World")
layout_wid.insertWidget(0, widget)
def update_ui(layout, node):
pass Qt # myNodeUi.py
from Qt import QtWidgets
def build_ui(layout, node):
app = QtWidgets.QApplication.instance()
window = {o.objectName(): o for o in app.topLevelWidgets()}["MayaWindow"]
parent = window
for child in layout.split("|")[1:]:
parent = parent.findChild(QtWidgets.QWidget, child)
widget = QtWidgets.QLabel("Hello World")
layout = parent.findChild(QtWidgets.QBoxLayout) # Cast to QBoxLayout
layout.insertWidget(0, widget)
def update_ui(layout, node):
pass |
What are our thoughts on this? He isn't pointing out what the difference is exactly, anyone experienced this problem? |
His code isn't true. >>> import sip
>>> import sys
>>> from PyQt4 import QtGui
>>> app = QtGui.QApplication(sys.argv)
>>> button = QtGui.QPushButton("Hello world")
>>> pointer = sip.unwrapinstance(button)
>>> sip.wrapinstance(long(pointer))
# Traceback (most recent call last):
# File "<stdin>", line 1, in <module>
# TypeError: wrapinstance() takes exactly 2 arguments (1 given)
>>> sip.wrapinstance(long(pointer), None)
# Traceback (most recent call last):
# File "<stdin>", line 1, in <module>
# TypeError: must be sip.wrappertype, not None I've implemented this regardless, as I can't see why not and it makes for less code. The feature is ignored when used with a import sys
from Qt import QtCompat, QtWidgets, __binding__
app = QtWidgets.QApplication(sys.argv)
try:
button = QtWidgets.QPushButton("Hello world")
button.setObjectName("MySpecialButton")
pointer = QtCompat.getCppPointer(button)
widget = QtCompat.wrapInstance(long(pointer))
assert isinstance(widget, QtWidgets.QWidget), widget
assert widget.objectName() == button.objectName()
if __binding__ in ("PySide", "PySide2"):
assert widget != button
else:
assert widget == button
finally:
app.exit() In either case, I uncovered a subtle difference between |
Hi @mottosso I just wanted to check what the current state of this PR was in your view? What still needs doing? I was considering merging this into our local copy of |
Hey @dgovil, no major changes as far as I can see, what's missing now is testing.
Please do, after some use, if you haven't encountered any issues, that'd be a good point to merge this. |
|
||
""" | ||
|
||
assert isinstance(ptr, long), "Argument 'ptr' must be of type <long>" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As far as I know, in Python3 there is no longer a long
, and instead it should be an int in python3.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hm, you're right. This wasn't caught by Travis, I'll take a closer look as to why that is tomorrow.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, this has been fixed.
Turns out shiboken
isn't supported by Python 3.5.
pyside/shiboken-setup#3
Get top-level window in any binding and any application.sip and shiboken is sometimes used to fetch the main window of an application in order to make it a parent of a custom window. Below is an example of how to find said window efficiently and in any situation. from Qt import QtWidgets
current = QtWidgets.QApplication.activeWindow()
while current:
parent = current
current = parent.parent()
print(parent) Limitations
|
FWIW, been running this in production for a week and it seems to be working correctly after porting some existing tools over to using this. |
That's great @dgovil, thanks for reporting back. |
ptr (long): Pointer to QObject in memory | ||
base (QObject, optional): Base class to wrap with. Defaults to QObject, | ||
which should handle anything. | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Perhaps we should add a "Returns:" to the docstring to make it easier to understand the different types of return values are possible here, as I'm guessing it's not always a QObject
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, and long
must also change.
Not sure how to tackle the fact that long
is absent from Python 3. I had a look at us internally taking int
for both Python 2 and 3, but then we stray from the original PySide2.wrapInstance
argument signature.
base = getattr(Qt.QtWidgets, super_class_name) | ||
|
||
else: | ||
base = Qt.QtCore.QObject |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Out of curiosity, in which scenario(s) will base
be set to Qt.QtCore.QObject
?
Would you mind adding comments in the if/else conditions to make this a bit more grokable?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Likely never, all objects you could get from this function are subclasses of QObject. It's set as default here as a fallback, as anything, even a QWidget
or your own subclass of a subclass of QMainWindow
could be interpreted as QObject
. In those cases, you still gain access to various superclass properties like objectName()
but not the more specialised methods like resize()
etc.
I'm sorry I've been so absent in regards to this addition to Qt.py. This is a fantastic PR. Crossing my fingers we won't have to spend too much time maintaining this though. 😉 Before merging, I'd like to test this Qt.py version internally:
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- We should revise the
loadUi
examples. - (Don't forget to update the
__version__
)
I'll look into this but I'm getting a weird "QMainWindow already deleted" error in Nuke. The same code works fine with Qt.py 1.0.0b3: # uisetup.py
class MyWindow(QtWidgets.QMainWindow):
def __init__(self, parent=None, resize=[]):
super(MyWindow, self).__init__(parent)
main_window_file = os.path.join(ui_dir, 'main_window.ui') # ui file is using the QMainWindow class
self.main_widget = QtCompat.loadUi(main_window_file) # errors as seen below
...
... The class is called like this from within panel = nukescripts.panels.registerWidgetAsPanel(
widget='uisetup.MyWindow',
name='SomeWindow',
id='uk.co.thefoundry.' + 'SomeWindow',
create=True) The error: Traceback (most recent call last):
File "C:/Users/[redacted]/bin/nuke/builds/nuke_v9.0v9_win64/plugins\nukescripts\panels.py", line 153, in makeUI
self.widget = self.widgetClass()
File "c:\users\iruser\code\repos\skalman\skalman\uisetup.py", line 154, in __init__
self.main_widget = QtCompat.loadUi(main_window_file)
File "C:\Users\iruser\skalman\condaenvs\skalman_py27\Lib\site-packages\Qt.py", line 1028, in _loadUi
Qt.QtCore.QMetaObject.connectSlotsByName(widget)
RuntimeError: Internal C++ object (PySide.QtGui.QMainWindow) already deleted. |
Ok, here's a reproducible. This runs just fine in Nuke 9.0v9 with Qt.py 1.0.0b3 but errors using Qt.py from this PR: import tempfile
import os
import io
from Qt import QtWidgets
from Qt import QtCompat
import nuke
import nukescripts
TEMPDIR = tempfile.mkdtemp()
tempfile = os.path.join(TEMPDIR, "qmainwindow.ui")
with io.open(tempfile, "w", encoding="utf-8") as f:
f.write(u"""\
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>MainWindow</class>
<widget class="QMainWindow" name="MainWindow">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>223</width>
<height>140</height>
</rect>
</property>
<property name="windowTitle">
<string>MainWindow</string>
</property>
<widget class="QWidget" name="centralwidget">
<widget class="QPushButton" name="pushButton">
<property name="geometry">
<rect>
<x>70</x>
<y>40</y>
<width>80</width>
<height>16</height>
</rect>
</property>
<property name="text">
<string>PushButton</string>
</property>
</widget>
</widget>
<widget class="QMenuBar" name="menubar">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>223</width>
<height>17</height>
</rect>
</property>
</widget>
<widget class="QStatusBar" name="statusbar"/>
</widget>
<resources/>
<connections/>
</ui>
""")
class MyWindow(QtWidgets.QMainWindow):
def __init__(self, parent=None):
super(MyWindow, self).__init__(parent)
self.main_widget = QtCompat.loadUi(tempfile)
self.setCentralWidget(self.main_widget)
# assert hasattr(Qt, '_wrapinstance') # Make sure we are using abstractfactory:implement59
panel = nukescripts.panels.registerWidgetAsPanel(
widget='MyWindow',
name='SomeWindow',
id='uk.co.thefoundry.' + 'SomeWindow',
create=True)
pane = nuke.getPaneFor('Properties.1')
panel.addToPane(pane)
my_window_widget = panel.customKnob.getObject().widget
assert my_window_widget |
Aha, this was introduced in 1.0.0b4 and not in this PR. https://github.com/abstractfactory/Qt.py/blame/666111eef6d3d73a4f50c7399d5408dbad5f2cf8/Qt.py#L966 |
Here's a slightly more portable piece of reproducible code which doesn't require Nuke. This requires PySide. import tempfile
import os
import io
from Qt import QtWidgets
from Qt import QtCompat
TEMPDIR = tempfile.mkdtemp()
tempfile = os.path.join(TEMPDIR, "qmainwindow.ui")
with io.open(tempfile, "w", encoding="utf-8") as f:
f.write(u"""\
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>MainWindow</class>
<widget class="QMainWindow" name="MainWindow">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>223</width>
<height>140</height>
</rect>
</property>
<property name="windowTitle">
<string>MainWindow</string>
</property>
<widget class="QWidget" name="centralwidget">
<widget class="QPushButton" name="pushButton">
<property name="geometry">
<rect>
<x>70</x>
<y>40</y>
<width>80</width>
<height>16</height>
</rect>
</property>
<property name="text">
<string>PushButton</string>
</property>
</widget>
</widget>
<widget class="QMenuBar" name="menubar">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>223</width>
<height>17</height>
</rect>
</property>
</widget>
<widget class="QStatusBar" name="statusbar"/>
</widget>
<resources/>
<connections/>
</ui>
""")
class MyWindow(QtWidgets.QMainWindow):
def __init__(self, parent=None):
super(MyWindow, self).__init__(parent)
self.main_widget = QtCompat.loadUi(tempfile)
self.setCentralWidget(self.main_widget)
my_window = MyWindow()
my_window.show() |
Whoa, that was a bit unnecessary... first time testing the review features...
I can't replicate that error with the PySide2 version for Maya 2017, or PySide 1.2.2 (Qt 4.8.5) and or PyQt4.10.1 (Qt 4.8.5). Haven't tried with PyQt5 yet though. Granted I'm trying your last example standalone without Nuke being involved. Edit: |
@dgovil The code snippet I gave works fine everywhere but in Nuke. Since this was introduced in a previous version (and not in this PR) I'll file a separate issue. MayaNuke |
@mottosso I was wondering where wrapInstance ended up into making it into |
That's good enough for me. If you could have a look at merging this with the current state of the repo then we should be able to have it merged by dinner time. |
# Conflicts: # Qt.py
This adds a wrapper for
wrapInstance
andgetCppPointer
fromshiboken2
and automatically unifies the differences withshiboken
andsip
for both Python 2 and 3.QtCompat.wrapInstance(addr=long, type=QObject)
QObject
QtCompat.getCppPointer(object=QObject)
long
Usage
Maya Example
This works for both 2016 and 2017.
Important
This addition requires
sip
,shiboken
orshiboken2
to be available on your system. If not found, Qt.py will still import successfully, but these members will not be available.In such cases, here is a Qt-only version and guaranteed cross-compatible version of the above.
The same pattern may be applied to any and all uses of
sip
,shiboken
andshiboken2
, as discussed in-depth at #53.Enjoy!