Selecting the top-most item of a QTreeView - python

I have a Treeview (inherits from QTreeView) with model QFileSystemModel. This Treeview object is added to another widget with a QPushButton. The button will open a directory through a QFileDialog, and call the Treeview's set_directory method. The method will set the root path of the model to the selected directory and will modify the root index of the tree view. The top-most item in the treeview is then selected.
However, when selecting a directory for the first time, the top-most item is not selected. Commenting out line 11: self.model.directoryLoaded.connect(self.set_directory), solves the issue. But this means the set_directory method is called twice:
# default directory opened
<class 'NoneType'>
# open new directory, set_directory called twice
<class 'NoneType'>
<class 'PyQt5.QtWidgets.QFileSystemModel'>
As seen on the command line output, the QModelIndex.model() method returns a NoneType when selecting a directory for the first time. How do I set the treeview's current index to the top most item without calling the set_directory method twice? And why is the QModelIndex model a NoneType when the directory has not been visited?
Main script below:
from PyQt5.QtWidgets import QPushButton, QTreeView, QFileSystemModel, QWidget, QFileDialog, QApplication, QHBoxLayout
import sys
class Treeview(QTreeView):
def __init__(self, parent=None):
super(QTreeView, self).__init__(parent)
self.model = QFileSystemModel()
self.setModel(self.model)
self.set_directory(".")
# self.model.directoryLoaded.connect(self.set_directory)
def set_directory(self, path):
self.setRootIndex(self.model.setRootPath(path))
self.setCurrentIndex(self.model.index(0, 0, self.rootIndex()))
print(type(self.model.index(0, 0, self.rootIndex()).model()))
class MainWidget(QWidget):
def __init__(self, parent=None):
super(QWidget, self).__init__(parent)
self.tview = Treeview(self)
vlayout = QHBoxLayout(self)
vlayout.addWidget(self.tview)
open_button = QPushButton(self)
open_button.clicked.connect(self.open_dir)
vlayout.addWidget(open_button)
def open_dir(self):
filepath = QFileDialog.getExistingDirectory(self)
if filepath:
self.tview.set_directory(filepath)
if __name__ == '__main__':
app = QApplication(sys.argv)
widget = MainWidget()
widget.show()
sys.exit(app.exec_())

There are three problems:
directoryLoaded is called whenever the contents of a directory are loaded the first time, which happens asynchronously on a separate thread; if the contents of the "root" directory has never been loaded yet, it will have no children yet, so any attempt to access the children of the root index will return invalid indexes (that's why you get None);
the signal is also sent whenever a directory is expanded in the tree for the first time, which creates a problem if you connect it to set_directory: if you open the folder "home" and then expand the directory "documents", the model will load all its contents and emit the directoryLoaded for the "documents" folder, which will then call set_directory in turn once again;
the file system model also sorts items (alphabetically by default, with directories first), and this means that it will need some more time to get what you believe is the first item: for performance reasons, the OS returns the contents of a directory depending on the file system, which depends on its implementation: sometimes it's sorted by creation/modification time, others by the physical position/index of the blocks, etc;
Considering the above, you cannot rely on directoryLoaded (and surely you should not connect it to set_directory), but you can use the layoutChanged signal, since it's always emitted whenever sorting has completed (and QFileSystemModel always sorts the model when the root changes); the only catch is that you must do it only when needed.
The solution is to create a function that tries to set the top item, if it's not valid then it will connect itself to the layoutChanged signal; at that point, the signal will be emitted when the model has completed its job, and the top index has become available. Using a flag helps us to know if the signal has been connected, and then disconnect it, which is important in case you need to support sorting.
class Treeview(QTreeView):
layoutCheck = False
def __init__(self, parent=None):
super(QTreeView, self).__init__(parent)
self.model = QFileSystemModel()
self.setModel(self.model)
self.set_directory(QDir.currentPath())
self.setSortingEnabled(True)
def setTopIndex(self):
topIndex = self.model.index(0, 0, self.rootIndex())
print(topIndex.isValid(), topIndex.model())
if topIndex.isValid():
self.setCurrentIndex(topIndex)
if self.layoutCheck:
self.model.layoutChanged.disconnect(self.setTopIndex)
self.layoutCheck = False
else:
if not self.layoutCheck:
self.model.layoutChanged.connect(self.setTopIndex)
self.layoutCheck = True
def set_directory(self, path):
self.setRootIndex(self.model.setRootPath(path))
self.setTopIndex()
Please consider that the layoutCheck flag is very important, for many reasons:
as explained before, layoutChanged is always emitted when the model is sorted; this not only happens when trying to sort using the headers, but also when files or directories are being added;
signal connections are not exclusive, and you can even connect the same signal to the same slot/function more than once, with the result that the function will be called as many time as it's been connected; if you're not very careful, you could risk recursion;
the flag works fine also for empty directories, avoiding the above risk of recursion; if a new root directory is opened and it's empty, it will obviously return an invalid index for the top item (since there's none), but the signal will be disconnected in any case: if another directory (with contents) is opened but not yet loaded, the signal won't be connected again, and if, instead, the contents have been already loaded, it will disconnect it no matter what;
A possibility is to use the Qt.UniqueConnection connection type and a try block for the disconnection, but, while this approach works and is consistent, it's a bit cumbersome: as long as the connection is always paired with the setting, using a basic boolean flag is much simpler and easier to read and understand.

