Skip to content
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

Merged
merged 7 commits into from
Jun 27, 2017

Conversation

mottosso
Copy link
Owner

@mottosso mottosso commented Mar 29, 2017

This adds a wrapper for wrapInstance and getCppPointer from shiboken2 and automatically unifies the differences with shiboken and sip for both Python 2 and 3.

Attribute Returns
QtCompat.wrapInstance(addr=long, type=QObject) QObject
QtCompat.getCppPointer(object=QObject) long

Usage

import sys
from Qt import QtCompat, QtWidgets
app = QtWidgets.QApplication(sys.argv)
button = QtWidgets.QPushButton("Hello world")
pointer = QtCompat.getCppPointer(button)
widget = QtCompat.wrapInstance(long(pointer), QtWidgets.QWidget)
assert widget == button
app.exit()

Maya Example

This works for both 2016 and 2017.

import sys
from maya import OpenMayaUI
from Qt import QtCompat, QtWidgets
pointer = OpenMayaUI.MQtUtil.mainWindow()
widget = QtCompat.wrapInstance(long(pointer), QtWidgets.QWidget)
assert isinstance(widget, QtWidgets.QWidget)

Important

This addition requires sip, shiboken or shiboken2 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.

from Qt import QtWidgets
app = QtWidgets.QApplication.instance()
widget = {o.objectName(): o for o in app.topLevelWidgets()}["MayaWindow"]

The same pattern may be applied to any and all uses of sip, shiboken and shiboken2, as discussed in-depth at #53.

Enjoy!

@mottosso
Copy link
Owner Author

mottosso commented Mar 29, 2017

More Examples

For consideration into the main README.

wrapInstance have found particular use in Autodesk Maya, below are a few scenarios in which it is commonly used along with cross-binding alternatives.


Finding Widget Through MEL

shiboken

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 Name

shiboken

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 Template

For 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 shiboken and Qt.

Store the .mel file in your $HOME/maya/scripts/AETemplates directory, or in any directory on your MAYA_SCRIPT_PATH

Notes

  • For whatever reason, templates can be in both XML and MEL format; only XML is documented (?), but mostly MEL is found in the Maya installation directory (e.g. Maya2018\scripts\AETemplates).
  • MAYA_CUSTOM_TEMPLATE_PATH is the documented variable for templates, which doesn't seem to have any effect. MAYA_SCRIPT_PATH on the other hand does.

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

@mottosso
Copy link
Owner Author

What are our thoughts on this?

He isn't pointing out what the difference is exactly, anyone experienced this problem?

@mottosso
Copy link
Owner Author

mottosso commented Apr 7, 2017

He isn't pointing out what the difference is exactly, anyone experienced this problem?

His code isn't true. sip does not support what he's implemented for shiboken.

>>> 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 base argument and kicks in automatically when ignoring it.

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 sip and shiboken whilst writing the tests.
Notice, the objects are not directly comparable in PySide 1 and 2, but are in PyQt4 and PyQt5.

@dgovil
Copy link
Contributor

dgovil commented May 18, 2017

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 Qt.py till the PR was merged unless you think there will be major changes?

@mottosso
Copy link
Owner Author

Hey @dgovil, no major changes as far as I can see, what's missing now is testing.

I was considering merging this into our local copy of Qt.py

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>"
Copy link
Contributor

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.

Copy link
Owner Author

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.

Copy link
Owner Author

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

@mottosso mottosso changed the title Implement #59 Implement wrapInstance and getCppPointer (#59) May 22, 2017
@mottosso
Copy link
Owner Author

mottosso commented May 22, 2017

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

  • If run from within an already custom window that did not have it's parent set to the main window or a descendant of it, then this will return the custom window and may exit when it exists.

@dgovil
Copy link
Contributor

dgovil commented May 29, 2017

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.

@mottosso
Copy link
Owner Author

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.

Copy link
Collaborator

@fredrikaverpil fredrikaverpil May 30, 2017

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?

Copy link
Owner Author

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
Copy link
Collaborator

@fredrikaverpil fredrikaverpil May 30, 2017

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?

Copy link
Owner Author

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.

@fredrikaverpil
Copy link
Collaborator

fredrikaverpil commented May 30, 2017

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:

pip install git+https://github.com/abstractfactory/Qt.py.git@implement59

Really nice job, @mottosso and @dgovil!

Copy link
Collaborator

@fredrikaverpil fredrikaverpil left a 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__)

@fredrikaverpil
Copy link
Collaborator

fredrikaverpil commented May 30, 2017

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 uisetup.py:

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.

@fredrikaverpil
Copy link
Collaborator

fredrikaverpil commented May 30, 2017

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

@fredrikaverpil
Copy link
Collaborator

fredrikaverpil commented May 30, 2017

Aha, this was introduced in 1.0.0b4 and not in this PR.

https://github.com/abstractfactory/Qt.py/blame/666111eef6d3d73a4f50c7399d5408dbad5f2cf8/Qt.py#L966

@fredrikaverpil
Copy link
Collaborator

fredrikaverpil commented May 30, 2017

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()

@fredrikaverpil fredrikaverpil dismissed their stale review May 30, 2017 09:26

Whoa, that was a bit unnecessary... first time testing the review features...

@dgovil
Copy link
Contributor

dgovil commented May 30, 2017

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.
They all show me a window with a PushButton, and the other ui elements are available.

Granted I'm trying your last example standalone without Nuke being involved.

Edit:
Tried under Nuke 10.0v4 as well for your last example with PySide 1.2.2 (Qt 4.8.5) without error.
Could be a bug in the PySide version shipped with Nuke 9?

@fredrikaverpil
Copy link
Collaborator

fredrikaverpil commented May 31, 2017

@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.

Maya

screen shot 2017-05-31 at 08 20 20

Nuke

screen shot 2017-05-31 at 08 20 47

@dgovil
Copy link
Contributor

dgovil commented Jun 25, 2017

@mottosso I was wondering where wrapInstance ended up into making it into Qt.py? We've been using it in production for a while now without issue so I was wondering if it is possible to get it into Qt.py. Right now I am managing our own fork but I'd love to get it down to as few changes as possible

@mottosso
Copy link
Owner Author

We've been using it in production for a while now without issue so I was wondering if it is possible to get it into Qt.py.

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.

@mottosso mottosso merged commit 3095c3f into mottosso:master Jun 27, 2017
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants