PyQt5 QSortFilterProxyModel按适当的数据类型(int、float、datetime、string等)对列进行排序

2024-05-19 00:00:52 发布

您现在位置:Python中文网/ 问答频道 /正文

我将QAbstractTableModel实例包装在QSortFilterProxyModel中,以便能够对其进行排序和过滤。排序请求将列数据作为字符串进行排序—相反,我希望列可以按其相应的数据类型进行排序

网站上有一些问题/答案粗略地描述了这个问题,但没有一个提供了一个包含的解决方案来解决表的“适当数据类型排序”问题,其中需要能够按照适合其数据类型的方法对任何列进行排序

通过适当的排序方法,我的意思是我希望QSortFilterProxyModel将整数列排序为整数,将浮点列排序为浮点,将日期列排序为日期,并将字符串列排序为字符串(嗯,字符串类型,它已经处理了…)

似乎有一两种方法可以实现这一点。一个是设置源模型,使其具有自己的排序规则,例如,按整数对列进行排序。是否可以调整该实现以按“本机”数据类型对列进行排序,而不是为每个列预先指定排序规则

在我的特定用例中,源模型实际上是一个数据帧模型。这是值得注意的,因为可能很方便,我的每一列都有一个本机/内置/可靠的数据类型,可以通过编程方式知道。因此,如果它使实现变得更容易,我将放弃对具有许多不同数据类型的单个列进行排序的能力*,以获得通过其数据类型的正确方法对任何列进行排序的能力。(*我意识到在某些情况下,这种能力是有用的,并且理解默认行为可能不可知地解决了这一问题,但我并不立即需要它。)

我看到的另一个解决方案是单独讨论或与特殊角色一起讨论,包括重新实现QSortFilterProxyModel类和重载lessThan函数。这个解决方案对我来说是有意义的,我只是还没有找到一个例子,在这个例子中,一个干净的实现可以根据排序的数据类型为数据应用适当的排序方法。这些解决方案似乎都依赖于列的硬编码方法。似乎应该在调用lessThan方法的过程中,或者至少在每次sourcemodel刷新时,以编程方式处理“列排序方法”查找

有关最小可复制示例,请参见文章末尾

或查看以下代码片段:

class App(QtWidgets.QApplication):
    # responsible for instantiating the view, controllers, and model(s) 
    # and passing references between them.
    
    def __init__(self, sys_argv):
        super(App, self).__init__(sys_argv)
        
        self.ticks_model = TicksModel()
        self.proxy_ticks_model=QSortFilterProxyModel()
        self.proxy_ticks_model.setSourceModel(self.ticks_model)

        self.depth_model=DepthModel()
        self.depth_model_harem=DepthModelHarem()
        
        self.sc_model=ScModel()
        self.main_controller = MainController(self.ticks_model,
                                              self.proxy_ticks_model, self.depth_model,
                                              self.depth_model_harem,self.sc_model)
        
        self.main_view = MainView(self.ticks_model, 
                                  self.proxy_ticks_model, self.depth_model,
                                  self.depth_model_harem, self.sc_model,
                                  self.main_controller)
        
        self.main_view.show()
class DataFrameModel(QtCore.QAbstractTableModel):
    DtypeRole = QtCore.Qt.UserRole + 1000
    ValueRole = QtCore.Qt.UserRole + 1001
    # ColumnHasNumbersRole = QtCore.Qt.UserRole + 1002
# bunch of other methods
    def data(self, index, role=QtCore.Qt.DisplayRole):
        # dt stands for data type here
        if not index.isValid() or not (0 <= index.row() < self.rowCount() \
            and 0 <= index.column() < self.columnCount()):
            return QtCore.QVariant()
        row = self._dataframe.index[index.row()]
        col = self._dataframe.columns[index.column()]
        dt = self._dataframe[col].dtype

        val = self._dataframe.iloc[row][col]
        if role == QtCore.Qt.DisplayRole:
            return str(val)
        elif role == DataFrameModel.ValueRole:
            return val
        if role == DataFrameModel.DtypeRole:
            return dt

        if role == QtCore.Qt.BackgroundRole:
            if val == 'Sell':
                return QtGui.QColor('red')
            elif val == 'Buy':
                return QtGui.QColor('blue')
            
        return QtCore.QVariant()
