How to solve a tkinter memory leak? - button

I have a dynamic table with a fixed row number (like a FIFO Queue), which updates continuously through tkinter's after() function. Inside the table is a Button, which text is editable.
To make the Button's text editable I used the solution of BrenBarn and reference a loop variable into a function call at the command-attribute.
When the function update_content_items() is cycled, I found, that the memory usage is increasing MB by MB per second. I can confirm that after commenting out the lambda expression, the memory leak was gone. (as seen live running 'top' in the terminal)
It seems I have to use the lambda, otherwise the Button will have a wrong index and the user edits the wrong row, when I just used self.list_items[i], though the user clicked the right one.
Is there a way to solve the problem? How can the user click the right button and edit it while having the right index and getting rid of the leak?
The corresponding code:
def update_content_items(self):
"""
Continuously fills and updates the Table with rows and content.
The size of the table rows is initially fixed by an external value at config.ini
:return: nothing
"""
if len(self.list_items) > self.queueMaxlen:
self.queueMaxlen = len(self.list_items)
self.build_table()
try:
for i in range(len(self.list_items)):
item = self.list_items[i]
self.barcodeImgList[i].image = item.plateimage
orig_image = Image.open(io.BytesIO(item.plateimage))
ein_image = ImageTk.PhotoImage(orig_image)
self.barcodeImgList[i].configure(image=ein_image)
# keeps a reference, because somehow tkinter forgets it...??? Bug of my implementation???
self.barcodeImgList[i].image = ein_image
orig_image = None
ein_image = None
#FIXME Memory LEAK?
self.numberList[i].configure(text=item.number,
command=lambda K=i: self.edit_barcode(self.list_items[K]))
self.timestampList[i].configure(text=item.timestamp)
self.search_hitlist[i].config(bg='white', cursor="xterm")
self.search_hitlist[i].unbind("<Button-1>")
if item.queryresult is not None:
if item.queryresult.gesamtstatus != 'Gruen':
self.search_hitlist[i].insert(tk.END, item.queryresult.barcode +
'\n' + item.queryresult.permitlevel)
self.search_hitlist[i].configure(bg='red', cursor="hand2")
self.search_hitlist[i].bind("<Button-1>", item.url_callback)
else:
self.search_hitlist[i].configure(bg='green', cursor="xterm")
self.search_hitlist[i].configure(state=tk.DISABLED)
self.on_frame_configure(None)
self.canvas.after(10, self.update_content_items)
except IndexError as ie:
for number, thing in enumerate(self.list_items):
print(number, thing)
raise ie
def edit_barcode(self, item=None):
"""
Opens the number plate edit dialogue and updates the corresponding list item.
:param item: as Hit DAO
:return: nothing
"""
if item is not None:
new_item_number = EditBarcodeEntry(self.master.master, item)
if new_item_number.mynumber != 0:
item.number = new_item_number.mynumber
self.list_items.request_work(item, 'update')
self.list_items.edit_hititem_by_id(item)
self.parent.master.queryQueue.put(item)
else:
print("You shouldn't get here at all. Please see edit_barcode function.")
EDIT: It seems there is indeed a deeper memory leak (python itself). The images won't get garbage collected. Memory is slowly leaking in Python 3.x and I do use PIL. Also here: Image loading by file name memory leak is not properly fixed
What can I do, because I have to cycle through a list with records and update Labels with images? Is there a workaround? PhotoImage has no explicit close() function, and if I call del, the reference is gc'ed and no configuring of the Label possible.