Related

In PyQT why somes widgets needs the "self" parameter before calling them, while others don't

I'm a little bit confused about the use of the "self" parameter with some widgets like (QLineEdit), indeed when learning to use the QLabel widget, I used to call the class without the self paramater, or when using the QLineEdit widget, the widget wouldn't work without the "self" parameter, here's the code I'm working on :
# Import necessary modules
import sys
from PyQt5.QtWidgets import QApplication, QWidget, QLabel, QLineEdit, QPushButton
from PyQt5.QtCore import Qt
class EntryWindow(QWidget): # Inherits QWidget
def __init__(self): # Constructor
super().__init__() # Initializer which calls constructor for QWidget
self.initializeUI() # Call function used to set up window
def initializeUI(self):
"""
Initialize the window and display its contents to the screen
"""
self.setGeometry(400, 300, 400, 200)
self.setWindowTitle('QLineEdit Widget')
self.displayWidgets()
self.show() # Show everything
def displayWidgets(self):
'''
Setup the QLineEdit and other widgets.
'''
# Create name label and line edit widgets
QLabel("Please enter your name below.", self).move(100, 20)
name_label = QLabel("Name:", self)
name_label.move(55, 70)
self.name_entry = QLineEdit(self)
self.name_entry.move(120, 68)
self.name_entry.resize(200, 25) # Change size of entry field
self.name_entry.setAlignment(Qt.AlignLeft) # The default alignment
text_font = self.name_entry.font() # Get font option from the Qlineedit
text_font.setPointSize(12) # Modify font size
#text_font.setBold(True) # Bold
self.name_entry.setFont(text_font) # Apply font
self.clear_button = QPushButton('Clear text', self)
self.clear_button.clicked.connect(self.clearEntries)
self.clear_button.move(120, 130)
self.exit_button = QPushButton("Exit", self)
self.exit_button.clicked.connect(self.exitApplication)
self.exit_button.move(240, 130)
def clearEntries(self):
sender = self.sender()
if sender.text() == 'Clear text':
self.name_entry.clear()
def exitApplication(self):
sender = self.sender()
if sender.text() == "Exit":
self.close() # Close the window
# Run program
if __name__ == '__main__':
app = QApplication(sys.argv)
window = EntryWindow()
sys.exit(app.exec_())
So here is where I'm confused, when using QLabel, I didn't have to put the "self" parameter before, or when using QLineEdit, I had to put "self" otherwise my code wouldn't work :
QLabel("Please enter your name below.", self).move(100, 20)
self.name_entry = QLineEdit(self)
First of all, the problem or difference has nothing to do with the "self", but what it is for, pre-established rules in Qt design.
In Qt there is a hierarchy tree between the QObjects where it is established that the parent QObject manages the memory (the life cycle of its children) so if the parent deleted the children, they will also be deleted. This allows avoiding memory leaks since many QObjects are generally used in many applications.
On the other hand, that concept of kinship is also passed to QWidgets since they are also QObjects, but there is also another characteristic: a QWidget in general will be drawn on top of its parent. So if you want the QLineEdit and QLabel to be part of the window then they must be children of the window so it is necessary that you pass the window object which is "self" as parent.
So when you go to the window (in this case "self") you avoid 2 problems:
That the object has a longer life cycle (the same as the window).
And you make the widget (either QLabel or QLineEdit) be placed on top of the window.
The self name is a Python convention to indicate the instance of an object. When set as argument in an object constructor, it's normally used to give the new object a reference to the one that created it, and that's normally called a "parent". Note that while self is usually used as parent argument, you can set any QWidget instance as parent.
In Qt terms, not only it indicates the roles in a "parentship" of the object structure, but also results in two important aspects:
In the world of QObjects (from which QWidget inherits) this helps for memory management: when a parent is deleted, all its children objects are deleted as well. It's also useful to find child objects (using findChild() or findChildren()) which are direct children or [great, ...] grandchildren of another.
When creating a widget with a QWidget parent, that widget is also only "drawn inside" it: it can only be visible within the boundaries of the parent, not outside it; note that this is usually optional, as most times you'll add a widget to a layout manager (but you didn't), which automatically reparents the widget to the widget that is managed by that layout: if it was a child of another widget, it's removed from that and added to the new one. Also consider that if a widget has no parent and is not added to a layout, it's considered a "top level widget" (a standalone window), and if show() or setVisible(True) is called it will have its own window.
This means that if the widget has a parent and the parent becomes visible, the child will be automatically visible too (unless hide() or setVisible() was explicitly called).
Note that while a QWidget that has no parent is always a top level window, a widger that has a parent could be a top level window, and that happens if it has the QtCore.Qt.Window flag set (using the f keyword in the constructor, or with setWindowFlags()). This is useful when the widget has to be a top level window, but the parentship must still be preserved, not only for memory management (when the parent window is closed and deleted, any child should be deleted as well) but for interaction purposes: for example, with QDialogs (which are standalone windows) it's important to set the parent, so that they can be modal to that parent, meaning that no keyboard/mouse interaction can happen on the parent until the dialog is closed.
Finally, it's important to remember that creating widgets "on the fly" in python has a (sometimes perceived incoherent) result: if no python reference is given to the widget but the widget has a parent, it won't garbage collected.
Consider the following:
def test1(self):
someDialog = QtWidgets.QDialog()
someDialog.show()
This will probably show a window just for an instant, and then it will disappear instantly.
def test2(self):
someDialog = QtWidgets.QDialog(self)
someDialog.show()
In this case, the dialog will not disappear, that's because it has a parent, PyQt knows it, and it will not try to delete it.
Even this is actually valid (but, no, don't do it):
def test2b(self):
QtWidgets.QDialog(self).show()
Similar result, but only apparently:
def test3(self):
self.someDialog = QtWidgets.QDialog()
self.someDialog.show()
In the case above, the visual result will be probably the same as test2, but if test3 is called again while the dialog is still visible, it will delete the current one and create a new one: that's because the dialog has no parent, and overwriting self.someDialog with a new instance will cause python to release the previous one, which will tell Qt that it can safely erase it, since it has no parent.
Finally, it's not necessary to set the parent to widgets, as long as they are added to a layout (which is the preferred approach, as using fixed geometries is highly discouraged), since the layout will automatically set the parent anyway as soon as it's [going to be] set on the actual parent.
Note that QDialog is normally shown with exec_() in order to correctly display it as modal of the parent and use it as it should.
Note the import statements at the top
from PyQt5.QtWidgets import QApplication, QWidget, QLabel, QLineEdit, QPushButton
QLabel and QLineEdit as can be seen in the documentations
https://doc.qt.io/qtforpython/PySide2/QtWidgets/QLabel.html
https://doc.qt.io/qtforpython/PySide2/QtWidgets/QLineEdit.html
both take a QWidget as a parameter. The self passed as the second parameter indicates the current QWidget type. For other methods inside the class, self is used to invoke the instance methods.

Getting parent of wrapInstance causes window to break

I've been using a modified version of this code to wrap a Qt window inside Maya's own workspaceControl. To get the outside level of the widget to query geometry, size, move, etc, I actually need to do self.parent().parent().parent().parent().parent(), since it's wrapped within quite a lot of widgets.
I've been working around one or two various issues caused by doing that, but today I hit something a bit more vital and decided to find exactly what the cause is.
Through testing I've narrowed down the code as much as possible, and I've found it's when attempting to get the parent of shiboken2.wrapInstance. Attempting to create the window after that comes up with an error saying RuntimeError: Internal C++ object (PySide2.QtWidgets.QWidget) already deleted..
import maya.OpenMayaUI as omUI
import pymel.core as pm
import shiboken2
from PySide2 import QtWidgets
win_id = 'test'
#Delete existing window
if pm.workspaceControl(win_id, exists=True):
pm.deleteUI(win_id)
#Create window and setup wrapper
pm.workspaceControl(win_id)
c_pointer = omUI.MQtUtil.findControl(win_id)
parent_wrap = shiboken2.wrapInstance(int(c_pointer), QtWidgets.QWidget)
print parent_wrap.parent()
#Create actual window
#This will error if parent_wrap.parent() was called
win = QtWidgets.QMainWindow(parent_object)
How could I get the parent of the wrap instance without causing issues? I assume it's something to do with unreferencing things from memory prematurely, but I'm not sure how I could go about fixing it.
I found a fix that is kinda dirty but seems to work reliably so far.
So to start with I found if you create two instances of the above parent_wrap, and get the parent before you create the second one, it's possible to pass into the window separately without causing issues. It works up until the point the window is docked/detached, but that deletes the old parents and provides new ones, so the old pointer is no longer valid.
It seemed that providing this parent before the window being initialised allowed me to then regerate it with self.parent().parent().... without breaking the code.
It needs to be updated if the state has changed (floating/docked), and whenever a RuntimeError occurs, but as it's not going to be a huge hit in performance, for simplicity I'm just regenerating it each time it is needed.
To override a function, like move for example, this would be how to use it:
def move(self, x, y):
if self.dockable:
return self._parent_override().move(x, y)
return QtWidgets.QMainWindow.move(self, x, y)
And this is the override class to solve the issue:
def _parent_override(self, create=True):
#Determine if it's a new window, we need to get the C++ pointer again
if not hasattr(self, '__temp_parent'):
base = qt_get_window(self.ID)
else:
base = self.parent()
#Get the correct parent level
if pm.workspaceControl(self.ID, query=True, floating=True):
parent = base.parent().parent().parent().parent()
else:
parent = base.parent().parent()
#Even though this attribute is never used,
#PySide2 is dumb and deletes the parent if it's not set
self.__temp_parent = parent
return parent

QLayout Additem - prevent transfer of ownership

I am having problems with the object references when using the QLayout to arrange my widgets in on bigger window next to each other.
I have the following situation
class MyClass(QObject):
widgetCollection = []
def spawn(self):
widget = MyQWidget() #containing a QLineWidget called "nameEdit"
self.widgetCollection.append(widget)
self._window = QtGui.QWidget()
layout = QtGui.QHBoxLayout()
listView = QtGui.QListWidget()
for equation in self.wigdetCollection:
equationName = equation.nameEdit.text()
item = QtGui.QListWidgetItem(equationName)
listView.addItem(item)
layout.addWidget(listView)
layout.addWidget(widget)
self._window.setWindowTitle("Equation Editor")
self._window.setLayout(layout)
self._window.show()
def respawn(self):
self.spawn()
Each time I call spawn(), I want to add a new widget to the collection. Addionally, I want to open a new window where there is a ListView with all widget names on the left and the newly created widget on the right.
Now the first call to the spawn()-method works like expected. But the second call throws an exeption:
equationName = widget.nameEdit.text()
RuntimeError: wrapped C/C++ object of type QLineEdit has been deleted
I think it has something to do with the line
layout.addWidget(widget)
I have read somewhere that the layout takes ownership of the widget when added as an item. With that I loose the widget as soon as I am out of the scope of the layout-reference (which is local in this case). So it seems that the widget-item in my collection gets deleted too.
Can someone help? How do I prevent that.
The problem is that self._window is replaced every time spawn() is closed: Python discards the old window widget, and self._window is made to reference a new QWidget instance. Since the old window widget is parent to a list view and qlineedit widget (called -- confusingly -- just "widget" in the code), these will be destroyed in the Qt sense of the term -- i.e. their C++ portion will be destroyed. The list view is not referenced anywhere else in the shown code so the Python portion of the list view will be destroyed too, and this is the way it should be.
HOWEVER, the qlineedit is referenced in the class-wide registry (widgetCollection) and so the Python portion of the qlineedit will NOT be destroyed. This means that every time spawn() is called, the previous instance of QLineEdit (via MyQWidget in class-wide widgetCollection) becomes a zombie: it is dead at the C++ Qt level, but it is still alive at the Python level. A zombie's methods can still be accessed, but in many cases you will see the Qt error about C/C++ having been deleted, and you may get a crash or other undefined behavior.
It is not clear from the code posted what is the intent of the widgetCollection, but since the previous item appended will become a zombie during a spawn(), widgetCollection is useless as shown. The solution to the problem depends on details about how the collection is used, but since one can presume that the collection has a purpose, then the issue is the replacement of the window widget: perhaps the widget collection should be a window collection, then when self._window is replaced, the previous window stays alive so the child widgets stay alive too.
Solved it by myself :-)
My initial call to the spawn()-method was like that:
mc = MyClass()
mc.spawn()
First one okay. Now I wanted to spawn another from within inst1. For that I have used
self.spawn()
in the respawn()-method throwing the above mentioned error.
It has to be the following
def respawn(self):
mc = MyClass()
mc.spawn()
I had to create another instance of MyClass that shares the widgetCollection with all other instances. As desired....

