Skip to content
  • Categories
  • Recent
  • Tags
  • Popular
  • Users
  • Groups
  • Search
  • Get Qt Extensions
  • Unsolved
Collapse
Brand Logo
  1. Home
  2. Qt Development
  3. Qt for Python
  4. QStandardItemModel custom sorting rules

QStandardItemModel custom sorting rules

Scheduled Pinned Locked Moved Solved Qt for Python
pyside2python
6 Posts 2 Posters 964 Views
  • Oldest to Newest
  • Newest to Oldest
  • Most Votes
Reply
  • Reply as topic
Log in to reply
This topic has been deleted. Only users with topic management privileges can see it.
  • Q Offline
    Q Offline
    QueTeeHelper
    wrote on last edited by QueTeeHelper
    #1

    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_())
    
    JonBJ 1 Reply Last reply
    0
    • Q QueTeeHelper

      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_())
      
      JonBJ Offline
      JonBJ Offline
      JonB
      wrote on last edited by
      #2

      @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? :)

      1 Reply Last reply
      1
      • Q Offline
        Q Offline
        QueTeeHelper
        wrote on last edited by QueTeeHelper
        #3

        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!

        JonBJ 1 Reply Last reply
        0
        • Q QueTeeHelper

          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!

          JonBJ Offline
          JonBJ Offline
          JonB
          wrote on last edited by
          #4

          @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.

          Q 1 Reply Last reply
          1
          • JonBJ JonB

            @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.

            Q Offline
            Q Offline
            QueTeeHelper
            wrote on last edited by
            #5

            @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!

            JonBJ 1 Reply Last reply
            1
            • Q QueTeeHelper

              @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!

              JonBJ Offline
              JonBJ Offline
              JonB
              wrote on last edited by
              #6

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

              1 Reply Last reply
              0

              • Login

              • Login or register to search.
              • First post
                Last post
              0
              • Categories
              • Recent
              • Tags
              • Popular
              • Users
              • Groups
              • Search
              • Get Qt Extensions
              • Unsolved