How to sort items in QTreeview by the following concepts ?
self.model_01 = self.model()
for i in range(self.model_01.rowCount()):
if self.itemData(i) is None:
self.setItemData(i, i)
...
def ascending_order(self):
self.model_01.setSortRole(Qt.DisplayRole)
self.model_01.sort(self.modelColumn(), Qt.AscendingOrder)
def descending_order(self):
self.model_01.setSortRole(Qt.DisplayRole)
self.model_01.sort(self.modelColumn(), Qt.DescendingOrder)
def given_order(self):
print("given order")
self.model_01.setSortRole(Qt.UserRole)
self.model_01.sort(self.modelColumn(), Qt.AscendingOrder)
def reverse_order(self):
print("reverse order")
self.model_01.setSortRole(Qt.UserRole)
self.model_01.sort(self.modelColumn(), Qt.DescendingOrder)
using this code , I can able to sort item in ascending order as well as in descending order in Qt.DisplayRole.
But in Qt.UserRole, I cant able to sort items.
How to sort items in ascending(Original order) or in reverse of original order ?
Update - Minimal reproducible example
from PyQt5.QtWidgets import *
from PyQt5.QtGui import *
from PyQt5.QtCore import *
data = {
"select all": {
'Group 1': ['item11', 'item12'],
'Group 3': ['item32', 'item31'],
'Group 2': ['item21', 'item22'],
'Group 4': ['item41', 'item42'],
}
}
class MyModel(QStandardItemModel):
def __init__(self):
super().__init__()
self.root_text, self.parent_text, self.child_text = [], [], []
for root_key, root_value in data.items():
if root_key not in self.root_text:
self.root_text.append(root_key)
root_item = QStandardItem()
root_item.setData(root_key, role=Qt.DisplayRole)
root_item.setCheckable(True)
self.appendRow(root_item)
for parent_key, parent_value in root_value.items():
if parent_key not in self.parent_text:
self.parent_text.append(parent_key)
parent_item = QStandardItem()
parent_item.setData(parent_key, role=Qt.DisplayRole)
parent_item.setCheckable(True)
root_item.appendRow(parent_item)
for child_value in parent_value:
if child_value not in self.child_text:
self.child_text.append(child_value)
child_item = []
child_item = QStandardItem()
child_item.setData(child_value, role=Qt.DisplayRole)
child_item.setCheckable(True)
parent_item.appendRow(child_item)
self.itemChanged.connect(self.update_children)
def update_children(self, item, fromUser=True):
print(item,"item")
if fromUser:
# temporarily disconnect to avoid recursion
self.itemChanged.disconnect(self.update_children)
for i in range(item.rowCount()):
child = item.child(i)
child.setCheckState(item.checkState())
# explicitly call update_children
self.update_children(child, False)
if fromUser:
root = self.invisibleRootItem()
parent = item.parent() or root
while True:
count = parent.rowCount()
checked = 0
for i in range(count):
state = parent.child(i).checkState()
if state == Qt.Checked:
checked += 1
elif state == Qt.PartiallyChecked:
parent.setCheckState(Qt.PartiallyChecked)
break
else:
if not checked:
parent.setCheckState(Qt.Unchecked)
elif checked == count:
parent.setCheckState(Qt.Checked)
else:
parent.setCheckState(Qt.PartiallyChecked)
if parent == root:
break
parent = parent.parent() or root
self.itemChanged.connect(self.update_children)
class MyCombo(QComboBox):
clickedData = None
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.treeView = QTreeView()
self.treeView.setHeaderHidden(True)
self.setView(self.treeView)
self.treeView.viewport().installEventFilter(self)
# Qmenu intilize
self.menu = QMenu()
self.setContextMenuPolicy(Qt.CustomContextMenu)
self.customContextMenuRequested.connect(self.cntxt_menu)
self.RightClickMenu()
self.delegate = QStyledItemDelegate(self.treeView)
def eventFilter(self, obj, event):
if (
event.type() == event.MouseButtonPress
and event.button() == Qt.LeftButton
):
index = self.treeView.indexAt(event.pos())
if index.isValid():
opt = self.treeView.viewOptions()
opt.rect = self.treeView.visualRect(index)
self.delegate.initStyleOption(opt, index)
checkRect = self.style().subElementRect(
QStyle.SE_ItemViewItemCheckIndicator, opt, self.treeView)
if checkRect.contains(event.pos()):
self.clickedData = index, checkRect
elif event.type() == event.MouseButtonRelease:
if event.button() == Qt.LeftButton and self.clickedData:
index = self.treeView.indexAt(event.pos())
pressIndex, checkRect = self.clickedData
if index == pressIndex and event.pos() in checkRect:
state = index.data(Qt.CheckStateRole)
if state == Qt.Checked:
state = Qt.Unchecked
else:
state = Qt.Checked
self.model().setData(index, state, Qt.CheckStateRole)
self.clickedData = None
return True
elif (
event.type() == event.MouseButtonPress
and event.button() == Qt.LeftButton
):
index = self.treeView.indexAt(event.pos())
state = index.data(Qt.CheckStateRole)
if state == Qt.Checked:
state = Qt.Unchecked
else:
state = Qt.Checked
self.model().setData(index, state, Qt.CheckStateRole)
self.treeView.viewport().update()
self.clickedData = None
return True
return super().eventFilter(obj, event)
def showPopup(self):
self.treeView.expandAll()
width = self.treeView.sizeHintForColumn(0)
maxCount = self.maxVisibleItems()
index = self.model().index(0, 0, self.rootModelIndex())
visible = 0
while index.isValid():
visible += 1
index = self.treeView.indexBelow(index)
if visible > maxCount:
# the visible count is higher than the maximum, so the vertical
# scroll bar will be shown and we have to consider its width.
# Note that this does NOT consider styles that use "transient"
# scroll bars, which are shown *within* the content of the view,
# as it happens on macOs; see QStyle.styleHint() and
# QStyle::SH_ScrollBar_Transient
width += self.treeView.verticalScrollBar().sizeHint().width()
break
self.treeView.setMinimumWidth(width)
super().showPopup()
def RightClickMenu(self):
self.menu.clear()
self.ascending_action = QAction('Ascending',self)
self.menu.addAction(self.ascending_action)
self.ascending_action.triggered.connect(self.ascending_order)
self.descending_action = QAction('Descending')
self.descending_action.triggered.connect(self.descending_order)
self.menu.addAction(self.descending_action)
self.original_action = QAction('Original Order')
self.original_action.triggered.connect(self.original_order)
self.menu.addAction(self.original_action)
self.reverse_action = QAction('Reverse order')
self.reverse_action.triggered.connect(self.reverse_order)
self.menu.addAction(self.reverse_action)
def cntxt_menu(self,pos):
self.model_01 = self.model()
self.menu.exec_(self.mapToGlobal(pos))
def ascending_order(self):
self.model_01.setSortRole(Qt.DisplayRole)
self.model_01.sort(self.modelColumn(),Qt.AscendingOrder)
def descending_order(self):
self.model_01.setSortRole(Qt.DisplayRole)
self.model_01.sort(self.modelColumn(), Qt.DescendingOrder)
def original_order(self):
print("given order")
self.model_01.setSortRole(Qt.UserRole)
# self.model_01.sort(0, Qt.AscendingOrder)
self.model_01.sort(0,Qt.AscendingOrder)
def reverse_order(self):
print("reverse order")
self.model_01.setSortRole(Qt.UserRole)
self.model_01.sort(self.modelColumn(), Qt.DescendingOrder)
class MainWindow(QWidget):
def __init__(self):
super().__init__()
self.setWindowTitle("QCombobox")
self.comboBox = MyCombo()
self.comboBox.setEditable(False)
self.model = MyModel()
self.comboBox.setModel(self.model)
self.vbox = QVBoxLayout()
self.setLayout(self.vbox)
self.vbox.addWidget(self.comboBox)
if __name__ == '__main__':
import sys
app = QApplication(sys.argv)
ex = MainWindow()
ex.show()
sys.exit(app.exec_())
Qt already provides such a mechanism with standard views: both QTableView and QTreeView provide a sortByColumn()
function. The assumption is, as long as the model supports it (as explained in the docs), using -1
for the column should revert to the original layout.
Unfortunately, while it generally works on QTableView, it seems that there is some inconsistency with QTreeView and QStandardItemModel (I did some basic testing, but I have not been able to find the cause yet; there is a couple of reports in the Qt bug report system, but they seem to be still unresolved).
Not all is lost, though: as long as the insertion order is consistent (see the notes below), we can use a custom user role whenever a new item is added to the model, including the very first item of any parent (including top level ones).
In order to do that, we need to connect the rowsInserted
signal before any item (row) is created, and properly set the user role starting from the first
row up to the last one of the row count of the parent (in order to correctly update the indexes of all following items in case an item is inserted).
class MyModel(QStandardItemModel):
def __init__(self):
super().__init__()
self.rowsInserted.connect(self.updateInsertionOrder)
# ...
def updateInsertionOrder(self, parent, first, last):
with QSignalBlocker(self):
for row in range(first, self.rowCount(parent)):
self.setData(self.index(row, 0, parent), row, Qt.UserRole)
Note that:
sort()
using a column higher than the first will obviously not work; eventually consider to implement a custom function (eg. sortByInsertionOrder
) that will always call sort()
with the first column index;sort()
is called with different arguments (the sort role is not UserRole
, the column is not 0 or the order is descending) and a new item is inserted supposing the current sorting, the "original" order will not be consistent: for instance, if the current order is descending and uses the display role and you insert an item at row 0 because it would theoretically follow the current sorting, the result is that the UserRole
of any (currently) following indexes will be rewritten, thus losing the original order; a more consistent approach would be to actually use a QSortFilterProxyModel for the view, and apply the sorting to that;rowsMoved
and rowsRemoved
) in case that becomes necessary;self.updateInsertionOrder(QModelIndex(), 0, self.rowCount() - 1)
;