pyqt: A correct way to connect multiple signals to the same function in pyqt (QSignalMapper not applicable)

I've ready many posts on how to connect multiple signals to the same event handler in python and pyqt. For example, connecting several buttons or comboboxes to the same function.
Many examples show how to do this with QSignalMapper, but it is not applicable when the signal carries a parameter, as with combobox.currentIndexChanged
Many people suggest it can be made with lambda. It is a clean and pretty solution, I agree, but nobody mentions that lambda creates a closure, which holds a reference - thus the referenced object can not be deleted. Hello memory leak!
Proof:
from PyQt4 import QtGui, QtCore
class Widget(QtGui.QWidget):
def __init__(self):
super(Widget, self).__init__()
# create and set the layout
lay_main = QtGui.QHBoxLayout()
self.setLayout(lay_main)
# create two comboboxes and connect them to a single handler with lambda
combobox = QtGui.QComboBox()
combobox.addItems('Nol Adyn Dwa Tri'.split())
combobox.currentIndexChanged.connect(lambda ind: self.on_selected('1', ind))
lay_main.addWidget(combobox)
combobox = QtGui.QComboBox()
combobox.addItems('Nol Adyn Dwa Tri'.split())
combobox.currentIndexChanged.connect(lambda ind: self.on_selected('2', ind))
lay_main.addWidget(combobox)
# let the handler show which combobox was selected with which value
def on_selected(self, cb, index):
print '! combobox ', cb, ' index ', index
def __del__(self):
print 'deleted'
if __name__ == '__main__':
import sys
app = QtGui.QApplication(sys.argv)
wdg = Widget()
wdg.show()
wdg = None
sys.exit(app.exec_())
The widget is NOT deleted though we clear the reference. Remove the connection to lambda - it gets deleted properly.
So, the question is: which is the proper way to connect several signals with parameters to a single handler without leaking memory?
It is simply untrue that an object cannot be deleted because a signal connection holds a reference in a closure. Qt will automatically remove all signal connections when it deletes an object, which will in turn remove the reference to the lambda on the python side.
But this implies that you cannot always rely on Python alone to delete objects. There are two parts to every PyQt object: the Qt C++ part, and the Python wrapper part. Both parts must be deleted - and sometimes in a specific order (depending on whether Qt or Python currently has ownership of the object). In addition to that, there's also the vagaries of the Python garbage-collector to factor in (especially during the short period when the interpreter is shutting down).
Anyway, in your specific example, the easy fix is to simply do:
# wdg = None
wdg.deleteLater()
This schedules the object for deletion, so a running event-loop is required for it have any effect. In your example, this will also automatically quit the application (because the object is the last window closed).
To more clearly see what's happening, you can also try this:
#wdg = None
wdg.deleteLater()
app.exec_()
# Python part is still alive here...
print(wdg)
# but the Qt part has already gone
print(wdg.objectName())
Output:
<__main__.Widget object at 0x7fa953688510>
Traceback (most recent call last):
File "test.py", line 45, in <module>
print(wdg.objectName())
RuntimeError: wrapped C/C++ object of type Widget has been deleted
deleted
EDIT:
Here's another debugging example that hopefully makes it even clearer:
wdg = Widget()
wdg.show()
wdg.deleteLater()
print 'wdg.deleteLater called'
del wdg
print 'del widget executed'
wd2 = Widget()
wd2.show()
print 'starting event-loop'
app.exec_()
Output:
$ python2 test.py
wdg.deleteLater called
del widget executed
starting event-loop
deleted
in many cases the parameter carried by signal can be catched in another way, e.g. if an objectName is set for the sending object, so QSignalMapper can be used:
self.signalMapper = QtCore.QSignalMapper(self)
self.signalMapper.mapped[str].connect(myFunction)
self.combo.currentIndexChanged.connect(self.signalMapper.map)
self.signalMapper.setMapping(self.combo, self.combo.objectName())
def myFunction(self, identifier):
combo = self.findChild(QtGui.QComboBox,identifier)
index = combo.currentIndex()
text = combo.currentText()
data = combo.currentData()

