QTreeView с QStandardItemModel: как реализовать собственную модель сортировки

У меня есть QTreeView, который я заполняю QStandardItemModel.

У меня включена сортировка, поэтому можно щелкнуть заголовок столбца для сортировки по этому столбцу. Но это не работает со специальными символами и немецкими умлаутами - я знаю об этом, поэтому хочу реализовать собственный алгоритм сортировки ICU.

Но как я могу реализовать это, щелкнув заголовок моего столбца в QTreeView? Я видел примеры для QSortFilterProxyModel, но почему-то не могу это реализовать и не понимаю, нужно ли мне поднимать его по сигналу или как работает механизм... Я много искал, но сдался...

Итак, цель: когда щелкают заголовок столбца, мой собственный алгоритм сортировки должен сортировать строки по значениям в конкретном столбце. Как этого добиться?

Пример полного рабочего кода:

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import sys
from PyQt5.QtGui import *
from PyQt5.QtCore import *
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *

class App(QWidget):
    COL_ID = 0
    MAIL_RANGE = 4
    ID, FROM, SUBJECT, DATE = range(COL_ID, MAIL_RANGE) # four elements: 0, 1, 2, 3

    def sort(self):
            print("sort() called.")

    def sortChanged(self):
        print("sortChanged() called.")
        if self.sortCaseSensitivityCheckBox.isChecked():
            caseSensitivity = Qt.CaseSensitive
        else:
            caseSensitivity = Qt.CaseInsensitive

        self.proxyModel.setSortCaseSensitivity(caseSensitivity)

    def __lt__(self, other):
        print("class App, __lt__() called.")
        column = self.dataView().sortColumn()
        k1 = self.text(column)
        k2 = other.text(column)
        return _human_key(k1) < _human_key(k2)

    def __init__(self):
        print("class App, __init__() called.")
        super().__init__()      
        self.left = 100
        self.top = 110
        self.width = 640
        self.height = 240
        self.initUI()

        self.dataView.setSelectionMode(QAbstractItemView.ExtendedSelection)  #  <- enable selection of rows in tree
        self.dataView.setEditTriggers(QAbstractItemView.NoEditTriggers)      #  <- disable editing items in tree
        self.dataView.setSortingEnabled(True)
        self.dataView.sortByColumn(2, Qt.AscendingOrder)        

        for i in range(0, 2):
            self.dataView.resizeColumnToContents(i)

        self.pbEdit = QPushButton(self)
        self.pbEdit.setText("Edit")
        self.pbEdit.move(400,0)
        self.pbEdit.show()

        self.pbDel = QPushButton(self)
        self.pbDel.setText("Delete")
        self.pbDel.move(500,0)
        self.pbDel.show()

        # connect handlers
        self.dataView.doubleClicked.connect(self.on_dataView_doubleClicked)
        self.pbEdit.clicked.connect(self.on_pbEdit_clicked)

    def open_dialog(self):
        rows = set(ix.row() for ix in self.dataView.selectedIndexes())        
        for row in rows:
            it = self.dataView.model().item(row, App.COL_ID)
            my_id = it.text()            
            self.create_dialog(my_id)

    def on_dataView_doubleClicked(self):
        print("class app, on_dataView_doubleClicked() called.")
        self.open_dialog()

    def on_pbEdit_clicked(self):
        print("class app, on_pbEdit_clicked() called.")
        self.open_dialog()

    def create_dialog(self, id):
        print("dialog called for " + str(id))
        myDlg = QDialog(self)
        lbl = QLabel(myDlg)
        lbl.setText("Hello id: " + str(id))
        # myDlg.exec_()  #  <- modal
        myDlg.show()     #  <- non-modal
        myDlg.resize(300,200)


    def initUI(self):
        print("class App, initUI() called.")
        self.setGeometry(self.left, self.top, self.width, self.height)

        self.dataGroupBox = QGroupBox("Inbox")
        self.dataView = QTreeView()
        self.dataView.setRootIsDecorated(False)
        self.dataView.setAlternatingRowColors(True)        

        dataLayout = QHBoxLayout()
        dataLayout.addWidget(self.dataView)
        self.dataGroupBox.setLayout(dataLayout)

        model = self.createMailModel(self.dataView)
        self.dataView.setModel(model)
        self.addMail(model, 1, '[email protected]', 'Zabel','03/25/2017 02:05 PM')
        self.addMail(model, 2, '[email protected]', 'Schneider','02/02/2017 03:05 PM')
        self.addMail(model, 3, '[email protected]', 'anabel','01/01/2017 04:05 PM')
        self.addMail(model, 4, '[email protected]', 'Arachno','03/25/2017 02:05 PM')
        self.addMail(model, 5, '[email protected]', 'Öztürk','02/02/2017 03:05 PM')
        self.addMail(model, 6, '[email protected]', 'Becker','01/01/2017 04:05 PM')

        self.dataView.setColumnHidden(App.COL_ID, True)

        mainLayout = QVBoxLayout()
        mainLayout.addWidget(self.dataGroupBox)
        self.setLayout(mainLayout)

        self.show()


    def createMailModel(self,parent=None):
        model = QStandardItemModel(0, self.MAIL_RANGE, parent)

        model.setHeaderData(self.ID, Qt.Horizontal, "ID")
        model.setHeaderData(self.FROM, Qt.Horizontal, "From")
        model.setHeaderData(self.SUBJECT, Qt.Horizontal, "Subject")
        model.setHeaderData(self.DATE, Qt.Horizontal, "Date")
        return model

    def addMail(self, model, mailID, mailFrom, subject, date):
        model.insertRow(0)
        model.setData(model.index(0, self.ID), mailID)
        model.setData(model.index(0, self.FROM), mailFrom)
        model.setData(model.index(0, self.SUBJECT), subject)
        model.setData(model.index(0, self.DATE), date)        

if __name__ == '__main__':    
    app = QApplication(sys.argv)
    ex = App()
    sys.exit(app.exec_())

person ProfP30    schedule 19.03.2019    source источник


Ответы (1)


Вы должны создать класс, наследуемый от QSortFilterProxyModel, и перезаписать метод lessThan:

import sys
from PyQt5 import QtCore, QtGui, QtWidgets

# fake _human_key
# https://stackoverflow.com/a/5254534/6622587
import re
def _human_key(key):
    parts = re.split(r'(\d*\.\d+|\d+)', key)
    return tuple((e.swapcase() if i % 2 == 0 else float(e))
            for i, e in enumerate(parts))

class HumanProxyModel(QtCore.QSortFilterProxyModel):
    def lessThan(self, source_left, source_right):
        data_left = source_left.data()
        data_right = source_right.data()
        if type(data_left) == type(data_right) == str:
            return _human_key(data_left) < _human_key(data_right)
        return super(HumanProxyModel, self).lessThan(source_left, source_right)


class StyledItemDelegate(QtWidgets.QStyledItemDelegate):
    def displayText(self, value, locale):
        if isinstance(value, QtCore.QDateTime):
            return value.toString('MM/dd/yyyy hh:mm A')
        return super(StyledItemDelegate, self).displayText(value, locale)

class App(QtWidgets.QWidget):
    COL_ID = 0
    MAIL_RANGE = 4
    ID, FROM, SUBJECT, DATE = range(COL_ID, MAIL_RANGE) # four elements: 0, 1, 2, 3

    def __init__(self):
        print("class App, __init__() called.")
        super().__init__()      
        self.left = 100
        self.top = 110
        self.width = 640
        self.height = 240
        self.initUI()

        self.dataView.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection)
        self.dataView.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers)
        self.dataView.setSortingEnabled(True)
        self.dataView.sortByColumn(2, QtCore.Qt.AscendingOrder)
        delegate = StyledItemDelegate(self.dataView)
        self.dataView.setItemDelegate(delegate)        

        for i in range(0, 2):
            self.dataView.resizeColumnToContents(i)

        self.pbEdit = QtWidgets.QPushButton(self)
        self.pbEdit.setText("Edit")
        self.pbEdit.move(400,0)
        self.pbEdit.show()

        self.pbDel = QtWidgets.QPushButton(self)
        self.pbDel.setText("Delete")
        self.pbDel.move(500,0)
        self.pbDel.show()

        # connect handlers
        self.dataView.doubleClicked.connect(self.on_dataView_doubleClicked)
        self.pbEdit.clicked.connect(self.on_pbEdit_clicked)

    def open_dialog(self):
        rows = set(ix.row() for ix in self.dataView.selectedIndexes())  
        for row in rows:
            ix = self.dataView.model().index(row, App.COL_ID)
            my_id = ix.data()            
            self.create_dialog(my_id)

    def on_dataView_doubleClicked(self):
        print("class app, on_dataView_doubleClicked() called.")
        self.open_dialog()

    def on_pbEdit_clicked(self):
        print("class app, on_pbEdit_clicked() called.")
        self.open_dialog()

    def create_dialog(self, _id):
        print("dialog called for {}".format(_id))
        myDlg = QtWidgets.QDialog(self)
        lbl = QtWidgets.QLabel(myDlg)
        lbl.setText("Hello id: {}".format(_id))
        # myDlg.exec_()  #  <- modal
        myDlg.show()     #  <- non-modal
        myDlg.resize(300,200)


    def initUI(self):
        print("class App, initUI() called.")
        self.setGeometry(self.left, self.top, self.width, self.height)

        self.dataGroupBox = QtWidgets.QGroupBox("Inbox")
        self.dataView = QtWidgets.QTreeView()
        self.dataView.setRootIsDecorated(False)
        self.dataView.setAlternatingRowColors(True)        

        dataLayout = QtWidgets.QHBoxLayout()
        dataLayout.addWidget(self.dataView)
        self.dataGroupBox.setLayout(dataLayout)

        model = self.createMailModel(self.dataView)
        proxy = HumanProxyModel(self)
        proxy.setSourceModel(model)
        self.dataView.setModel(proxy)
        self.addMail(model, 1, '[email protected]', 'Zabel', QtCore.QDateTime.fromString('03/25/2017 02:05 PM', 'MM/dd/yyyy hh:mm A'))
        self.addMail(model, 2, '[email protected]', 'Schneider', QtCore.QDateTime.fromString('02/02/2017 03:05 PM', 'MM/dd/yyyy hh:mm A'))
        self.addMail(model, 3, '[email protected]', 'anabel', QtCore.QDateTime.fromString('01/01/2017 04:05 PM', 'MM/dd/yyyy hh:mm A'))
        self.addMail(model, 4, '[email protected]', 'Arachno', QtCore.QDateTime.fromString('03/25/2017 02:05 PM', 'MM/dd/yyyy hh:mm A'))
        self.addMail(model, 5, '[email protected]', 'Öztürk', QtCore.QDateTime.fromString('02/02/2017 03:05 PM', 'MM/dd/yyyy hh:mm A'))
        self.addMail(model, 6, '[email protected]', 'Becker', QtCore.QDateTime.fromString('01/01/2017 04:05 PM', 'MM/dd/yyyy hh:mm A'))

        self.dataView.setColumnHidden(App.COL_ID, True)

        mainLayout = QtWidgets.QVBoxLayout(self)
        mainLayout.addWidget(self.dataGroupBox)
        self.show()


    def createMailModel(self,parent=None):
        model = QtGui.QStandardItemModel(0, self.MAIL_RANGE, parent)

        model.setHeaderData(self.ID, QtCore.Qt.Horizontal, "ID")
        model.setHeaderData(self.FROM, QtCore.Qt.Horizontal, "From")
        model.setHeaderData(self.SUBJECT, QtCore.Qt.Horizontal, "Subject")
        model.setHeaderData(self.DATE, QtCore.Qt.Horizontal, "Date")
        return model

    def addMail(self, model, mailID, mailFrom, subject, date):
        model.insertRow(0)
        model.setData(model.index(0, self.ID), mailID)
        model.setData(model.index(0, self.FROM), mailFrom)
        model.setData(model.index(0, self.SUBJECT), subject)
        model.setData(model.index(0, self.DATE), date)        

if __name__ == '__main__':    
    app = QtWidgets.QApplication(sys.argv)
    ex = App()
    sys.exit(app.exec_())
person eyllanesc    schedule 19.03.2019
comment
Большое спасибо, такие работы! Только один вопрос по этой строке: if source_left.column() == source_right.column() == App.SUBJECT: Здесь жестко закодирован столбец App.SUBJECT. Значит, мой фильтр каждый раз ищет этот столбец? Но я хочу использовать фильтр для любого столбца, на который я нажимаю... - person ProfP30; 20.03.2019
comment
@ProfP30 ProfP30 Я улучшил пример, преобразовав строки, представляющие время, в QDateTime. С другой стороны, я сделал эту фильтрацию, потому что вы хотите применить тот же порядок для дат, для чисел или для другого типа информации, теперь я улучшил метод, применив его только к строкам. - person eyllanesc; 20.03.2019
comment
@ProfP30 [продолжение] Хотя я думаю, что вы заново изобретаете колесо, Qt вы предоставляете подмодуль QtSql, который также доступен в PyQt5, я рекомендую вам проверить примеры PyQt, чтобы загрузить исходный код из riverbankcomputing.com/static/Downloads/PyQt5/, разархивируйте и просмотрите папку с примерами. - person eyllanesc; 20.03.2019
comment
выдает ошибку типа во время выполнения: TypeError: expected string or bytes-like object return _compile(pattern, flags).split(string, maxsplit) - person ProfP30; 20.03.2019
comment
@ProfP30 ProfP30 В какую строку выбрасывает вас эта ошибка? Какая у вас версия питона? Какая у вас версия PyQt5? - person eyllanesc; 20.03.2019
comment
@ProfP30 Вы скопировали весь мой код? Вы просто используете мой код или добавили другой код? - person eyllanesc; 20.03.2019
comment
спасибо за ваш пример. Я полностью скопировал ваш обновленный код, без изменений. Теперь у меня проблемы: 1.) Столбец с датами пуст 2.) Когда я дважды щелкаю по строке, вместо загрузки диалога выдается исключение: AttributeError: 'HumanProxyModel' object has no attribute 'item' Backend terminated (returncode: 3) Fatal Python error: Aborted Python: 3.7.1. (ОС: Windows) PyQt: 5.11.3 - person ProfP30; 20.03.2019
comment
@ProfP30 Только у QStandardItemModel есть QStandardItem, который можно получить с помощью метода item, но модель, которую вы установили в поле зрения, представляет собой QFilterSortProxyModel, у которой нет этого метода, поэтому для получения текста вы должны использовать QModelIndex - person eyllanesc; 20.03.2019