an example of my proposed changes, with indentation fixed:
def update_content_items(self):
"""
Continuously fills and updates the Table with rows and content.
The size of the table rows is initially fixed by an external value at config.ini
:return: nothing
"""
if len(self.list_items) > self.queueMaxlen:
self.queueMaxlen = len(self.list_items)
self.build_table()
try:
for i in range(len(self.list_items)):
item = self.list_items[i]
self.barcodeImgList[i].image = item.plateimage
orig_image = Image.open(io.BytesIO(item.plateimage))
ein_image = ImageTk.PhotoImage(orig_image)
self.barcodeImgList[i].configure(image=ein_image)
# keeps a reference, because somehow tkinter forgets it...??? Bug of my implementation???
self.barcodeImgList[i].image = ein_image
orig_image = None
ein_image = None
self.numberList[i].configure(text=item.number) # removed lambda
self.numberList[i].bind("<Button-1>", self.edit_barcode_binding) # added binding
self.timestampList[i].configure(text=item.timestamp)
self.search_hitlist[i].config(bg='white', cursor="xterm")
self.search_hitlist[i].unbind("<Button-1>")
if item.queryresult is not None:
if item.queryresult.gesamtstatus != 'Gruen':
self.search_hitlist[i].insert(tk.END, item.queryresult.barcode +
'\n' + item.queryresult.permitlevel)
self.search_hitlist[i].configure(bg='red', cursor="hand2")
self.search_hitlist[i].bind("<Button-1>", item.url_callback)
else:
self.search_hitlist[i].configure(bg='green', cursor="xterm")
self.search_hitlist[i].configure(state=tk.DISABLED)
self.on_frame_configure(None)
self.canvas.after(10, self.update_content_items)
except IndexError as ie:
for number, thing in enumerate(self.list_items):
print(number, thing)
raise ie
def edit_barcode_binding(self, event): # new wrapper for binding
K = self.numberList.index(event.widget) # get index from list
self.edit_barcode(self.list_items[K]) # call the original function
def edit_barcode(self, item=None):
"""
Opens the number plate edit dialogue and updates the corresponding list item.
:param item: as Hit DAO
:return: nothing
"""
if item is not None:
new_item_number = EditBarcodeEntry(self.master.master, item)
if new_item_number.mynumber != 0:
item.number = new_item_number.mynumber
self.list_items.request_work(item, 'update')
self.list_items.edit_hititem_by_id(item)
self.parent.master.queryQueue.put(item)
else:
print("You shouldn't get here at all. Please see edit_barcode function.")

Related

How to reset callback_query value to receive multiple inputs in Python-Telegram-Bot

After the user clicks on one button, the callback_query.data is assigned a value and is no longer able to receive additional inputs and the while loop constantly runs with that value. I am not sure how to either reset the callback_query.data value or to request the user for an additional input.
I would like to receive multiple inputs from the user using InlineKeyboard and only continue to the run code when the user clicks on the done InlineKeyboardButton.
reply_markup = InlineKeyboardMarkup(button_list)
update.message.reply_text('Please choose the headers:', reply_markup=reply_markup)
query = update.callback_query
query_list = query.data.split(" ")
while 'done' not in query_list:
if query.data not in query_list:
query_list.append(update.callback_query.data)
else:
pass
query.answer()
query.edit_message_text(text="Your input has been updated!")
This line of code uses the Python-Telegram-Bot package.
By using ConversationHandler and a while loop, I am able to receive a dynamic number of inputs.
def select(update: Update, context: CallbackContext) -> int:
global reply_markup
reply_markup = InlineKeyboardMarkup(build_menu(button_list, n_cols=1))
update.message.reply_text('Please choose the headers:', reply_markup=reply_markup)
return SECOND
def display(update: Update, context: CallbackContext) -> int:
query = update.callback_query
global query_list
query_list = query.data.split(" ")
context.bot.send_message(chat_id=update.effective_chat.id, text=f"{query.data} is added as a column header")
query.answer()
query.edit_message_text(text="Select other headers or click Done to end the selection!", reply_markup=reply_markup)
return THIRD
def display_2(update: Update, context: CallbackContext) -> int:
query = update.callback_query
while 'done' not in query_list:
if query.data not in query_list and query.data != 'done':
query_list.append(query.data)
context.bot.send_message(chat_id=update.effective_chat.id, text=f"{query.data} is added as a column header")
elif query.data == 'done':
break
else:
text = f"{query.data} is already added as a column header, choose another header or click Done to end the selection!"
context.bot.send_message(chat_id=update.effective_chat.id, text=text)
del update.callback_query.data
return THIRD
query.edit_message_text(text="I have received your input! Type /workout to receive your workout for today!")
return ConversationHandler.END
As seen in display_2 the while loop will bring the user back to the THIRD state of the conversation, which is display_2. I am not sure whether there is a more efficient way in doing this but this achieves my intent.

PyQt signal to track when row in QTableView was moved

I am using a subclassed version of QAbstractItemModel with a QTableView and have drag-and-drop activated with a subclassed model.dropMimeData(), model.insertRows(), model.removeRows(). Now I want to show the changes after a drag-and-drop operation is finished and offer the user to undo the operation again. I therefore implemented my own dropEvent()-method for the tableView. I also set the move method to InternalMove.
I check for a confirmation of the move inside the method and then call super(widget.__class__, widget).dropEvent(event). I would expect that after this execution the row was inserted at the new position and deleted at the old position. What happens is that it inserts the row at the specified position, but it deletes row at the old position only after dropEvent() is finished. It does not matter if I call event.accept() or event.acceptProposedAction() inside the function, it always waits until dropEvent() finishes.
I am looking for a signal that tells me when a drag-and-drop operation was executed. I would expect QAbstractItemModel's rowsMoved-signal to be what I want, but it is not emitted during the dnd-operation. The signals rowsInserted and rowsRemoved are however emitted. But the rowsRemoved signal is just emitted as soon as dropEvent() finishes. Does anybody know where QTableView executes the insertion of the target row, setting of the data and the removal of the source row?
I am using python3 with PyQt5 on Windows 10.
def dropEvent_withConfirm(widget, event):
dropInd = widget.indexAt(event.pos())
if not dropInd.isValid():
event.ignore()
return
confirmGUI = ConfirmGUI("Drag Element to new position?",
"Are you sure you want to drag the element to this position?",
True)
if confirmGUI.getConfirmation():
super(widget.__class__, widget).dropEvent(event)
event.accept()
# Here I want to do something after the finished move, but here the target row was inserted, but the source row is not yet deleted
else:
event.ignore()
self.client.tblView.dropEvent = lambda e: dropEvent_withConfirm(self.client.tblView, e)
I solved it now by not relying on the super().dropEvent(), but by implementing it myself. I couldn't find a fitting signal that is emitted on finalization of the drop. Down below is my updated code:
def dropEvent_withConfirm(widget, event):
dropInd = widget.indexAt(event.pos())
if not dropInd.isValid():
event.ignore()
return
confirmGUI = ConfirmGUI("Drag Element to new position?",
"Are you sure you want to drag the element to this position?",
True)
if confirmGUI.getConfirmation():
old_row, new_row = getSrcAndDstRow()
entry = getDraggedEntry()
self.myModel.insertRows(new_row)
self.myModel.setRow(new_row, entry)
if old_row > new_row:
self.myModel.removeRows(old_row + 1)
else:
self.myModel.removeRows(old_row)
event.accept()
# Here I can now do something after the finished move
else:
event.ignore()
self.client.tblView.dropEvent = lambda e: dropEvent_withConfirm(self.client.tblView, e)

When dragging multiple items from QListWidget, non-draggable items get removed

