Closed fredrikaverpil closed 10 months ago
Originally asked by Pierre Augeard:
Where? :O
Where?
Over at the Maya beta forums, but now I see he's interacting here. Ping @pipoun
@pipoun - could you please provide a use case for this request/question?
from Qt import QtGui, binder
import maya.OpenMayaUI
def get_maya_mainwindow():
"""Get the main Maya window
:return: :class:`QtGui.QMainWindow` instance of the top level Maya window
"""
ptr = maya.OpenMayaUI.MQtUtil.mainWindow()
if ptr is not None:
# wrap_instance (pep8), wrapInstance (shiboken) or wrapinstance (sip)
return binder.wrap_instance(long(ptr), QtGui.QMainWindow)
@pipoun do you ever use this for anything except returning the main Maya window when you want to parent your UI to Maya?
Would you consider this approach or do you feel QtWidgets.QApplication.activeWindow()
is too unreliable?
A portable approach would be preferable. However..
Would you consider this approach
This returns the script editor for me.
from Qt import QtWidgets
window = QtWidgets.QApplication.activeWindow().objectName()
print(window)
Unless I have another window active, in which case that window is returned.
There is this.
from Qt import QtWidgets
window = {w.objectName(): w for w in QtWidgets.qApp.topLevelWidgets()}["MayaWindow"]
print(window)
I just wanted to focus on the fact that sip.wrapinstance
and shiboken.wrapInstance
have not the same spelling nor the exact same behavior and it should be considered to have a wrapper to unify all of this ?
@pipoun do you ever use this for anything except returning the main Maya window when you want to parent your UI to Maya?
There are other use cases than getting a main window as a QMainWindow
instance.
QtWidgets.qApp.topLevelWidgets()
is very slow in some cases compared to MQtUtil.mainWindow
(when there are many Qt widgets init'd), see the implementation :
http://code.qt.io/cgit/qt/qtbase.git/tree/src/widgets/kernel/qapplication.cpp#n1776
Thanks for the added info @pipoun.
Would it be possible to show us some more examples? I haven't used this particular feature myself, so it's difficult to know what and how to wrap.
To me, it sounds like there's a need for a convenience function "Qt.wrap_instance()
", like this one, by Nathan Horne, previously mentioned:
def wrapinstance(ptr, base=None):
"""
Utility to convert a pointer to a Qt class instance (PySide/PyQt compatible)
:param ptr: Pointer to QObject in memory
:type ptr: long or Swig instance
:param base: (Optional) Base class to wrap with (Defaults to QObject, which should handle anything)
:type base: QtGui.QWidget
:return: QWidget or subclass instance
:rtype: QtGui.QWidget
"""
if ptr is None:
return None
ptr = long(ptr) #Ensure type
if globals().has_key('shiboken'):
if base is None:
qObj = shiboken.wrapInstance(long(ptr), QtCore.QObject)
metaObj = qObj.metaObject()
cls = metaObj.className()
superCls = metaObj.superClass().className()
if hasattr(QtGui, cls):
base = getattr(QtGui, cls)
elif hasattr(QtGui, superCls):
base = getattr(QtGui, superCls)
else:
base = QtGui.QWidget
return shiboken.wrapInstance(long(ptr), base)
elif globals().has_key('sip'):
base = QtCore.QObject
return sip.wrapinstance(long(ptr), base)
else:
return None
I've personally only used this to successfully fetch the Maya main window:
def maya_main_window():
'"""Returns the main Maya window"""
main_window_ptr = omui.MQtUtil.mainWindow()
return wrapinstance(long(main_window_ptr), QtGui.QWidget)
But for this particular thing, your code is much much nicer, @mottosso. I second @mottosso's request to see additional use cases.
Yeah, a feature, encompassing 14% of the entire codebase (32/230 LOC), for a single usecase isn't worth it.
Once we find more usecases, I'd be happy to reconsider this.
Sometimes it's useful to access sip.delete() / shiboken.delete(), especially when redefining layouts, selection models, or running complex unit tests. Admittedly it's only really needed for PyQt4 but it would be useful to abstract this as something like C_binding.
Hi @innerhippy,
Would it be possible to share an example of where sip could be useful? That would really help determine how to best see it realised.
Admittedly it's only really needed for PyQt4
Just a suggestion, would this work for you then?
if "PyQt4" in QtCompat.__binding__:
sip.delete()
shiboken.delete()
What I'm interested in, is whether there is an alternative to using sip.delete()
and friends in the first place, that is cross-compatible. But to do that, we'll need examples of where sip is used.
Thanks @fredrikaverpil , that would certainly work and similar to what I was thinking. I guess the consideration is if this should be provided by the Qt module itself. We use it to delete layouts and for some unit tests where the destruction order of models gets a bit messed up (in PyQt4 anyway). I agree that ideally this shouldn't be necessary, and maybe refactoring the code would solve the problem, but sometimes it's a question of having to just making stuff work. I'll post some code snippets on Monday but have to get clearance first.
Just wanted to add another voice here.
I found this request when looking at porting our stack over to Maya2017 and doing the Qt4->Qt5 shuffle. We have upwards of 20 projects which use wrapinstance - our previous (inhouse) PyQt4/PySide shim provided access to the underlying binding where we used a couple of very specific methods, e.g. wrapInstance, delete, signature/methodsignature where behaviour was comparable.
Common with others on this thread the largest use is wrapInstance generally against the Maya API: MQtUtil Class Reference, where *QObject/QWidget is returned.
We could ask people to port to pymel: uitypes.py within Maya-specific code, which is already providing this switching logic. But the API brings us back to a specific PyQt/PySide implementations which is what makes Qt.py so attractive.
I guess the reason why we see so much of this in the codebase is that Maya's examples are driving people towards wrapInstance: Autodesk Docs: Working with PySide in Maya I would love for us to be able to produce a translation of that document as "Working with Qt.py in Maya".
If people are happy for it to be added - I'd be happy to help put together a patch.
I would love for us to be able to produce a translation of that document as "Working with Qt.py in Maya".
Good idea. Perhaps as an extension of Fredrik's document.
To include pros and cons of these binding-specific methods. Could also be elsewhere, even in the README of this project since it maybe relevant enough.
To follow up on my post from 1st Oct, here's an example of where we use the delete function:
def deleteLayout(widget, layout=None, deleteTopLevelLayout=True):
""" Function to delete a widget's layout. This needs to be done in
reverse order of each item in the layout as the index is changed
for each itemAt() call.
If deleteTopLevelLayout==True, the layout is deleted using the sip
module to ensure that all underlying C++ destructors are called,
otherwise the layout will have stale widgets from the previous
setLayout() calls
""""
if widget is not None and layout is None:
layout = widget.layout()
if layout is not None:
for i in xrange(layout.count()-1, -1, -1):
item = layout.itemAt(i)
if isinstance(item, QtGui.QWidgetItem):
item.widget().setParent(None)
elif isinstance (item, QtGui.QLayoutItem):
# recursive call if the item is a layout
deleteLayout(item)
if deleteTopLevelLayout:
import sip
sip.delete(layout)
That is perfect @innerhippy! Thanks for that!
I guess the reason why we see so much of this in the codebase is that Maya's examples are driving people towards wrapInstance: Autodesk Docs: Working with PySide in Maya I would love for us to be able to produce a translation of that document as "Working with Qt.py in Maya".
If people are happy for it to be added - I'd be happy to help put together a patch.
In case you decide to start doing this, you can fork this project and make a pull request with the translation. If anyone has the time he/she can join in and help out by making pull requests to your fork. This way it could become a collaborative process.
Good idea. Perhaps as an extension of Fredrik's document.
To include pros and cons of these binding-specific methods. Could also be elsewhere, even in the README of this project since it maybe relevant enough.
If this were to be created, I think it would be useful to keep it as a markdown file (linked from the README) here in the repository.
Hi guys, Sorry to jump in the discussion. First thanks for Qt.py, it's been a huge lifesaver here at Golaem !! I've ported all our code to it but there's still a function which relies around wrap_instance that I can't convert (yet). We're having a findLayout function which relies on Nathan Ortiz one (http://nathanhorne.com/embedding-a-maya-widget-into-a-pyqt-ui/). This allows us fetching the Attribute Editor layout of a specific node and override it with our own QT controllers (such as this one:
For sure a wrapInstance binding will be awesome but I let this decision up to you :) Let me know if you need more info
Hi @glm-nchaverou! Thanks for the kind words, and you are most welcome to join the discussion.
I can see the use of wrapping sip/shiboken, but I'm still out the lookout for reason enough to warrant it. So far, the uses I have seen and been shown have had a non-sip equivalent; the most prominent being parenting to the main Maya window.
Let me know if you need more info
If you could provide me with an example of doing what you're looking to do, in this case modifying the Attribute Editor, using sip/shiboken, then I'd be happy to try and find a non-sip/shiboken equivalent.
If there is none, then that would be the best of reasons to start looking at wrapping enough of it to facilitate what you are looking to do.
I couldn't find how to get the Attribute Editor, but if it's similar to finding the Channel Box, then maybe this could be of help.
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}")
Pure 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}")
Hey,
Ok here's how we hacked the AttributeEditor (can't find who we inspired this first... sorry)
In the AEMyNodeTemplate.mel file:
global proc AEMyNodeTemplate( string $nodeName )
{
...
editorTemplate -callCustom "melbuildQT" "melUpdateQT" "";
...
}
melBuildQT( string $nodeName )
{
string $parent = `setParent -q`;
python("buildQT('" + $parent + "')");
}
And on the Python side:
def buildQT(layout):
mayaLayoutPtr = omui.MQtUtil.findLayout(layout)
mayaLayout = wrapInstance(long(mayaLayoutPtr), QtWidgets.QWidget)
mayaLayoutObj = mayaLayout.findChild(QtWidgets.QBoxLayout)
#add the widget to the layout
mayaLayoutObj.insertWidget(0,widget)
It should highlight eveything :) Let me know if not
I can see the use of wrapping sip/shiboken, but I'm still out the lookout for reason enough to warrant it.
Our teams have a reasonable volume of code to port from Qt4 to Qt5. For those porting it might be their first time working with this project and certainly some of the original reasoning/intended behavior is perhaps lost in the mists of time.
The first pass is a like for like replacement - with much of the work performed by a conversion script.
Locally, we've extended Qt.py using something along the lines of:
c_binding = imp.new_module("c_binding")
c_binding.delete = shiboken2.delete
c_binding.wrapInstance = shiboken2.wrapInstance
_add(PySide2, "c_binding", c_binding)
We're waiting feedback on how well this works in the field so YMMV. I started to put together a pull request, but haven't had the opportunity to complete as other priorities have overtaken me. If it is of any use, the initial work in progress is here: https://github.com/boglin/Qt.py/tree/expose_binding
Sorry to jump in on this but I also have use for the sip.wrapInstance setup. We get the statusline in Maya to place our tools so they are easily accessible:
def getMayaStatusLine(): gStatusLine = mel.eval('$temp=$gStatusLine') ptr = apiUI.MQtUtil.findControl(gStatusLine) if ptr is not None: return sip.wrapinstance(long(ptr), QtWidgets.QWidget)
Any tips on how to get the status line using Qt.py, I have tried the above solution replacing "MayaWindow" with "StatusLine" but it appears there are a number of QWidgets and QMenus without object names
I found a good gist that gives me what I am after:
def getMayaStatusLine():
gStatusLine = mel.eval('$temp=$gStatusLineForm')
ptr = apiUI.MQtUtil.findControl(gStatusLine)
print ptr
if ptr is not None:
statusLine = shiboken2.wrapInstance(long(ptr), QtWidgets.QWidget)
return statusLine.children()[1].children()[1]
Hi Ben, please be aware that in your second snippet you are using shiboken2 which is the library that helps bind PySide2 to the underlying Qt (C++) library.
This will of course work in Maya2017, but you will have linked yourself directly with PySide2 - which maybe defeats the point of using Qt.py.
Given the continued interest in this topic I reimplemented my changes on the current master (see pull request #180). I'm aware that this issue is closed and would respect it if this is felt beyond the scope of this project.
With these changes, Ben should be able to do:
from Qt import c_binding
def getMayaStatusLine():
gStatusLine = mel.eval('$temp=$gStatusLineForm')
ptr = apiUI.MQtUtil.findControl(gStatusLine)
print ptr
if ptr is not None:
statusLine = c_binding.wrapInstance(long(ptr), QtWidgets.QWidget)
return statusLine.children()[1].children()[1]
My ability to test is somewhat limited at present, so feedback from those on this issue would be appreciated.
Hey guys I see your point on usiung shiboken_2. How would I build pull request 180 it to test this for boglin?
You can pick up the change from the c_binding branch of my fork (https://github.com/boglin/Qt.py/tree/c_binding)
Hey guys, sorry for the lagging replies, I'm currently on holiday until the 10th of Feb and will address this as soon as I'm back. So far, considering the popularity of the request, it seems what's best for both the project and yourselves to include something like this.
Will comment on specifics in the current pull request.
Reopening this so that we may close it once it has been implemented
Thanks guys!
Hi @glm-nchaverou,
I'm having trouble running your example with the Attribute Editor Template. Could you post a full template for me, along with how I use it?
Hi @ben-hearn-sb,
Let me know if this works for you.
from maya import mel
from Qt import QtWidgets
def getMayaStatusLine():
window = {o.objectName(): o for o in QtGui.qApp.topLevelWidgets()}["MayaWindow"]
gStatusLine = mel.eval('$temp=$gStatusLineForm')
statusLine = window.findChild(QtGui.QWidget, gStatusLine)
return statusLine
statusLine = getMayaStatusLine()
statusLine.setStyleSheet("QWidget {background: red}")
Having tried a few examples at this point, the pattern seems to be that wherever there is a findLayout
or findControl
you replace it with window.findChild
. Any reference to sip
or shiboken
is included in that call, as passing the superclass you are looking for. Such as QtWidgets.QLayout
or QtWidgets.QWidget
.
Hey @mottosso, sorry for the late answer was stucked with work those past days Find attached a small 101 maya plugin showing how to hack the AEditor
Let me know if you have any question
Thanks @glm-nchaverou, can't quite get it to work. Here's what I did.
$HOME/maya/scripts/AETemplates
cmds.createNode("DummyNode")
QLabel
, and no error messages.Sorry, I have no experience with AETemplates and Autodesk's help page haven't been very helpful.
What am I doing wrong?
Oops sorry, assumed you're using Maya regularly :) Maya needs to know where to source scripts from. You can put both AEDummyNodeTemplate.mel and DummyNodeUI.py into a dir referenced within %MAYA_SCRIPT_PATH% On Win, it's usually C:\Users\chaverou\Documents\maya\2016\scripts :)
Those will be sourced automatically
If the AEXXTemplate is loaded, you should see the following in the AE for this node:
If the python UI file is loaded correctly, you should see this label on top :
Oops sorry, assumed you're using Maya regularly :)
Haha, I do. Just not AETemplates.
If the AEXXTemplate is loaded, you should see the following in the AE for this node:
Ok, that did the trick. Thanks.
The Python file had a reference to Qtpy.Qt
that I changed into Qt
which got the error to stop appearing.
How can I reload the AETemplate? Do I restart Maya each time?
Ah yes sorry !! I should have warned about Qt.py :) but, well, looks like you're used to use it :) DummyNode_updateQT gets called every time you unselect / reselect the node. Just source a function within the same name in the Script Editor to overwrite it :) The rest is static and will require you to restart Maya I'm afraid
Ok, here we go.
from Qt import QtWidgets, QtCore
# Cache main window so that it is only computed once.
app = QtWidgets.QApplication.instance()
window = {o.objectName(): o for o in app.topLevelWidgets()}["MayaWindow"]
def buildQT(layout, node):
"""Build QT Widget"""
ae = window.findChild(QtCore.QObject, "MainAttributeEditorLayout")
form = ae.findChild(QtCore.QObject, "AEcontrolFormLayout")
# The last element of the AE layout is expected to differ across runs
# or opening/closing of the AE, so we grab the last entry.
mayaLayout = form.findChild(QtCore.QObject, layout.rsplit("|", 1)[-1])
mayaLayoutObj = mayaLayout.findChild(QtWidgets.QBoxLayout)
widget = QtWidgets.QLabel()
widget.setText('my uber awesome label')
mayaLayoutObj.insertWidget(0, widget)
def updateQT(layout, node):
pass
No other change to the template or plug-in.
The trick here was to look at the value of layout
, which was this.
MayaWindow|MainAttributeEditorLayout|formLayout2|AEmenuBarLayout|AErootLayout|AEStackLayout|AErootLayoutPane|AEbaseFormLayout|AEcontrolFormLayout|AttrEdDummyNodeFormLayout|scrollLayout2|columnLayout4|frameLayout29|columnLayout5|columnLayout6
It's the location of the Qt widget in the widget hierarchy. With this information, we spot a few key members, such as MainAttributeEditor
and right before getting to DummyNode
specifically, a layout called AEcontrolFormLayout
.
Assuming these are fixed and consistent across versions of Maya and user sessions, the only remaining thing to do was to grab the layout, which is expected to have a unique name within this narrow space. The rest is noise.
Let me know what you think.
Looks good enough to me. I'll have to check that it's consistent through all existing versions of Maya and upcoming ones :) Thanks a ton for the handy tip
If you wanted to play it safe, you could traverse the hierarchy as given. I'd imagine this is what Maya's findLayout
is doing under the hood.
This runs in the Maya Script Editor, though odds are the final element will differ because of the dynamic generation of Maya's Attribute Editor. The concept however is sound.
from Qt import QtWidgets, QtGui, QtCore
hierarchy = "MayaWindow|MainAttributeEditorLayout|formLayout2|AEmenuBarLayout|AErootLayout|AEStackLayout|AErootLayoutPane|AEbaseFormLayout|AEcontrolFormLayout|AttrEdDummyNodeFormLayout|scrollLayout2|columnLayout4|frameLayout29|columnLayout5|columnLayout6"
window = {o.objectName(): o for o in QtWidgets.QApplication.instance().topLevelWidgets()}["MayaWindow"]
# Previous method
ae = window.findChild(QtCore.QObject, "MainAttributeEditorLayout")
form = ae.findChild(QtCore.QObject, "AEcontrolFormLayout")
layout = form.findChild(QtCore.QObject, hierarchy.rsplit("|", 1)[-1])
# Safe method
parent = window
for child in hierarchy.split("|")[1:]:
parent = parent.findChild(QtCore.QObject, child)
# Prove that the results are identical
assert parent == layout
The final child is guaranteed to exist, as it is given by Maya. As your code runs synchronously, there is no chance of the hierarchy changing whilst it runs.
Having said that, I'm sure there is a better way of achieving your goal if we aren't trying to mimic code written specifically for use with shiboken/sip.
Hi, Jumping back on this again (yeah I know, sorry :)) While the solution proposed worked efficiently for docked main attribute editor, it doesn't for floating Attribute Editors... Maya allows to use the Copy Tab button of an AE to create a dupplicated floating AE window
Thus a Layout which is named..
MayaWindow|MainAttributeEditorLayout|formLayout87|AEmenuBarLayout|AErootLayout|AEStackLayout|AErootLayoutPane|AEbaseFormLayout|AEcontrolFormLayout|AttrEdCrowdChOpSensorInputFormLayout|scrollLayout2|columnLayout5|frameLayout37|columnLayout8|columnLayout9
..in the Main AE is now named..
MayaWindow|nexFloatWindow|paneLayout1|scrollLayout1|formLayout46|formLayout48|frameLayout9|frameLayout10|formLayout49
...in a floating one.
OpenMayaUi findLayout works like a charm in this situation.
Editor note: Formatted long layout names
Jumping back on this again (yeah I know, sorry :))
Not a problem! This isn't over until we've found at least 1 case where a cross-compatible solution is not possible.
If you could post an example of what doesn't work, I'd be happy to take a closer look.
Just wanted to add my 2c. I think it would be great if there was an abstraction to wrapInstance as we use it in a few tools especially when we mix Python/C++ Qt, and it's not just limited to Maya.
There are actually quite a few tools that are useful in sip/shiboken unfortunately they don't keep those consistent which is annoying. But wrapInstance/wrapinstance would definitely get the most mileage.
I think @boglin had the best implementation for this: https://github.com/boglin/Qt.py/tree/c_binding We might merge that into our internal copy of SpiQt because it would definitely be handy.
Thanks @dgovil.
To everyone, please let me know if this is just me.
So far, there are 46 comments in this thread. Half of them is requests for wrapInstance
with Qt.py, and the other half is me saying no.
Now I would be most delighted to include something I felt was worth including, but that worth can't be built on anything but communication. The request you are making is nonsensical and unnecessary.
So tell me, what would you do?
Against my better judgement and due to the overwhelming number of requests, I've made a PR just now with a cross-compatible solution that I hope will live up to your expectations.
I agree with you on that ideally Qt.py
should not have to handle it, since like you said there are other ways to achieve the same.
Unfortunately the other methods require some form of traversal of hierarchies or knowing what elements are called (which may change in the future) whereas if there's a guaranteed way to get a pointer from c++ or Python (for example with Maya's getMainWindow function) , the directness would be worth it.
There's also the matter of education. The majority of reference and experience so far online says to use wrapInstance. While alternatives exist like you suggested, people will end up using sip/shiboken directly which kind of negates some of the benefits of qt.py in not having to worry about what binding is being used.
So yes I totally agree with you on that there are better ways to do it, but I think it's just so common that it would be beneficial to have it here. I realize that potentially opens the floodgate of "well what about this feature?" But I think that the rest of sip/shiboken are so incompatible it won't happen.
Your PR looks like it hits the sweet spot of giving the most bang for the buck.
Hello all,
Sorry for the late replies we have jumped back onto Maya2017 integration here at the studio and I am ready to give my 2cents back to the discussion. I am testing the proposed solutions now. They work for the statusline and the main maya window although @mottosso suggestion does not work for me at the moment. Is it absolutely necessary to replace the Shiboken stuff since I am only using it for the statusline?
I am getting the main Maya window and storing it like so:
def storeMayaWindow():
ptr = apiUI.MQtUtil.mainWindow()
if ptr is not None:
app = QtWidgets.QApplication.instance()
global_config.mayaMainWindow = {o.objectName(): o for o in app.topLevelWidgets()}["MayaWindow"]
and returning my statusline like so:
def getMayaStatusLine():
gStatusLine = mel.eval('$temp=$gStatusLineForm')
ptr = apiUI.MQtUtil.findControl(gStatusLine)
if ptr is not None:
statusLine = shiboken2.wrapInstance(long(ptr), QtWidgets.QWidget)
#statusLine = c_binding.wrapInstance(long(ptr), QtWidgets.QWidget)
return statusLine.children()[1].children()[1]
Hi @ben-hearn-sb,
@mottosso suggestion does not work for me at the moment.
Could you let me know what exactly isn't working?
Originally asked by Pierre Augeard: