Important: Please read the Qt Code of Conduct - https://forum.qt.io/topic/113070/qt-code-of-conduct

QStandardItemModel custom sorting rules



  • I'm attempting to implement custom sorting based on the following rules:

    1. Default: Ascending order when no search criteria is provided or no matches are found.
    2. Custom: Prioritized sorting by search criteria matches first (ascending), followed by non-matches (also ascending)

    If my QLineEdit input is "ca", I expect all the items matching "ca.*" to be displayed first and sorted ascending, followed by all the non matches, sorted ascending.
    The problem I'm encountering is that I can't get the matches to show up in an ascending order and it's not clear to me where I'm messing up.

    It looks like it's almost there but I can't figure out what I'm missing here, so any help to get this working correctly would be appreciated!


    Expected Behaviour

    To simplify, this is the result I expect...

    1. Default sorting: If QLineEdit has no input or input doesn't match any items, sort everything in ascending order. (green)
      a.PNG

    2. Prioritized sorting: If QLineEdit has input that match items, sorting will done in 2 phases.
      First, the item.data that matches (regex: 'c.*') will be moved to the front and sorted ascending. (green)
      Second, matches will be followed by the non-matches, also sorted ascending. (orange)
      c.png


    Result and Test

    Unfortunately, the closest I've been able to get is the following.
    The item.data that matches the search criteria is prioritized; however, I can't figure out how to make the order ascending. (red)
    I expect [cycle, canary, car, camera, cat] to be displayed as [camera, canary, car, cat, cycle], like in the image above.
    b.PNG

    I've provided a rough, reproducible example below:

    import sys
    from PySide2 import QtCore
    from PySide2 import QtGui
    from PySide2 import QtWidgets
    
    class MainDialog(QtWidgets.QDialog):
        def __init__(self, parent=None):
            super(MainDialog, self).__init__(parent)
            self.setWindowTitle('Sorting Test')
            self.setFixedSize(QtCore.QSize(500, 200))
            self.show()
    
            # Test gui.
            main_layout = QtWidgets.QVBoxLayout(self)
            self.setLayout(main_layout)
    
            self.search_edit = QtWidgets.QLineEdit(self)
            self.item_viewer = ItemViewer(self)
    
            main_layout.addWidget(self.search_edit)
            main_layout.addWidget(self.item_viewer)
    
            # Signals.
            self.search_edit.textChanged.connect(self._on_search_update)
    
        def _on_search_update(self, text):
           self.item_viewer.sort_by_search_criteria(text)
    
    class ItemViewer(QtWidgets.QListView):
        def __init__(self, parent=None):
            super(ItemViewer, self).__init__(parent)
            self.setSpacing(2)
            self.setFlow(self.LeftToRight)
            self.setWrapping(True)
            self.setViewMode(self.ListMode)
            self.setWordWrap(True)
            self.setFrameShape(self.NoFrame)
    
            self.__setup_test_model()
    
        def __setup_test_model(self):
            self.source_model = QtGui.QStandardItemModel()
    
            self.proxy_model = TestProxyModel(self)
            self.proxy_model.setSourceModel(self.source_model)
            self.proxy_model.setFilterCaseSensitivity(QtCore.Qt.CaseInsensitive)
            self.proxy_model.setSortRole(QtCore.Qt.DisplayRole)
    
            self.setModel(self.proxy_model)
    
    
            items = ['000', 'cat', 'bat', 'door', 'camera', 'floor', '001', 'car',
                     'train', 'file', 'canary', 'zebra', 'dog', 'cycle', 'farm']
    
            for i in items:
                item = QtGui.QStandardItem()
                item.setData(i, QtCore.Qt.DisplayRole)
    
                self.source_model.appendRow(item)
    
            self.proxy_model.sort(0)
    
        def sort_by_search_criteria(self, text):
            self.proxy_model.sort_by_match(text)
    
    
    class TestProxyModel(QtCore.QSortFilterProxyModel):
        def __init__(self, parent=None):
            super(TestProxyModel, self).__init__(parent)
            self.__sort_regex = QtCore.QRegExp()
    
        def sort_by_match(self, search_text):
            r_str = '{r}.*'.format(r=search_text) if search_text else ''
    
            regex = QtCore.QRegExp(r_str,
                                   QtCore.Qt.CaseInsensitive,
                                   QtCore.QRegExp.RegExp)
            self.__sort_regex = regex
    
            self.invalidate()
            self.sort(0)
    
        def lessThan(self, left, right):
            l_item = self.sourceModel().itemFromIndex(left)
            r_item = self.sourceModel().itemFromIndex(right)
            l_data = l_item.data(QtCore.Qt.DisplayRole)
            r_data = r_item.data(QtCore.Qt.DisplayRole)
    
            if l_data < r_data:
                if self.__sort_regex.exactMatch(l_data) and \
                        self.__sort_regex.exactMatch(r_data):
                    return True
    
                elif self.__sort_regex.exactMatch(r_data):
                    return False
    
            # Works but not ascending.
            if l_data > r_data:
                if self.__sort_regex.exactMatch(l_data):
                    return True
    
            return l_data < r_data
    
    
    if __name__ == '__main__':
        app = QtWidgets.QApplication(sys.argv)
        d = MainDialog()
        sys.exit(app.exec_())
    


  • @QueTeeHelper
    I don't get your proposed algorithm.

    Purely in my head, not tested, I see it very differently, like:

        def lessThan(self, left, right):
            l_item = self.sourceModel().itemFromIndex(left)
            r_item = self.sourceModel().itemFromIndex(right)
            l_data = l_item.data(QtCore.Qt.DisplayRole)
            r_data = r_item.data(QtCore.Qt.DisplayRole)
    
            is_l_match = self.__sort_regex.exactMatch(l_data)
            is_r_match = self.__sort_regex.exactMatch(r_data)
    
            if is_l_match and not is_r_match:
                return True
            if not is_l_match and is_r_match:
                return False
    
            return l_data < r_data
    

    Am I right? Or quite wrong/misunderstanding? :)



  • @QueTeeHelper
    I don't get your proposed algorithm.

    Purely in my head, not tested, I see it very differently, like:

        def lessThan(self, left, right):
            l_item = self.sourceModel().itemFromIndex(left)
            r_item = self.sourceModel().itemFromIndex(right)
            l_data = l_item.data(QtCore.Qt.DisplayRole)
            r_data = r_item.data(QtCore.Qt.DisplayRole)
    
            is_l_match = self.__sort_regex.exactMatch(l_data)
            is_r_match = self.__sort_regex.exactMatch(r_data)
    
            if is_l_match and not is_r_match:
                return True
            if not is_l_match and is_r_match:
                return False
    
            return l_data < r_data
    

    Am I right? Or quite wrong/misunderstanding? :)



  • Hey @JonB,

    On the contrary, I was the one misunderstanding the purpose of lessThan (first time subclassing QSortFilterProxyModel)! :)
    I tested out your logic and it appears to give me the correct results!

    I still don't fully understand why your change works, or what my train of thought was that lead to the logic I used in my example.
    However, after doing some testing with your example, I can already see where I went wrong and how I was overthinking the logic. I think I need to debug a bit more, to better understand how the underlying code uses lessThan.

    Thanks a lot for your solution, it's much appreciated!



  • @QueTeeHelper
    All you know and care about is that your lessThan will be called repeatedly to produce the desired sort order. It will be called potentially with any of your items in either "left" or "right" item to compare.



  • @JonB said in QStandardItemModel custom sorting rules:

    @QueTeeHelper
    All you know and care about is that your lessThan will be called repeatedly to produce the desired sort order. It will be called potentially with any of your items in either "left" or "right" item to compare.

    I think I understand now (see comments below).

        def lessThan(self, left, right):
            l_item = self.sourceModel().itemFromIndex(left)
            r_item = self.sourceModel().itemFromIndex(right)
            l_data = l_item.data(QtCore.Qt.DisplayRole)
            r_data = r_item.data(QtCore.Qt.DisplayRole)
    
            is_l_match = self.__sort_regex.exactMatch(l_data)
            is_r_match = self.__sort_regex.exactMatch(r_data)
    
            # Left items matching search pattern will always be less than non-matching right items.
            # This will ensure matching items get priority in list.
            if is_l_match and not is_r_match:
                return True
    
            # Non-matching left items will always be greater than matching right items.
            # This will ensure non-matching items never get re-order priority over matching.
            if not is_l_match and is_r_match:
                return False
    
            # Now that priorities have been established, all other comparisons are
            # sorted in ascending order.
            return l_data < r_data
    

    Thanks again for your help!



  • @QueTeeHelper
    Exactly right, those are the comments I would have typed if it were my code :)


Log in to reply