I have two QListWidgets. The user can select multiple items from one list and drag them to the other list. But within each list, some items are draggable and some are not. If the selection contains both draggable and non-draggable items, a problem happens. Only the draggable items appear in the second list, which is correct. But all the items disappear from the first list.
In the animated image above, items 00, 01, and 02 are selected. Only items 00 and 02 are drag enabled. After the drag-and-drop, all three items are gone from the first list. How can I fix this?
Here is some code to reproduce the problem:
import random
import sys
from PySide import QtCore, QtGui
class TestMultiDragDrop(QtGui.QMainWindow):
def __init__(self, parent=None):
super(TestMultiDragDrop, self).__init__(parent)
centralWidget = QtGui.QWidget()
self.setCentralWidget(centralWidget)
layout = QtGui.QHBoxLayout(centralWidget)
self.list1 = QtGui.QListWidget()
self.list1.setDragDropMode(QtGui.QAbstractItemView.DragDrop)
self.list1.setDefaultDropAction(QtCore.Qt.MoveAction)
self.list1.setSelectionMode(QtGui.QListWidget.ExtendedSelection)
self.list2 = QtGui.QListWidget()
self.list2.setDragDropMode(QtGui.QAbstractItemView.DragDrop)
self.list2.setDefaultDropAction(QtCore.Qt.MoveAction)
self.list2.setSelectionMode(QtGui.QListWidget.ExtendedSelection)
layout.addWidget(self.list1)
layout.addWidget(self.list2)
self.fillListWidget(self.list1, 8, 'someItem')
self.fillListWidget(self.list2, 4, 'anotherItem')
def fillListWidget(self, listWidget, numItems, txt):
for i in range(numItems):
item = QtGui.QListWidgetItem()
newTxt = '{0}{1:02d}'.format(txt, i)
if random.randint(0, 1):
item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
else:
# If the item is draggable, indicate it with a *
newTxt += ' *'
item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsDragEnabled)
item.setText(newTxt)
listWidget.addItem(item)
def openMultiDragDrop():
global multiDragDropUI
try:
multiDragDropUI.close()
except:
pass
multiDragDropUI = TestMultiDragDrop()
multiDragDropUI.setAttribute(QtCore.Qt.WA_DeleteOnClose)
multiDragDropUI.show()
return multiDragDropUI
if __name__ == '__main__':
app = QtGui.QApplication([])
openMultiDragDrop()
sys.exit(app.exec_())
Here I have some suspicion on setDefaultDropAction(QtCore.Qt.MoveAction)
Read below para from documentation: Specially the bold line
In the simplest case, the target of a drag and drop action receives a copy of the data being dragged, and the source decides whether to delete the original. This is described by the CopyAction action. The target may also choose to handle other actions, specifically the MoveAction and LinkAction actions. If the source calls QDrag::exec(), and it returns MoveAction, the source is responsible for deleting any original data if it chooses to do so. The QMimeData and QDrag objects created by the source widget should not be deleted - they will be destroyed by Qt.
(http://doc.qt.io/qt-4.8/dnd.html#overriding-proposed-actions)
First give a try with QtCore.Qt.CopyAction
Second, if MoveAction is mandatory, try creating QMimeData and QDrag objects in your source list widget's mouseMoveEvent.
Here in below link, you can find some help for creating QMimeData and QDrag objects in your source list widget's mouseMoveEvent. (code is in C++, My intention is to get conceptual idea).
http://doc.qt.io/qt-4.8/dnd.html#overriding-proposed-actions
I think Kuba Ober is right that this is a Qt bug. In the C++ source code, there is a function void QAbstractItemViewPrivate::clearOrRemove(). It deletes all selected rows, but it does not look at whether each item is drag-enabled or not.
That being the case, I came up with a few workarounds:
Method 1: Make all non-draggable items non-selectable as well
This is the easiest method. Just remove the QtCore.Qt.ItemIsEnabled flag from all non-draggable items. Of course if you want all of your items to be selectable, this won't work.
Method 2: Recreate the "startDrag" function
Since the clearOrRemove function belongs to a private class, I cannot override it. But that function is called by the startDrag function, which can be overridden. So I essentially duplicated the function in Python and replaced the call to clearOrRemove with my own function removeSelectedDraggableItems.
The problem with this method is that startDrag contains calls to a few other functions belonging to a private class. And those functions call other private class functions. Specifically, these functions are responsible for controlling how the items are drawn during the drag event. Since I didn't want to recreate all the functions, I just ignored those. The result is that this method results in the correct functionality, but it loses the graphical indication of which items are being dragged.
class DragListWidget(QtGui.QListWidget):
def __init__(self):
super(DragListWidget, self).__init__()
def startDrag(self, supportedDragActions):
indexes = self.getSelectedDraggableIndexes()
if not indexes:
return
mimeData = self.model().mimeData(indexes)
if not mimeData:
return
drag = QtGui.QDrag(self)
rect = QtCore.QRect()
# "renderToPixmap" is from a private class in the C++ code, so I can't use it.
#pixmap = renderToPixmap(indexes, rect)
#drag.setPixmap(pixmap)
drag.setMimeData(mimeData)
# "pressedPosition" is from a private class in the C++ code, so I can't use it.
#drag.setHotSpot(pressedPostion() - rect.topLeft())
defaultDropAction = self.defaultDropAction()
dropAction = QtCore.Qt.IgnoreAction
if ((defaultDropAction != QtCore.Qt.IgnoreAction) and
(supportedDragActions & defaultDropAction)):
dropAction = defaultDropAction
elif ((supportedDragActions & QtCore.Qt.CopyAction) and
(self.dragDropMode() != self.InternalMove)):
dropAction = QtCore.Qt.CopyAction
dragResult = drag.exec_(supportedDragActions, dropAction)
if dragResult == QtCore.Qt.MoveAction:
self.removeSelectedDraggableItems()
def getSelectedDraggableIndexes(self):
""" Get a list of indexes for selected items that are drag-enabled. """
indexes = []
for index in self.selectedIndexes():
item = self.itemFromIndex(index)
if item.flags() & QtCore.Qt.ItemIsDragEnabled:
indexes.append(index)
return indexes
def removeSelectedDraggableItems(self):
selectedDraggableIndexes = self.getSelectedDraggableIndexes()
# Use persistent indices so we don't lose track of the correct rows as
# we are deleting things.
root = self.rootIndex()
model = self.model()
persistentIndices = [QtCore.QPersistentModelIndex(i) for i in selectedDraggableIndexes]
for pIndex in persistentIndices:
model.removeRows(pIndex.row(), 1, root)
Method 3: Hack "startDrag"
This method changes the drop action from "MoveAction" to "CopyAction" before calling the built-in "startDrag" method. Then it calls a custom function for deleting the selected drag-enabled items. This solves the problem of losing the graphical dragging animation.
This is a pretty easy hack, but it comes with its own problem. Say the user installs an event filter that changes the drop action from "MoveAction" to "IgnoreAction" in certain cases. This hack code doesn't get the updated value. It will still delete the items as though the action is "MoveAction". (Method 2 does not have this problem.) There are workarounds for this problem, but I won't go into them here.
class DragListWidget2(QtGui.QListWidget):
def startDrag(self, supportedDragActions):
dropAction = self.defaultDropAction()
if dropAction == QtCore.Qt.MoveAction:
self.setDefaultDropAction(QtCore.Qt.CopyAction)
super(DragListWidget2, self).startDrag(supportedDragActions)
if dropAction == QtCore.Qt.MoveAction:
self.setDefaultDropAction(dropAction)
self.removeSelectedDraggableItems()
def removeSelectedDraggableItems(self):
# Same code from Method 2. Removed here for brevity.
pass

How to deselect all items in QTreeWidget?

I have tried every suggestion I can come across online, from setting flags to using selectionModel
def DatabaseLoadWrapper(self,database, init):
self.treeWidget.currentItemChanged.disconnect(self.updateStackedWidget)
self.DatabaseLoad(database, init)
self.treeWidget.clearSelection()
self.treeWidget.setCurrentItem(self.treeWidget.findItems(self.selectedDatabase,Qt.MatchExactly|Qt.MatchRecursive)[0])
self.treeWidget.currentItemChanged.connect(self.updateStackedWidget)
This is where my code needs to force a selection on the QTreeWidget, none of the code I use throws up any errors but also has no effect on the selection. And I end up with this where the user has selected Database 1 but I need to revert back to having only Database 2 selected:
Edit: The Tree Widget is built using this code:
def setupMenu(self):
self.DatabaseParent = QTreeWidgetItem(['Databases'])
for item in NamesInDatabase():
self.DatabaseParent.addChild(QTreeWidgetItem([item]))
self.AverageParent = QTreeWidgetItem(['Averaged Database'])
self.SortingParent = QTreeWidgetItem(['Waste Composition'])
self.ResultParent = QTreeWidgetItem(['Results'])
self.treeWidget.addTopLevelItem(self.DatabaseParent)
self.treeWidget.addTopLevelItem(self.AverageParent)
self.treeWidget.addTopLevelItem(self.SortingParent)
self.treeWidget.addTopLevelItem(self.ResultParent)
It basically is adding databases, averaged database, waste compisition & results, as fixed parts of the navigation menu and then populating children of databases with the names of the databases in the save file.
Your question fails to expose the part of the code that is causing the problem. By default, setting the current item, as you do, also sets the selection. So this code, for example, correctly sets the selection to item "b":
from PySide import QtCore,QtGui
if __name__ == '__main__':
import sys
qApp = QtGui.QApplication(sys.argv)
treeWidget = QtGui.QTreeWidget()
parent = QtGui.QTreeWidgetItem(['Databases'])
items = []
for item_text in ["a","b","c"]:
item = QtGui.QTreeWidgetItem([item_text])
items.append(item)
parent.addChild(item)
treeWidget.addTopLevelItem(parent)
treeWidget.setCurrentItem(items[1])
treeWidget.show()
sys.exit(qApp.exec_())
However, I suspect there is code elsewhere in your project that is affecting this. For example, if you had set the selection mode for the QTableWidget selection model to MultiSelection then selections become cumulative:
from PySide import QtCore,QtGui
if __name__ == '__main__':
import sys
qApp = QtGui.QApplication(sys.argv)
treeWidget = QtGui.QTreeWidget()
parent = QtGui.QTreeWidgetItem(['Databases'])
items = []
for item_text in ["a","b","c"]:
item = QtGui.QTreeWidgetItem([item_text])
items.append(item)
parent.addChild(item)
treeWidget.addTopLevelItem(parent)
treeWidget.setSelectionMode(QtGui.QAbstractItemView.MultiSelection)
treeWidget.setCurrentItem(items[0])
treeWidget.setCurrentItem(items[2])
treeWidget.show()
sys.exit(qApp.exec_())
However, that still doesn't explain your issue because the clearSelection call should have cleared the preceding selection in any case. Further debugging of your code is needed, for example to check that the wrapper function and the setCurrentItem are being called as you claim. Also check what else is being called subsequent to the DatabaseLoadWrapper.
In Pyside2, This works for me:
If you click on treewidget the selection will be clear.
self.treeWidget.setSelectionMode(QtWidgets.QAbstractItemView.ContiguousSelection)

(PyQt) QTreeView - want to expand/collapse all children and grandchildren

I want to be able to expand or collapse all children of a particular branch in a QTreeView. I am using PyQt4.
I know that QTreeView's have an expand all children feature that is bound to *, but I need two things: It needs to be bound to a different key combination (shift-space) and I also need to be able to collapse all children as well.
Here is what I have tried so far:
I have a subclass of a QTreeView wherein I am checking for the shift-space key combo. I know that QModelIndex will let me pick a specific child with the "child" function, but that requires knowing the number of children. I am able to get a count of the children by looking at the internalPointer, but that only gives me info for the first level of the hierarchy. If I try to use recursion, I can get a bunch of child counts, but then I am lost as to how to get these converted back into a valid QModelIndex.
Here is some code:
def keyPressEvent(self, event):
"""
Capture key press events to handle:
- enable/disable
"""
#shift - space means toggle expanded/collapsed for all children
if (event.key() == QtCore.Qt.Key_Space and
event.modifiers() & QtCore.Qt.ShiftModifier):
expanded = self.isExpanded(self.selectedIndexes()[0])
for cellIndex in self.selectedIndexes():
if cellIndex.column() == 0: #only need to call it once per row
#I can get the actual object represented here
item = cellIndex.internalPointer()
#and I can get the number of children from that
numChildren = item.get_child_count()
#but now what? How do I convert this number into valid
#QModelIndex objects? I know I could use:
# cellIndex.child(row, 0)
#to get the immediate children's QModelIndex's, but how
#would I deal with grandchildren, great grandchildren, etc...
self.setExpanded(cellIndex, not(expanded))
return
Here is the beginning of the recursion method I was investigating, but I get stuck when actually trying to set the expanded state because once inside the recursion, I lose "contact" with any valid QModelIndex...
def toggle_expanded(self, item, expand):
"""
Toggles the children of item (recursively)
"""
for row in range(0,item.get_child_count()):
newItem = item.get_child_at_row(row)
self.toggle_expanded(newItem, expand)
#well... I'm stuck here because I'd like to toggle the expanded
#setting of the "current" item, but I don't know how to convert
#my pointer to the object represented in the tree view back into
#a valid QModelIndex
#self.setExpanded(?????, expand) #<- What I'd like to run
print "Setting", item.get_name(), "to", str(expand) #<- simple debug statement that indicates that the concept is valid
Thanks to all for taking the time to look at this!
Ok... siblings did not actually get me to where I wanted to go. I managed to get the code working as follows (and it seems like a decent implementation). Kudos still to Prof.Ebral who got me going on the right track with the idea of siblings (turns out I needed to use QModelIndex.child(row, column) and iterate recursively from there).
Note that there is the following assumption in the code: It assumes that your underlying data store objects have the ability to report how many children they have (get_child_count() in my code). If that is not the case, you will somehow have to get a child count differently... perhaps by just arbitrarily trying to get child indexes - using QModelIndex.child(row, col) - with an ever increasing row count till you get back an invalid index? - this is what Prof.Ebral suggested and I might still try that (It is just that I already have an easy way to get the child count by requesting it from my data store).
Also note that I actually expand/collpase each node at a different point in the recursion based on whether I am expanding or collapsing. This is because, through trial and error, I discovered that animated tree views would stutter and pop if I just did it at one place in the code. Now, by reversing the order in which I do it based on whether I am at the top level (i.e. the root of the branch I am affecting - not the root of the entire treeview) I get a nice smooth animation. This is documented below.
The following code is in a QTreeView subclass.
#---------------------------------------------------------------------------
def keyPressEvent(self, event):
if (event.key() == QtCore.Qt.Key_Space and self.currentIndex().column() == 0):
shift = event.modifiers() & QtCore.Qt.ShiftModifier
if shift:
self.expand_all(self.currentIndex())
else:
expand = not(self.isExpanded(self.currentIndex()))
self.setExpanded(self.currentIndex(), expand)
#---------------------------------------------------------------------------
def expand_all(self, index):
"""
Expands/collapses all the children and grandchildren etc. of index.
"""
expand = not(self.isExpanded(index))
if not expand: #if collapsing, do that first (wonky animation otherwise)
self.setExpanded(index, expand)
childCount = index.internalPointer().get_child_count()
self.recursive_expand(index, childCount, expand)
if expand: #if expanding, do that last (wonky animation otherwise)
self.setExpanded(index, expand)
#---------------------------------------------------------------------------
def recursive_expand(self, index, childCount, expand):
"""
Recursively expands/collpases all the children of index.
"""
for childNo in range(0, childCount):
childIndex = index.child(childNo, 0)
if expand: #if expanding, do that first (wonky animation otherwise)
self.setExpanded(childIndex, expand)
subChildCount = childIndex.internalPointer().get_child_count()
if subChildCount > 0:
self.recursive_expand(childIndex, subChildCount, expand)
if not expand: #if collapsing, do it last (wonky animation otherwise)
self.setExpanded(childIndex, expand)
model.rowCount(index) is the method you want.
model = index.model() # or some other way of getting it
for i in xrange(model.rowCount(index)):
child = model.index(i,0, index)
# do something with child
model.index(row,col, parent) is essentially the same as calling index.child(row,col); just with fewer indirections.
I would recommend using a QTreeWidget which inherits QTreeView. You can then grab the children as a QTreeWidgetItem.
Since you do not want to use the QTreeWidget but want to stick to your current model .. you can iterate through the 'possible' children using, .isValid(). You should not use the internalPointer() though. Instead use the cellItem you have, as it is the original ModalIndex .. then attempt to find it's siblings. Something like
x = 0; y =0
while cellIndex.sibling(x, y).isValid():
child = cellIndex.sibling(x, y)
x += 1
I make a evnetFilter Class for that.
My particular use case is shift click the drop indicator then expand all or collapse all the child nodes like software maya outliner.
class MTreeExpandHook(QtCore.QObject):
"""
MTreeExpandHook( QTreeView )
"""
def __init__(self, tree):
super(MTreeExpandHook, self).__init__()
tree.viewport().installEventFilter(self)
self.tree = tree
def eventFilter(self, receiver, event):
if (
event.type() == QtCore.QEvent.Type.MouseButtonPress
and event.modifiers() & QtCore.Qt.ShiftModifier
):
pos = self.tree.mapFromGlobal(QtGui.QCursor.pos())
index = self.tree.indexAt(pos)
if not self.tree.isExpanded(index):
self.tree.expandRecursively(index)
return True
return super(MTreeExpandHook, self).eventFilter(self.tree, event)
Usage Example below
import sys
from PySide2 import QtCore,QtGui,QtWidgets
class MTreeExpandHook(QtCore.QObject):
"""
MTreeExpandHook( QTreeView )
"""
def __init__(self, tree):
super(MTreeExpandHook, self).__init__()
self.setParent(tree)
# NOTE viewport for click event listen
tree.viewport().installEventFilter(self)
self.tree = tree
def eventFilter(self, receiver, event):
if (
# NOTE mouse left click
event.type() == QtCore.QEvent.Type.MouseButtonPress
# NOTE keyboard shift press
and event.modifiers() & QtCore.Qt.ShiftModifier
):
# NOTE get mouse local position
pos = self.tree.mapFromGlobal(QtGui.QCursor.pos())
index = self.tree.indexAt(pos)
if not self.tree.isExpanded(index):
# NOTE expand all child
self.tree.expandRecursively(index)
return True
return super(MTreeExpandHook, self).eventFilter(self.tree, event)
if __name__ == '__main__':
app = QtWidgets.QApplication(sys.argv)
model = QtGui.QStandardItemModel()
# NOTE create nested data
for i in range(3):
parent = QtGui.QStandardItem('Family {}'.format(i))
for j in range(3):
child = QtGui.QStandardItem('Child {}'.format(i*3+j))
for k in range(3):
sub_child = QtGui.QStandardItem("Sub Child")
child.appendRow([sub_child])
for x in range(2):
sub_child_2 = QtGui.QStandardItem("Sub Child 2")
sub_child.appendRow([sub_child_2])
parent.appendRow([child])
model.appendRow(parent)
treeView = QtWidgets.QTreeView()
treeView.setHeaderHidden(True)
MTreeExpandHook(treeView)
treeView.setModel(model)
treeView.show()
sys.exit(app.exec_())
example gif

Resources