PyQt signal to track when row in QTableView was moved - qt

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)

Related

How to reimplement a QDialog's accept and reject slots?

I'm working on a GUI project where the user is faced with the following QDialog:
class StockSelectorDialog(QDialog, stockselector_ui):
def __init__(self, parent_, *args, **kwargs):
super(StockSelectorDialog, self).__init__(*args, **kwargs)
self.setAttribute(Qt.WA_DeleteOnClose, on=True)
self.setupUi(self)
self.dialogButtonBox.accepted.connect(self.accept)
self.dialogButtonBox.rejected.connect(self.reject)
self.parent_ = parent_
self.symbolsbuffer = parent_.symbols.copy()
self.symbolmodel = SymbolListModel(self)
self.listView.setModel(self.symbolmodel)
self.symbolAddButton.clicked.connect(self.onAddButtonClicked)
self.symbolDeleteButton.clicked.connect(self.onDeleteButtonClicked)
def onAddButtonClicked(self, s):
symbol = self.symbolEdit.text()
if symbol:
self.symbolsbuffer.append(symbol)
self.symbolmodel.layoutChanged.emit()
self.symbolEdit.setText("")
def onDeleteButtonClicked(self, s):
indexes = self.listView.selectedIndexes()
if indexes:
for i in indexes:
del self.symbolsbuffer[i.row()]
self.symbolmodel.layoutChanged.emit()
self.listView.clearSelection()
def accept(self):
self.parent_.symbols = self.symbolsbuffer
self.parent_.onSymbolsChanged()
self.close()
def reject(self):
self.close()
The UI file is here: https://github.com/danib-prog/stockmarket-helper/blob/master/stockselector.ui
Everything worked fine until I added the buffer sysytem, for which I had to reimplement the accept and the reject slots of the dialog (although I'm not very sure about the latter). Now the dialog opens when necessary and the ListView works fine with all the buttons and the LineEdit, but my dialogButtonBox is not responding.
Why is this happening? And what is a solution to the problem?
You should not call close() on a dialog for that, mostly because it causes itself a call to reject(); luckily Qt is smart enough to prevent recursion, but the point remains: both those methods are expected to set the dialog's result and use done(), of close(), so that their event loop correctly exits from its exec_().
If you need to override a class function to do something other than the default behavior, you should always remember to call the base implementation too in order to correctly achieve the expected result.
def accept(self):
self.parent_.symbols = self.symbolsbuffer
self.parent_.onSymbolsChanged()
super(StockSelectorDialog, self).accept()
def reject(self):
super(StockSelectorDialog, self).reject()
Also, note that Qt Designer already connects the buttons of a QDialogButtonBox when it's created using the default dialog templates with buttons, so you should not connect them again, otherwise both accept and reject would be called twice.

How to solve a tkinter memory leak?

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.")

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

QTextEdit shift-tab wrong behaviour

shift+tab behaves as tab in QTextEdit/QPlainTextEdit.
Looks like a common problem with no good solution.
Is there any "classical" way to enable this functionality when tab increases indentation level and shift-tab decreases it?
This is a bit of an old question, but I got this figured out.
You just need to reimplement QPlainTextEdit (or QTextEdit) with your own class that inherits from it, and override the keyPressEvent.
By default a tab inserts a tabstop, but the below code catches a Qt.Key_Backtab event, which as near as I can tell is the event that occurs when you press Shift+Tab.
I tried and failed to catch Qt.Key_Tab and a Qt.Key_Shift or Qt.Key_Tab and a Shift modifier, so this must be the way to do it.
import sys
from PyQt4 import QtCore, QtGui
class TabPlainTextEdit(QtGui.QTextEdit):
def __init__(self,parent):
QtGui.QTextEdit.__init__(self, parent)
def keyPressEvent(self, event):
if event.key() == QtCore.Qt.Key_Backtab:
cur = self.textCursor()
# Copy the current selection
pos = cur.position() # Where a selection ends
anchor = cur.anchor() # Where a selection starts (can be the same as above)
# Can put QtGui.QTextCursor.MoveAnchor as the 2nd arg, but this is the default
cur.setPosition(pos)
# Move the position back one, selection the character prior to the original position
cur.setPosition(pos-1,QtGui.QTextCursor.KeepAnchor)
if str(cur.selectedText()) == "\t":
# The prior character is a tab, so delete the selection
cur.removeSelectedText()
# Reposition the cursor with the one character offset
cur.setPosition(anchor-1)
cur.setPosition(pos-1,QtGui.QTextCursor.KeepAnchor)
else:
# Try all of the above, looking before the anchor (This helps if the achor is before a tab)
cur.setPosition(anchor)
cur.setPosition(anchor-1,QtGui.QTextCursor.KeepAnchor)
if str(cur.selectedText()) == "\t":
cur.removeSelectedText()
cur.setPosition(anchor-1)
cur.setPosition(pos-1,QtGui.QTextCursor.KeepAnchor)
else:
# Its not a tab, so reset the selection to what it was
cur.setPosition(anchor)
cur.setPosition(pos,QtGui.QTextCursor.KeepAnchor)
else:
return QtGui.QTextEdit.keyPressEvent(self, event)
def main():
app = QtGui.QApplication(sys.argv)
w = TabPlainTextEdit(None)
w.show()
sys.exit(app.exec_())
if __name__ == "__main__":
main()
I'm still refining this, but the rest of the code is on GitHub.

(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