class TicksModel(DataFrameModel):
    def __init__(self):
        super(TicksModel, self).__init__()
    def setDataFrame(self):
        self.beginResetModel()
        df=get_df()
        self._dataframe = df.copy()
        self.endResetModel()

@eyllanesc这里是一个最小的可复制示例

import logging 
__log__=logging.getLogger()
import sys 
import pandas as pd 

from PyQt5.QtCore import QObject, pyqtSlot
from PyQt5 import QtCore, QtGui, QtWidgets
from PyQt5.QtWidgets import QMainWindow, QComboBox
from PyQt5.QtCore import pyqtSlot, QDate, QTime, QSortFilterProxyModel

def get_df():
    return pd.DataFrame([{'int':-1,'str':'foo'},
            {'int':0,'str':'bar'},
            {'int':1, 'str':'abc'},
            {'int':10,'str':'abc'},
            {'int':101, 'str':'def'},
            {'int':2,'str':'def'}])
    

class DataFrameModel(QtCore.QAbstractTableModel):
    DtypeRole = QtCore.Qt.UserRole + 1000
    ValueRole = QtCore.Qt.UserRole + 1001
    ColumnHasNumbersRole = QtCore.Qt.UserRole + 1002

    def __init__(self, df=pd.DataFrame(), parent=None):
        super(DataFrameModel, self).__init__(parent)
        self._dataframe = df
        
    def dataFrame(self):
        return self._dataframe

    def setDataFrame(self):
        # i tend to overload this in child classes with the data getting
        return 
    # this seems to be some kind of after the fact decorator?? ask
    dataFrame = QtCore.pyqtProperty(pd.DataFrame, fget=dataFrame, fset=setDataFrame)

    @QtCore.pyqtSlot(int, QtCore.Qt.Orientation, result=str)
    def headerData(self, section: int, orientation: QtCore.Qt.Orientation,
                   role: int = QtCore.Qt.DisplayRole):
        if role == QtCore.Qt.DisplayRole:
            if orientation == QtCore.Qt.Horizontal:
                return self._dataframe.columns[section]
            else:
                return str(self._dataframe.index[section])
        return QtCore.QVariant()

    def rowCount(self, parent=QtCore.QModelIndex()):
        if parent.isValid():
            return 0
        return len(self._dataframe.index)

    def columnCount(self, parent=QtCore.QModelIndex()):
        if parent.isValid():
            return 0
        return self._dataframe.columns.size

    def data(self, index, role=QtCore.Qt.DisplayRole):
        # dt stands for data type here
        if not index.isValid() or not (0 <= index.row() < self.rowCount() \
            and 0 <= index.column() < self.columnCount()):
            return QtCore.QVariant()
        row = self._dataframe.index[index.row()]
        col = self._dataframe.columns[index.column()]
        dt = self._dataframe[col].dtype

        val = self._dataframe.iloc[row][col]
        if role == QtCore.Qt.DisplayRole:
            return str(val)
        elif role == DataFrameModel.ValueRole:
            return val
        if role == DataFrameModel.DtypeRole:
            return dt

        if role == QtCore.Qt.BackgroundRole:
            if val == 'Sell':
                return QtGui.QColor('red')
            elif val == 'Buy':
                return QtGui.QColor('blue')
            
        return QtCore.QVariant()
    
    def roleNames(self):
        roles = {QtCore.Qt.DisplayRole: b'display',
            DataFrameModel.DtypeRole: b'dtype',
            DataFrameModel.ValueRole: b'value'}
        return roles
    
    def signalUpdate(self):
        ''' tell viewers to update their data 
        (this is full update, not efficient)'''
        self.layoutChanged.emit()
    
    def sort(self, Ncol, order):
        """Sort table by given column number."""
        try:
            self.layoutAboutToBeChanged.emit()
            self._dataframe = self._dataframe.sort_values(self._dataframe.columns[Ncol], ascending=not order).reset_index(drop=True)
            self.layoutChanged.emit()
        except Exception as e:
            __log__.error(e)
            
class TicksModel(DataFrameModel):
    def __init__(self):
        super(TicksModel, self).__init__()
    def setDataFrame(self):
        self.beginResetModel()
        df=get_df()
        self._dataframe = df.copy()
        self.endResetModel()
        
class App(QtWidgets.QApplication):
    # responsible for instantiating the view, controllers, and model(s) 
    # and passing references between them.
    
    def __init__(self, sys_argv):
        super(App, self).__init__(sys_argv)
        
        self.ticks_model = TicksModel()
        self.proxy_ticks_model=QSortFilterProxyModel()
        self.proxy_ticks_model.setSourceModel(self.ticks_model)

        self.main_controller = MainController(self.ticks_model,
                                              self.proxy_ticks_model)
        
        self.main_view = MainView(self.ticks_model, 
                                  self.proxy_ticks_model, self.main_controller)
        
        self.main_view.show()
        

class MainController(QObject):
    # perform logic, set data in model
    
    def __init__(self, ticks_model, proxy_ticks_model, 
                 ):
        super().__init__()

        self._ticks_model = ticks_model
        self._proxy_ticks_model=proxy_ticks_model
        self.refresh_ticks_df()
        
    @pyqtSlot()
    def refresh_ticks_df(self):
        # methods here can be called from the view,
        # accept value as arguments from the widgets, say, performs logic, and sets attributes on the model.
        self._ticks_model.setDataFrame()
        
    @pyqtSlot()
    def setFilterRegExp(self, search):
        self._proxy_ticks_model.setFilterRegExp(search)
        
    @pyqtSlot(int)
    def setFilterKeyColumn(self, index):
        self._proxy_ticks_model.setFilterKeyColumn(index)

class MainView(QMainWindow):

# contain the minimal code required to connect to the signals coming from the widgets in your layout. 
# View events can call and pass basic information to a method in the view class and onto a method in a controller class,
# where any logic should be. It would look something like:

    def __init__(self, ticks_model, 
                 proxy_ticks_model, main_controller):
        super().__init__()
        self._ticks_model=ticks_model
        self._proxy_ticks_model = proxy_ticks_model
        self._main_controller = main_controller
        
        # import the relevant auto-generated classes from the .py layout files.
        self._ui = Ui_blackbird()
        self._ui.setupUi(self)

        # its possible thsi should go somewhere else, but thinking this makes sense
        # as its about the UI really
        self._ui.tableView.setModel(self._proxy_ticks_model)
        self._ui.tableView.resizeColumnsToContents()
        self._ui.tableView.setSortingEnabled(True)
        self._ui.comboBox.addItems(["{0}".format(col) for col in 
                                    self._ticks_model._dataframe.columns])

        # connect widgets to controller        
        self._ui.lineEdit_3.returnPressed.connect(self.on_lineEdit_textChanged)
        self._ui.pushButton_4.clicked.connect(self.on_lineEdit_textChanged)

        self._ui.pushButton.clicked.connect(self._main_controller.refresh_ticks_df)
        
        
    @pyqtSlot()
    def on_lineEdit_textChanged(self):
        text=self._ui.lineEdit_3.text().strip()
        search = QtCore.QRegExp(    text,
                                    QtCore.Qt.CaseInsensitive,
                                    QtCore.QRegExp.RegExp
                                    )

        self._main_controller.setFilterRegExp(search)

    @pyqtSlot(int)
    def on_comboBox_currentIndexChanged(self, index):
        self._main_controller.setFilterKeyColumn(index)
        

class Ui_blackbird(object):
    def setupUi(self, blackbird):
        blackbird.setObjectName("blackbird")
        blackbird.resize(920, 586)
        sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding)
        sizePolicy.setHorizontalStretch(0)
        sizePolicy.setVerticalStretch(0)
        sizePolicy.setHeightForWidth(blackbird.sizePolicy().hasHeightForWidth())
        blackbird.setSizePolicy(sizePolicy)
        self.centralwidget = QtWidgets.QWidget(blackbird)
        sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding)
        sizePolicy.setHorizontalStretch(1)
        sizePolicy.setVerticalStretch(1)
        sizePolicy.setHeightForWidth(self.centralwidget.sizePolicy().hasHeightForWidth())
        self.centralwidget.setSizePolicy(sizePolicy)
        self.centralwidget.setObjectName("centralwidget")
        self.formLayout = QtWidgets.QFormLayout(self.centralwidget)
        self.formLayout.setObjectName("formLayout")
        self.tabWidget = QtWidgets.QTabWidget(self.centralwidget)
        sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding)
        sizePolicy.setHorizontalStretch(1)
        sizePolicy.setVerticalStretch(1)
        sizePolicy.setHeightForWidth(self.tabWidget.sizePolicy().hasHeightForWidth())
        self.tabWidget.setSizePolicy(sizePolicy)
        self.tabWidget.setObjectName("tabWidget")
        self.tab_3 = QtWidgets.QWidget()
        sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred)
        sizePolicy.setHorizontalStretch(0)
        sizePolicy.setVerticalStretch(0)
        sizePolicy.setHeightForWidth(self.tab_3.sizePolicy().hasHeightForWidth())
        self.tab_3.setSizePolicy(sizePolicy)
        self.tab_3.setObjectName("tab_3")
        self.gridLayout = QtWidgets.QGridLayout(self.tab_3)
        self.gridLayout.setObjectName("gridLayout")
        self.tableView = QtWidgets.QTableView(self.tab_3)
        sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding)
        sizePolicy.setHorizontalStretch(1)
        sizePolicy.setVerticalStretch(1)
        sizePolicy.setHeightForWidth(self.tableView.sizePolicy().hasHeightForWidth())
        self.tableView.setSizePolicy(sizePolicy)
        self.tableView.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
        self.tableView.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
        self.tableView.setSizeAdjustPolicy(QtWidgets.QAbstractScrollArea.AdjustToContents)
        self.tableView.setSortingEnabled(True)
        self.tableView.setObjectName("tableView")
        self.gridLayout.addWidget(self.tableView, 2, 0, 1, 1)
        self.groupBox = QtWidgets.QGroupBox(self.tab_3)
        self.groupBox.setObjectName("groupBox")
        self.gridLayout_2 = QtWidgets.QGridLayout(self.groupBox)
        self.gridLayout_2.setObjectName("gridLayout_2")
        self.comboBox = QtWidgets.QComboBox(self.groupBox)
        self.comboBox.setObjectName("comboBox")
        self.gridLayout_2.addWidget(self.comboBox, 1, 1, 1, 1)
        self.label_2 = QtWidgets.QLabel(self.groupBox)
        self.label_2.setObjectName("label_2")
        self.gridLayout_2.addWidget(self.label_2, 1, 2, 1, 1)
        self.pushButton = QtWidgets.QPushButton(self.groupBox)
        sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed)
        sizePolicy.setHorizontalStretch(0)
        sizePolicy.setVerticalStretch(0)
        sizePolicy.setHeightForWidth(self.pushButton.sizePolicy().hasHeightForWidth())
        self.pushButton.setSizePolicy(sizePolicy)
        self.pushButton.setObjectName("pushButton")
        self.gridLayout_2.addWidget(self.pushButton, 0, 0, 1, 7)
        self.label = QtWidgets.QLabel(self.groupBox)
        self.label.setObjectName("label")
        self.gridLayout_2.addWidget(self.label, 1, 0, 1, 1)
        self.lineEdit_3 = QtWidgets.QLineEdit(self.groupBox)
        self.lineEdit_3.setObjectName("lineEdit_3")
        self.gridLayout_2.addWidget(self.lineEdit_3, 1, 3, 1, 1)
        self.pushButton_4 = QtWidgets.QPushButton(self.groupBox)
        self.pushButton_4.setObjectName("pushButton_4")
        self.gridLayout_2.addWidget(self.pushButton_4, 1, 4, 1, 1)
        self.gridLayout.addWidget(self.groupBox, 1, 0, 1, 1)
        self.tabWidget.addTab(self.tab_3, "")
        self.tab_4 = QtWidgets.QWidget()
        self.tab_4.setObjectName("tab_4")
        self.verticalLayout = QtWidgets.QVBoxLayout(self.tab_4)
        self.verticalLayout.setObjectName("verticalLayout")
        self.lineEdit = QtWidgets.QLineEdit(self.tab_4)
        self.lineEdit.setObjectName("lineEdit")
        self.verticalLayout.addWidget(self.lineEdit)
        self.dateTimeEdit = QtWidgets.QDateTimeEdit(self.tab_4)
        self.dateTimeEdit.setObjectName("dateTimeEdit")
        self.verticalLayout.addWidget(self.dateTimeEdit)
        self.pushButton_2 = QtWidgets.QPushButton(self.tab_4)
        self.pushButton_2.setObjectName("pushButton_2")
        self.verticalLayout.addWidget(self.pushButton_2)
        self.tableView_2 = QtWidgets.QTableView(self.tab_4)
        sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding)
        sizePolicy.setHorizontalStretch(1)
        sizePolicy.setVerticalStretch(1)
        sizePolicy.setHeightForWidth(self.tableView_2.sizePolicy().hasHeightForWidth())
        self.tableView_2.setSizePolicy(sizePolicy)
        font = QtGui.QFont()
        font.setPointSize(8)
        self.tableView_2.setFont(font)
        self.tableView_2.setLineWidth(0)
        self.tableView_2.setObjectName("tableView_2")
        self.verticalLayout.addWidget(self.tableView_2)
        self.tableView_4 = QtWidgets.QTableView(self.tab_4)
        sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding)
        sizePolicy.setHorizontalStretch(1)
        sizePolicy.setVerticalStretch(1)
        sizePolicy.setHeightForWidth(self.tableView_4.sizePolicy().hasHeightForWidth())
        self.tableView_4.setSizePolicy(sizePolicy)
        font = QtGui.QFont()
        font.setPointSize(8)
        self.tableView_4.setFont(font)
        self.tableView_4.setLineWidth(0)
        self.tableView_4.setObjectName("tableView_4")
        self.verticalLayout.addWidget(self.tableView_4)
        self.tabWidget.addTab(self.tab_4, "")
        self.tab_13 = QtWidgets.QWidget()
        self.tab_13.setObjectName("tab_13")
        self.verticalLayout_2 = QtWidgets.QVBoxLayout(self.tab_13)
        self.verticalLayout_2.setObjectName("verticalLayout_2")
        self.pushButton_3 = QtWidgets.QPushButton(self.tab_13)
        self.pushButton_3.setObjectName("pushButton_3")
        self.verticalLayout_2.addWidget(self.pushButton_3)
        self.lineEdit_2 = QtWidgets.QLineEdit(self.tab_13)
        self.lineEdit_2.setObjectName("lineEdit_2")
        self.verticalLayout_2.addWidget(self.lineEdit_2)
        self.tableView_3 = QtWidgets.QTableView(self.tab_13)
        sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding)
        sizePolicy.setHorizontalStretch(1)
        sizePolicy.setVerticalStretch(1)
        sizePolicy.setHeightForWidth(self.tableView_3.sizePolicy().hasHeightForWidth())
        self.tableView_3.setSizePolicy(sizePolicy)
        self.tableView_3.setSortingEnabled(True)
        self.tableView_3.setObjectName("tableView_3")
        self.verticalLayout_2.addWidget(self.tableView_3)
        self.tabWidget.addTab(self.tab_13, "")
        self.formLayout.setWidget(0, QtWidgets.QFormLayout.SpanningRole, self.tabWidget)
        blackbird.setCentralWidget(self.centralwidget)
        self.statusbar = QtWidgets.QStatusBar(blackbird)
        self.statusbar.setObjectName("statusbar")
        blackbird.setStatusBar(self.statusbar)

        self.retranslateUi(blackbird)
        self.tabWidget.setCurrentIndex(0)
        QtCore.QMetaObject.connectSlotsByName(blackbird)

    def retranslateUi(self, blackbird):
        _translate = QtCore.QCoreApplication.translate
        blackbird.setWindowTitle(_translate("blackbird", "Blackbird"))
        self.groupBox.setTitle(_translate("blackbird", "Actions"))
        self.label_2.setText(_translate("blackbird", "Regex:"))
        self.pushButton.setText(_translate("blackbird", "refresh"))
        self.label.setText(_translate("blackbird", "Regex Filter Column: "))
        self.pushButton_4.setText(_translate("blackbird", "Push Me or just Hit Enter Regex Filter"))
        self.tabWidget.setTabText(self.tabWidget.indexOf(self.tab_3), _translate("blackbird", "Ticks"))
        self.dateTimeEdit.setDisplayFormat(_translate("blackbird", "M/d/yyyy h:mm:ss AP"))
        self.pushButton_2.setText(_translate("blackbird", "Load"))
        self.tabWidget.setTabText(self.tabWidget.indexOf(self.tab_4), _translate("blackbird", ""))
        self.pushButton_3.setText(_translate("blackbird", "Load"))
        self.tabWidget.setTabText(self.tabWidget.indexOf(self.tab_13), _translate("blackbird", ""))


        
if __name__ == '__main__':
    logging.basicConfig()
    app = App(sys.argv)
    try:
        sys.exit(app.exec_())
    except Exception as e:
        __log__.error('%s', e)

Tags: selfmodelreturndefqttabblackbirdticks
1条回答
网友
1楼 · 发布于 2024-05-19 00:00:52

一个简单的解决方案: 从重新实现的QSortFilterProxyModel的lessThan方法调用源模型(从QabStretctTableModel继承)中的data方法,传递一个角色参数,该参数将向lessThan方法而不是字符串公开值。然后是自然<;运算符排序“正常工作”,因为它对值进行排序。。。(只要值具有可比性(我添加了一些无处理等,以避免在不可比数据上崩溃))

因此,重新实现的QSortFilterProxy模型可能如下所示:

class DataframeQSortFilterProxyModel(QtCore.QSortFilterProxyModel):
    def __init__(self):
        super(DataframeQSortFilterProxyModel, self).__init__()
        self.role=DataFrameModel.ValueRole
        
    def lessThan(self, left, right):
        role=self.role
        leftData=self.sourceModel().data(left, role)
        rightData=self.sourceModel().data(right, role)
        if leftData is None:
            return True
        elif rightData is None:
            return False
        elif type(leftData) != type(rightData): 
            # don't want to sort at all in these cases, False is just a copout ...
            # should warn user
            return False
        return leftData < rightData

其中源模型中的数据方法与以前类似,如下所示:

    def data(self, index, role=QtCore.Qt.DisplayRole):
        # dt stands for data type here
        if not index.isValid() or not (0 <= index.row() < self.rowCount() \
            and 0 <= index.column() < self.columnCount()):
            return QtCore.QVariant()
        row = self._dataframe.index[index.row()]
        col = self._dataframe.columns[index.column()]
        dt = self._dataframe[col].dtype

        val = self._dataframe.iloc[row][col]
        if role == QtCore.Qt.DisplayRole:
            return str(val)
        elif role == DataFrameModel.ValueRole:
            return val
        elif role == DataFrameModel.DtypeRole:
            return dt
        elif role == QtCore.Qt.BackgroundRole:
            if val == 'Sell':
                return QtGui.QColor('red')
            elif val == 'Buy':
                return QtGui.QColor('blue')
            
        return QtCore.QVariant()

相关问题 更多 >

    热门问题