Is deleteLater() necessary in PyQt/PySide?

Since there is already a Garbage Collector in Python, is deleteLater() necessary in PyQt/PySide?
It depends what you mean by "necessary".
An application could potentially consume a lot of memory if (for example) care is not taken when closing widgets. The QObject-based classes are designed to be (optionally) linked together in a hierarchy. When a top-level object is deleted, Qt will automatically delete all its child objects as well. However, when closing widgets (which are sub-classes of QObject), automatic deletion will only happen if the Qt.WA_DeleteOnClose attribute is set (which, by default, it usually isn't).
To illustrate, try repeatedly opening and closing the dialog in this demo script, and watch how the global list of objects grows:
import sys
from PyQt5 import QtCore, QtWidgets
class Window(QtWidgets.QWidget):
def __init__(self):
super().__init__()
self.checkbox = QtWidgets.QCheckBox('Delete')
self.button = QtWidgets.QPushButton('Open', self)
self.button.clicked.connect(self.openDialog)
layout = QtWidgets.QHBoxLayout(self)
layout.addWidget(self.checkbox)
layout.addWidget(self.button)
def openDialog(self):
widget = QtWidgets.QDialog(self)
if (self.checkbox.isChecked() and
not widget.testAttribute(QtCore.Qt.WA_DeleteOnClose)):
widget.setAttribute(QtCore.Qt.WA_DeleteOnClose)
for child in self.findChildren(QtWidgets.QDialog):
if child is not widget:
child.deleteLater()
label = QtWidgets.QLabel(widget)
button = QtWidgets.QPushButton('Close', widget)
button.clicked.connect(widget.close)
layout = QtWidgets.QVBoxLayout(widget)
layout.addWidget(label)
layout.addWidget(button)
objects = self.findChildren(QtCore.QObject)
label.setText('Objects = %d' % len(objects))
print(objects)
widget.show()
if __name__ == '__main__':
app = QtWidgets.QApplication(sys.argv)
window = Window()
window.setGeometry(500, 300, 100, 50)
window.show()
sys.exit(app.exec_())
With PyQt/PySide, there are two aspects to object ownership: the Python part, and the Qt part. Often, removing the last Python reference to an object won't be enough to fully clean up, because there could still be a reference held on the Qt side.
In general, Qt tends not to implicity delete objects. So if your application creates and removes lots of QObjects (or opens and closes lots of QWidgets), you may need to take steps to delete them explicitly if memory usage is a concern.
UPDATE:
Just to add to the points above on object ownership. Sometimes, it is possible to hold a Python reference to an object, whilst the Qt part gets deleted. When this happens, you will see an error like this:
RuntimeError: underlying C/C++ object has been deleted
Usually, the Qt documentation will give some hints about when this might happen. For instance, QAbstractItemView.setModel gives this warning:
The view does not take ownership of the model unless it is the model's parent object...
This is telling you that you must either keep a Python reference to the object, or pass a suitable parent object to the object's constructor, because Qt will not always automatically reparent it.
One application of deleteLater can be cleaning up of yourself, i.e. scheduling a delete of an QObject (for example in threading) to free resources from within the object itself.
Here for example someone is using it in connection with the signal thread.finished. It might be restricted to cases with heavy signalling though.

Categories