sortingpyside6qtreeview

QTreeView Pyside6 not sorting 2 last columns of QstandardItem


I build a tree as illustrated on the image.

Treeview image

I want the tree to be able to sort each column this workds for the Well Name column and Depth type column (I may have different depth type called "depth","MD","Time",...) but it does not work for any of the other columns. THere is no difference (as far as I know) in the type of item I sued for each columns: a simple Qstandard item built from a string.

The tree is fill from nested dictionnaries and lists in the following form: {well:{depthtype:{logtype:[log1,log2,log3]}}}

with the following code

def fillWellTree(self, parent, dico, depth=0):
        if isinstance(dico, dict):
            for key, value in dico.items():
                item1=QStandardItem(str(key))
                item1.setEditable(False)
                itemList=[item1]
                if depth==0:
                    itemList[0].setIcon(QIcon("./icon/IconWell.png"))
                if isinstance(value, dict):
                    for i in range(depth):
                        emptyItem=QStandardItem("")
                        emptyItem.setEditable(False)
                        itemList.insert(0,emptyItem)
                    parent.appendRow(itemList)
                    self.fillWellTree(itemList[0], value, depth+1)
                elif isinstance(value, list):
                    for i in range(depth):
                        emptyItem=QStandardItem("")
                        emptyItem.setEditable(False)
                        itemList.insert(0,emptyItem)
                    parent.appendRow(itemList)
                    for val in value:
                        item_i=QStandardItem(str(val))
                        item_i.setEditable(False)
                        itemLogList=[item_i]
                        for i in range(depth+1):
                            emptyItem=QStandardItem("")
                            emptyItem.setEditable(False)
                            itemLogList.insert(0,emptyItem)
                        itemList[0].appendRow(itemLogList)

I wondered if the sort did not work because the parent item of the rows with data in the 2 last filled columns (logtype and log name) have empty strings so I replaced the empty strings with a random string but it did not change anything Thank you for your help and suggestions

EDIT here is a working script:

import sys

from PySide6.QtWidgets import QApplication, QTreeView
from PySide6.QtGui import QStandardItem, QStandardItemModel

app = QApplication(sys.argv)

TreeViewLogSelection=QTreeView()
WellModel = QStandardItemModel()
WellModel.setColumnCount(6)
TreeViewLogSelection.setModel(WellModel)
TreeViewLogSelection.model().setHorizontalHeaderLabels(['Well Name','Depth type','Log Type','Log Name','Role','Log unique name'])  
 
def fillWellTree(parent, dico, depth=0):
    if isinstance(dico, dict):
        for key, value in dico.items():
            item1=QStandardItem(str(key))
            item1.setEditable(False)
            itemList=[item1]
            if isinstance(value, dict):
                for i in range(depth):
                    emptyItem=QStandardItem("")
                    emptyItem.setEditable(False)
                    itemList.insert(0,emptyItem)
                parent.appendRow(itemList)
                fillWellTree(itemList[0], value, depth+1)
            elif isinstance(value, list):
                for i in range(depth):
                    emptyItem=QStandardItem("")
                    emptyItem.setEditable(False)
                    itemList.insert(0,emptyItem)
                parent.appendRow(itemList)
                for val in value:
                    item_i=QStandardItem(str(val))
                    item_i.setEditable(False)
                    itemLogList=[item_i]
                    for i in range(depth+1):
                        emptyItem=QStandardItem("")
                        emptyItem.setEditable(False)
                        itemLogList.insert(0,emptyItem)
                    itemList[0].appendRow(itemLogList)   
    TreeViewLogSelection.setSortingEnabled(True)             
