I created a tool that is able to dock in Maya's main ui, but I can't figure out a way to clean it up once it closes. The problem is if I create multiple instances of the tool then drag it in place to dock it, they will ALL show up when I right-click on Maya's window. How do I properly clean these up when the tool closes?
I already tried cmds.deleteUI, QObject.deleteLater() and at best I can only clear the tool's contents, but it will still exist in Maya. Here's an example of what I have so far:
from shiboken import wrapInstance
from PySide import QtGui, QtCore
from maya import OpenMayaUI as OpenMayaUI
from maya.app.general.mayaMixin import MayaQWidgetDockableMixin
class Window(MayaQWidgetDockableMixin, QtGui.QWidget):
def __init__(self, parent = None):
super(self.__class__, self).__init__(parent = parent)
mayaMainWindowPtr = OpenMayaUI.MQtUtil.mainWindow()
self.mayaMainWindow = wrapInstance(long(mayaMainWindowPtr), QtGui.QWidget)
self.setWindowFlags(QtCore.Qt.Window)
if cmds.window('myTool', q = True, ex = True):
cmds.deleteUI('myTool')
self.setObjectName('myTool')
self.setWindowTitle('My tool')
self.resize(200, 200)
self.myButton = QtGui.QPushButton('TEMP')
self.mainLayout = QtGui.QVBoxLayout()
self.mainLayout.addWidget(self.myButton)
self.setLayout(self.mainLayout)
def dockCloseEventTriggered(self):
self.deleteLater()
def run(self):
self.show(dockable = True)
myWin = Window()
myWin.run()
After digging around mayaMixin.py I managed to get a working solution with the behavior I'm after! The idea is that you need to dig through Maya's main window and delete any instances of it there.
The example below will cleanly delete any instances once the window is closed or a new instance is created. It doesn't matter if it's docked or floating, it seems to work ok. You could always tweak the behavior too if you don't want it to delete if it's closed while being docked. I left many comments in the code as there was a lot of "gotchas".
from shiboken import wrapInstance
from PySide import QtGui, QtCore
from maya import OpenMayaUI as OpenMayaUI
from maya.app.general.mayaMixin import MayaQWidgetDockableMixin
from maya.OpenMayaUI import MQtUtil
class MyWindow(MayaQWidgetDockableMixin, QtGui.QDialog):
toolName = 'myToolWidget'
def __init__(self, parent = None):
# Delete any previous instances that is detected. Do this before parenting self to main window!
self.deleteInstances()
super(self.__class__, self).__init__(parent = parent)
mayaMainWindowPtr = OpenMayaUI.MQtUtil.mainWindow()
self.mayaMainWindow = wrapInstance(long(mayaMainWindowPtr), QtGui.QMainWindow)
self.setObjectName(self.__class__.toolName) # Make this unique enough if using it to clear previous instance!
# Setup window's properties
self.setWindowFlags(QtCore.Qt.Window)
self.setWindowTitle('My tool')
self.resize(200, 200)
# Create a button and stuff it in a layout
self.myButton = QtGui.QPushButton('My awesome button!!')
self.mainLayout = QtGui.QVBoxLayout()
self.mainLayout.addWidget(self.myButton)
self.setLayout(self.mainLayout)
# If it's floating or docked, this will run and delete it self when it closes.
# You can choose not to delete it here so that you can still re-open it through the right-click menu, but do disable any callbacks/timers that will eat memory
def dockCloseEventTriggered(self):
self.deleteInstances()
# Delete any instances of this class
def deleteInstances(self):
mayaMainWindowPtr = OpenMayaUI.MQtUtil.mainWindow()
mayaMainWindow = wrapInstance(long(mayaMainWindowPtr), QtGui.QMainWindow) # Important that it's QMainWindow, and not QWidget/QDialog
# Go through main window's children to find any previous instances
for obj in mayaMainWindow.children():
if type( obj ) == maya.app.general.mayaMixin.MayaQDockWidget:
#if obj.widget().__class__ == self.__class__: # Alternatively we can check with this, but it will fail if we re-evaluate the class
if obj.widget().objectName() == self.__class__.toolName: # Compare object names
# If they share the same name then remove it
print 'Deleting instance {0}'.format(obj)
mayaMainWindow.removeDockWidget(obj) # This will remove from right-click menu, but won't actually delete it! ( still under mainWindow.children() )
# Delete it for good
obj.setParent(None)
obj.deleteLater()
# Show window with docking ability
def run(self):
self.show(dockable = True)
myWin = MyWindow()
myWin.run()
With this it's no longer polluting Maya's environment anymore. Hope it helps!
Related
I would like to understand how Python's and PyQt's garbage collectors work. In the following example, I create a QWidget (named TestWidget) that has a python attribute 'x'. I create TestWidget, interact with it, then close its window. Since I have set WA_DeleteOnClose, this should signal the Qt event loop to destroy my instance of TestWidget. Contrary to what I expect, at this point (and even after the event loop has finished) the python object referenced by TestWidget().x still exists.
I am creating an app with PyQt where the user opens and closes many many widgets. Each widget has attributes that take up a substantial amount of memory. Thus, I would like to garbage collect (in both Qt and Python) this widget and its attributes when the user closes it. I have tried overriding closeEvent and deleteEvent to no success.
Can someone please point me in the right direction? Thank you! Example code below:
from PyQt5.QtWidgets import QWidget, QVBoxLayout, QTextEdit
from PyQt5.QtCore import Qt
class TestWidget(QWidget):
def __init__(self, parent, **kwargs):
super().__init__(parent, **kwargs)
self.setAttribute(Qt.WA_DeleteOnClose)
self.widget = None
self.x = '1' * int(1e9)
def load(self):
layout = QVBoxLayout(self)
self.widget = QTextEdit(parent=self)
layout.addWidget(self.widget)
self.setLayout(layout)
if __name__ == '__main__':
from PyQt5.QtWidgets import QApplication
import gc
app = QApplication([])
widgets = []
widgets.append(TestWidget(parent=None))
widgets[-1].load()
widgets[-1].show()
widgets[-1].activateWindow()
app.exec()
print(gc.get_referrers(gc.get_referrers(widgets[-1].x)[0]))
An important thing to remember is that PyQt is a binding, any python object that refers to an object created on Qt (the "C++ side") is just a wrapper.
The WA_DeleteOnClose only destroys the actual QWidget, not its python object (the TestWidget instance).
In your case, what's happening is that Qt releases the widget, but you still have a reference on the python side (the element in your list): when the last line is executed, the widgets list and its contents still exist in that scope.
In fact, you can try to add the following at the end:
print(widgets[-1].objectName())
And you'll get the following exception:
Exception "unhandled RuntimeError"
wrapped C/C++ object of type TestWidget has been deleted
When also the python object is deleted, then all its attributes are obviously deleted as well.
To clarify, see the following:
class Attribute(object):
def __del__(self):
print('Deleting Attribute...')
class TestWidget(QWidget):
def __init__(self, parent, **kwargs):
super().__init__(parent, **kwargs)
self.setAttribute(Qt.WA_DeleteOnClose)
self.widget = None
self.x = Attribute()
def load(self):
layout = QVBoxLayout(self)
self.widget = QTextEdit(parent=self)
layout.addWidget(self.widget)
self.setLayout(layout)
def __del__(self):
print('Deleting TestWidget...')
You'll see that __del__ doesn't get called in any case with your code.
Actual deletion will happen if you add del widgets[-1] instead.
Explanation
To understand the problem, you must know the following concepts:
Python objects are eliminated only when they no longer have references, for example in your example when adding the widget to the list then a reference is created, another example is that if you make an attribute of the class then a new reference is created.
PyQt (and also PySide) are wrappers of the Qt library (See 1 for more information), that is, when you access an object of the QFoo class from python, you do not access the C++ object but the handle of that object. For this reason, all the memory logic that Qt creates is handled by Qt but those that the developer creates has to be handled by himself.
Considering the above, what the WA_DeleteOnClose flag does is eliminate the memory of the C++ object but not the python object.
To understand how memory is being handled, you can use the memory-profiler tool with the following code:
from PyQt5.QtWidgets import QWidget, QVBoxLayout, QTextEdit
from PyQt5.QtCore import Qt, QTimer
from PyQt5 import sip
class TestWidget(QWidget):
def __init__(self, parent, **kwargs):
super().__init__(parent, **kwargs)
self.setAttribute(Qt.WA_DeleteOnClose)
self.widget = None
self.x = ""
period = 1000
QTimer.singleShot(4 * period, self.add_memory)
QTimer.singleShot(8 * period, self.close)
QTimer.singleShot(12 * period, QApplication.quit)
def load(self):
layout = QVBoxLayout(self)
self.widget = QTextEdit()
layout.addWidget(self.widget)
def add_memory(self):
self.x = "1" * int(1e9)
if __name__ == "__main__":
from PyQt5.QtWidgets import QApplication
import gc
app = QApplication([])
app.setQuitOnLastWindowClosed(False)
widgets = []
widgets.append(TestWidget(parent=None))
widgets[-1].load()
widgets[-1].show()
widgets[-1].activateWindow()
app.exec()
In the previous code, in second 4 the memory of "x" is created, in second 8 the C++ object is eliminated but the memory associated with "x" is not eliminated and this is only eliminated when the program is closed since it is clear the list and hence the python object reference.
Solution
In this case a possible solution is to use the destroyed signal that is emitted when the C++ object is deleted to remove all references to the python object:
from PyQt5.QtWidgets import QWidget, QVBoxLayout, QTextEdit
from PyQt5.QtCore import Qt, QTimer
from PyQt5 import sip
class TestWidget(QWidget):
def __init__(self, parent=None):
super().__init__(parent)
self.setAttribute(Qt.WA_DeleteOnClose)
self.widget = None
self.x = ""
period = 1000
QTimer.singleShot(4 * period, self.add_memory)
QTimer.singleShot(8 * period, self.close)
QTimer.singleShot(12 * period, QApplication.quit)
def load(self):
layout = QVBoxLayout(self)
self.widget = QTextEdit()
layout.addWidget(self.widget)
def add_memory(self):
self.x = "1" * int(1e9)
class Manager:
def __init__(self):
self._widgets = []
#property
def widgets(self):
return self._widgets
def add_widget(self, widget):
self._widgets.append(widget)
widget.destroyed.connect(self.handle_destroyed)
def handle_destroyed(self):
self._widgets = [widget for widget in self.widgets if not sip.isdeleted(widget)]
if __name__ == "__main__":
from PyQt5.QtWidgets import QApplication
app = QApplication([])
app.setQuitOnLastWindowClosed(False)
manager = Manager()
manager.add_widget(TestWidget())
manager.widgets[-1].load()
manager.widgets[-1].show()
manager.widgets[-1].activateWindow()
app.exec()
I'm getting this weird result when using QMenuBar I've used this exact code before for the QMenuBar and it worked perfectly. But it doesn't show more than 1 QMenu
This is my code:
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
from PyQt5.QtGui import *
import sys
from functools import partial
class MainMenu(QWidget):
def __init__(self, parent = None):
super(MainMenu, self).__init__(parent)
# background = QWidget(self)
lay = QVBoxLayout(self)
lay.setContentsMargins(5, 35, 5, 5)
self.menu()
self.setWindowTitle('Control Panel')
self.setWindowIcon(self.style().standardIcon(getattr(QStyle, 'SP_DialogNoButton')))
self.grid = QGridLayout()
lay.addLayout(self.grid)
self.setLayout(lay)
self.setMinimumSize(400, 320)
def menu(self):
menubar = QMenuBar(self)
viewMenu = menubar.addMenu('View')
viewStatAct = QAction('Dark mode', self, checkable=True)
viewStatAct.setStatusTip('enable/disable Dark mode')
viewMenu.addAction(viewStatAct)
settingsMenu = menubar.addMenu('Configuration')
email = QAction('Set Email', self)
settingsMenu.addAction(email)
if __name__ == '__main__':
app = QApplication(sys.argv)
main = MainMenu()
main.show()
sys.exit(app.exec_())
Result:
I am aware that I am using QWidget when I should be using QMainWindow But is there a workaround???
(I apologize in advance for the terrible quality of the image, there is no good way to take a picture of a QMenuBar)
The problem is that with a QWidget you are not using the "private" layout that a QMainWindow has, which automatically resizes specific children widgets (including the menubar, the statusbar, the dock widgets, the toolbars and, obviously, the "centralWidget").
Remember that a QMainWindow has its own layout (which can't and shouldn't be changed), because it needs that specific custom layout to lay out the aforementioned widgets. If you want to set a layout for the main window, you'll need to apply it to its centralWidget.
Read carefully how the Main Window Framework behaves; as the documentation reports:
Note: Creating a main window without a central widget is not supported. You must have a central widget even if it is just a placeholder.
In order to work around that when using a basic QWidget, you'll have to manually resize the children widgets accordingly. In your case, you only need to resize the menubar, as long as you have a reference to it:
def menu(self):
self.menubar = QMenuBar(self)
# any other function has to be run against the *self.menubar* object
viewMenu = self.menubar.addMenu('View')
# etcetera...
def resizeEvent(self, event):
# calling the base class resizeEvent function is not usually
# required, but it is for certain widgets (especially item views
# or scroll areas), so just call it anyway, just to be sure, as
# it's a good habit to do that for most widget classes
super(MainMenu, self).resizeEvent(event)
# now that we have a direct reference to the menubar widget, we are
# also able to resize it, allowing all actions to be shown (as long
# as they are within the provided size
self.menubar.resize(self.width(), self.menubar.height())
Note: you can also "find" the menubar by means of self.findChild(QtWidgets.QMenuBar) or using the objectName, but using an instance attribute is usually an easier and better solution.
Set minimum width
self.setMinimumSize(320,240)
For a project I'm creating a GUI using Python 3 and PyQt5. Because it has to be usable by people outside of my immediate team, I want to disable actions on the menu until they've already filled out some forms in other parts of the program (e.g. disabling the final solution view when they haven't set up the initial data connection). The issue is that when I try to call the QAction's setEnabled function outside of the function that created it (but still inside the overall class), it's causing my script to crash with no error code, so I'm having trouble understanding the issue. In the snipit below, I'm trying to set the "View Solution" menu option as true. There are some more options in that menu, but I deleted them here to make it more easy to read.
The code is structured something like this:
import sys
from PyQt5.QtWidgets import QMainWindow, QAction, qApp, QApplication, QMessageBox, QStackedLayout
class MediaPlanner(QMainWindow):
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
# Menu bar example from: zetcode.com/gui/pyqt5/
exitAction = QAction('&Exit', self)
exitAction.setShortcut('Ctrl+Q')
exitAction.setStatusTip('Exit application')
exitAction.triggered.connect(self.close)
newProject = QAction('&New Project', self)
newProject.setShortcut('Ctrl+N')
newProject.setStatusTip('Start A New Project')
newProject.triggered.connect(self.createNewProject)
openProject = QAction('&Open Project',self)
openProject.setShortcut('Ctrl+O')
openProject.setStatusTip('Open A Saved Project')
openProject.setEnabled(False)
viewSolution = QAction('&View Solution',self)
viewSolution.setStatusTip('View the Current Solution (If Built)')
viewSolution.setEnabled(False)
self.statusBar()
menubar = self.menuBar()
filemenu = menubar.addMenu('&File')
filemenu.addAction(newProject)
filemenu.addAction(openProject)
filemenu.addAction(exitAction)
viewmenu = menubar.addMenu('&View')
viewmenu.addAction(viewSolution)
self.setGeometry(300,300,700,300)
self.setWindowTitle('Menubar')
self.show()
def createNewProject(self):
print('Project Created')
self.viewSolution.setEnabled(True)
if __name__ == '__main__':
app = QApplication(sys.argv)
gui = MediaPlanner()
sys.exit(app.exec_())
The problem is that viewSolution is a variable, but it is not a member of the class so you will not be able to access it through the self instance. One possible solution is to make viewSolution member of the class as shown below:
self.viewSolution = QAction('&View Solution',self)
self.viewSolution.setStatusTip('View the Current Solution (If Built)')
self.viewSolution.setEnabled(False)
...
viewmenu.addAction(self.viewSolution)
Another possible solution is to use the sender() function, this function returns the object that emits the signal, using the following:
def createNewProject(self):
print('Project Created')
self.sender().setEnabled(True)
I have an application which has a main window, which can have multiple subwindows. I would like to have one set of QActions in the main window that interact with the currently selected window. For example, the application might be a text editor, and clicking file->save should save the text file the user is currently working on. Additionally, some QActions are checkable, so their checked state should reflect the state of the currently active window.
Here is a minimum working example that has the basic functionality I want, but I suspect there is a better way to do it (further discussion below the code).
import sys
import PyQt4.QtGui as QtGui
class DisplayWindow(QtGui.QWidget):
def __init__(self, parent=None, name="Main Window"):
# run the initializer of the class inherited from
super(DisplayWindow, self).__init__()
self.myLayout = QtGui.QFormLayout()
self.FooLabel = QtGui.QLabel(self)
self.FooLabel.setText(name)
self.myLayout.addWidget(self.FooLabel)
self.setLayout(self.myLayout)
self.is_foo = False
def toggle_foo(self):
self.is_foo = not self.is_foo
if self.is_foo:
self.FooLabel.setText('foo')
else:
self.FooLabel.setText('bar')
class WindowActionMain(QtGui.QMainWindow):
def __init__(self):
super(WindowActionMain, self).__init__()
self.fooAction = QtGui.QAction('Foo', self)
self.fooAction.triggered.connect(self.set_foo)
self.fooAction.setCheckable(True)
menubar = self.menuBar()
fileMenu = menubar.addMenu('&File')
fileMenu.addAction(self.fooAction)
self.toolbar = self.addToolBar('File')
self.toolbar.addAction(self.fooAction)
self.centralZone = QtGui.QMdiArea()
self.centralZone.subWindowActivated.connect(
self.update_current_window)
self.setCentralWidget(self.centralZone)
self.create_dw("Window 1")
self.create_dw("Window 2")
def create_dw(self, name):
dw = DisplayWindow(name=name)
self.centralZone.addSubWindow(dw)
dw.show()
def update_current_window(self):
""" redirect future actions to affect the newly selected window,
and update checked statuses to reflect state of selected window"""
current_window = self.centralZone.activeSubWindow()
if current_window:
self.current_dw = self.centralZone.activeSubWindow().widget()
self.fooAction.setChecked(self.current_dw.is_foo)
def set_foo(self):
self.current_dw.toggle_foo()
if __name__ == "__main__":
app = QtGui.QApplication(sys.argv)
ex = WindowActionMain()
ex.show()
sys.exit(app.exec_())
My actual version of DisplayWindow could be useful in many different projects, and I want to package it up so that you don't have to add a lot of code to the main window to use it. Therefore, DisplayWindow, all of its functionality and a list of available actions should be in one module, which would be imported in WindowActionMain's module. I should then be able to add more actions for DisplayWindow without changing any code in WindowActionMain. In particular, I don't want to have to write a little function like WindowActionMain.set_foo(self) just to redirect each action to the right place.
Yes, this is possible by handling the QMenu's aboutToShow signal
and considering the QGuiApplication's focusWindow (or however you get that in Qt4).
Example below shows a generic 'Window' menu acting on the frontmost window.
http://doc.qt.io/qt-4.8/qmenu.html#aboutToShow
http://doc.qt.io/qt-5/qguiapplication.html#focusWindow
def on_windowMenu_aboutToShow(self):
self.windowMenu.clear()
self.newWindowAction = QtWidgets.QAction(self)
self.newWindowAction.setShortcut("Ctrl+n")
self.newWindowAction.triggered.connect(self.on_newWindowAction)
self.newWindowAction.setText("New Window")
self.windowMenu.addAction(self.newWindowAction)
self.windowMenu.addSeparator()
playerWindows = [w for w in self.topLevelWindows() if w.type()==QtCore.Qt.Window and w.isVisible()]
for i, w in enumerate(playerWindows):
def action(i,w):
a = QtWidgets.QAction(self)
a.setText("Show Window {num} - {title}".format(num=i+1, title=w.title()))
a.triggered.connect(lambda : w.requestActivate())
a.triggered.connect(lambda : w.raise_())
self.windowMenu.addAction(a)
action(i,w)
self.windowMenu.addSeparator()
self.closeWindowAction = QtWidgets.QAction(self)
self.closeWindowAction.setShortcut("Ctrl+w")
self.closeWindowAction.triggered.connect(lambda : self.focusWindow().close())
self.closeWindowAction.setText("Close")
self.windowMenu.addAction(self.closeWindowAction)
I have an extended main window with a QtGui.QTabWidget added to it. I am creating several widgets extended from QtGui.QWidget which I can add and remove to the tab widget.
What I would like to do is have a "pop-out" button that causes the child widget to be removed from the tab widget and come up as it's own independent window (and a "pop-in" button to put it back into the main window). The same sort of idea as Gtalk-in-Gmail has.
Note that if I close the main window, the other "tabs" or "windows" should also close, and I should be able to put all the windows side-by-side and have them all visible and updating at the same time. (I will be displaying near-realtime data).
I am new to Qt, but if I'm not mistaken, if a Widget has no parent it comes up independently. This works, but I then have no idea how I could "pop" the window back in.
class TCWindow(QtGui.QMainWindow):
.
.
.
def popOutWidget(self, child):
i = self.tabHolder.indexOf(child)
if not i == -1:
self.tabCloseRequested(i)
self.widgets[i].setParent(None)
self.widgets[i].show()
My gut says that there should still be a parent/child relationship between the two.
Is there a way to keep the parent but still have the window come up independently, or am I misunderstanding Qt's style?
Otherwise, would creating a variable in the child to hold a link to the main window (like self.parentalUnit = self.parent()) be a good idea or a hackish/kludgy idea?
Leave the parent as is. If you remove the parent, then closing main window won't close 'floating' tabs, since they are now top-level windows. windowFlags defines if a widget is window or a child widget. Basically, you need to alternate between QtCore.Qt.Window and QtCore.Qt.Widget
Below is a small but complete example:
#!/usr/bin/env python
# -.- coding: utf-8 -.-
import sys
from PySide import QtGui, QtCore
class Tab(QtGui.QWidget):
popOut = QtCore.Signal(QtGui.QWidget)
popIn = QtCore.Signal(QtGui.QWidget)
def __init__(self, parent=None):
super(Tab, self).__init__(parent)
popOutButton = QtGui.QPushButton('Pop Out')
popOutButton.clicked.connect(lambda: self.popOut.emit(self))
popInButton = QtGui.QPushButton('Pop In')
popInButton.clicked.connect(lambda: self.popIn.emit(self))
layout = QtGui.QHBoxLayout(self)
layout.addWidget(popOutButton)
layout.addWidget(popInButton)
class Window(QtGui.QWidget):
def __init__(self, parent=None):
super(Window, self).__init__()
self.button = QtGui.QPushButton('Add Tab')
self.button.clicked.connect(self.createTab)
self._count = 0
self.tab = QtGui.QTabWidget()
layout = QtGui.QVBoxLayout(self)
layout.addWidget(self.button)
layout.addWidget(self.tab)
def createTab(self):
tab = Tab()
tab.setWindowTitle('%d' % self._count)
tab.popIn.connect(self.addTab)
tab.popOut.connect(self.removeTab)
self.tab.addTab(tab, '%d' % self._count)
self._count += 1
def addTab(self, widget):
if self.tab.indexOf(widget) == -1:
widget.setWindowFlags(QtCore.Qt.Widget)
self.tab.addTab(widget, widget.windowTitle())
def removeTab(self, widget):
index = self.tab.indexOf(widget)
if index != -1:
self.tab.removeTab(index)
widget.setWindowFlags(QtCore.Qt.Window)
widget.show()
if __name__ == '__main__':
app = QtGui.QApplication(sys.argv)
w = Window()
w.show()
sys.exit(app.exec_())
In Qt, layout takes ownership over the widgets that are added to layout, so let it handle parentship.
You can create another widget (with no parent) which will be hidden until you press pop-out button and when it is pressed, you remove "pop-out widget" from its original layout and add it to layout of the hidden widget. And when pop-in button pressed - return widget to it's original layout.
To close this hidden window, when closing main window, you will need to redefine closeEvent(QCloseEvent* ev) to something like this (sorry for c++, but i bet, in python it's all the same):
void MainWindow::closeEvent(QCloseEvent* ev)
{
dw->setVisible(false); // independent of mainwindow widget
sw->setVisible(false); // independent of mainwindow widget
QWidget::closeEvent(ev); //invoking close event after all the other windows are hidden
}