dico={'Well-6': {'Depth': {'Sonic': ['DT', 'DTS', 'DTST', 'VPVS', 'DT_REG', 'Smoothing (DT_REG)', 'DT_FINAL'], 'Shallow resistivity': ['LLS', 'MSFL'], 'Bulk density': ['RHOB_RAW', 'RHOB_Predict_RFA', 'RHOB_REG', 'RHOB_DESPIKED', 'RHOB_DESPIKE_REG', 'Smoothing (RHOB_DESPIKE_REG)', 'RHOB_FINAL', 'RHOB_Predict_RF'], 'Shear slowness': ['DTS_Predict_RF', 'DTS_Predict_NN'], 'Deviation': ['DX', 'DY']}, 'MD': {'Sonic': ['DT_Predict_RF','DTS_predict']}}, 'DRILL-1': {'Depth': {'Bit size': ['BS'], 'Caliper': ['CALI'], 'Gamma ray': ['GR'], 'Neutron': ['NPHI'], 'Photoelectric factor': ['PE'], 'Spontaneous potential': ['SP'], 'Shallow resistivity': ['LLS'], 'Deep resistivity': ['LLD'], 'Sonic': ['DT', 'DTS','DTC'], 'Bulk density': ['RHOB'], 'Anonymous': ['WELLTOPS'], 'Badhole indicator': ['Bad_Hole']}}, 'WELLI-1': {'Depth': {'Sonic': ['DT', 'DTS','DTC'], 'Gamma ray': ['GR', 'GR2'], 'Shallow resistivity': ['LLS', 'MSFL'], 'Bulk density': ['RHOB_RAW', 'RHOB_Predict_RF']}, 'MD': {'Sonic': ['DT_Predict_RF', 'DT_Merged', 'DT_FINAL'], 'Impedance': ['AI','IP']}}}
fillWellTree(WellModel.invisibleRootItem(),dico)
TreeViewLogSelection.show()
sys.exit(app.exec())

Solution

  • Qt item models use the sort() function when items need to be sorted. That function is called by the view when setSortingEnabled() is set and sortByColumn() called (possibly by clicking on a header section).

    Fully implemented models like QStandardItemModel implement that sort() function each one in its own way.

    Before continuing with the explanation, it's important to understand how the parent/child relation of Qt item models works.

    Technically speaking, every item can be a parent with its own children, and those children are mapped as a table: therefore, every parent has, possibly, its own row and column count that doesn't match that of the model (the root); the different row count is obvious ("how many children"), the different column count is less intuitive, but also important. Note that this doesn't happen with QTreeWidget, because it assumes that the column count is persistent for all children.

    A view normally queries the model's rowCount() and columnCount() based on its rootIndex(), which by default is an invalid index, mapping to the root of the model.

    QTreeView also does that for children of the first column: virtually speaking, each parent item has its children shown as a "nested table", with the children in the first column capable of having further children on their own.

    Now, the issue and peculiarity of QStandardItemModel is that only items explicitly created actually exist. Specifically, a child row can have less items than declared in setColumnCount().

    This is exactly the cause of your problem: you are not creating items for all columns when you call appendRow(). This is also clearly visible by the fact that hovering or clicking on the right of the last "valid" (existing) child item will not do anything: there simply is no item there. Note that there is only one exception, which is for top level items, and the reason is that they are children of the root index, which then bases columnCount() on the value used with setColumnCount().

    The sort() implementation of QStandardItemModel only recursively sorts columns that do have valid items in their parents: for instance, if you have a model with 4 columns, and a parent (not a top level) only has three valid items, the fourth column of its children will not be sorted.

    There are two possible ways to work around this.

    The first is to properly create all items for all columns, even if they're empty, which can be easily achieved with a basic while loop before calling appendRow():

        ...
        while len(itemList) < model.columnCount():
            itemList.append(QStandardItem())
        parent.appendRow(itemList)
        ...
    

    The above must be done right before all calls to appendRow().

    Another and more appropriate way, which is also the preferred approach, is to use a QSortFilterProxyModel.

    WellModel = QStandardItemModel()
    WellModel.setColumnCount(6)
    WellModel.setHorizontalHeaderLabels(['Well Name','Depth type','Log Type','Log Name','Role','Log unique name'])  
    
    proxy = QSortFilterProxyModel()
    proxy.setSourceModel(WellModel)
    TreeViewLogSelection.setModel(proxy)
    

    QSortFilterProxyModel does not care about the odd implementation of QStandardItemModel and its "non existing items", since it implements sort() on its own, and sorting will work even if the source model has inconsistent column counts at any parent level.

    Another important benefit of using a proxy model is that it leaves the original model (including its original sorting) unchanged.

    Notes: 1. only classes and constants should have capitalized names, and you should really not use such a syntax for variables; spaces should also be used around operators, to improve readability; see the official Style Guide for Python Code, but these are common conventions for most programming languages; 2. setSortingEnabled() should not be called in a possibly recursive function.