如何在PyQt中为QTableView创建过滤器

18

我正在使用QTableView来显示从QtSql.QSqlQuery检索的数据。

我想知道如何创建类似于Excel的过滤器。

输入图像描述

在上面的图像中,我需要获取所有标题(Sh_Code,SH_Seq,Stage)的筛选器。过滤器将具有该列中的唯一值,可对其进行过滤。

所需结果

我需要表视图标头,其中包含下拉列表,其中包含该列中所有唯一值,就像Excel中那样。不需要像图片中显示的“Top,Standard Filter ...”,只需要“All”和唯一的“column items”

输入图像描述

这是我.NET应用程序中的截图,为了更清楚地上传。

输入图像描述

4个回答

32
这里是使用QSortFilterProxyModelQStandardItemModelQTableView在PyQt中进行过滤的示例,可以轻松适应其他视图和模型:
#!/usr/bin/env python
#-*- coding:utf-8 -*-

from PyQt4 import QtCore, QtGui

class myWindow(QtGui.QMainWindow):
    def __init__(self, parent=None):
        super(myWindow, self).__init__(parent)
        self.centralwidget  = QtGui.QWidget(self)
        self.lineEdit       = QtGui.QLineEdit(self.centralwidget)
        self.view           = QtGui.QTableView(self.centralwidget)
        self.comboBox       = QtGui.QComboBox(self.centralwidget)
        self.label          = QtGui.QLabel(self.centralwidget)

        self.gridLayout = QtGui.QGridLayout(self.centralwidget)
        self.gridLayout.addWidget(self.lineEdit, 0, 1, 1, 1)
        self.gridLayout.addWidget(self.view, 1, 0, 1, 3)
        self.gridLayout.addWidget(self.comboBox, 0, 2, 1, 1)
        self.gridLayout.addWidget(self.label, 0, 0, 1, 1)

        self.setCentralWidget(self.centralwidget)
        self.label.setText("Regex Filter")

        self.model = QtGui.QStandardItemModel(self)

        for rowName in range(3) * 5:
            self.model.invisibleRootItem().appendRow(
                [   QtGui.QStandardItem("row {0} col {1}".format(rowName, column))    
                    for column in range(3)
                    ]
                )

        self.proxy = QtGui.QSortFilterProxyModel(self)
        self.proxy.setSourceModel(self.model)

        self.view.setModel(self.proxy)
        self.comboBox.addItems(["Column {0}".format(x) for x in range(self.model.columnCount())])

        self.lineEdit.textChanged.connect(self.on_lineEdit_textChanged)
        self.comboBox.currentIndexChanged.connect(self.on_comboBox_currentIndexChanged)

        self.horizontalHeader = self.view.horizontalHeader()
        self.horizontalHeader.sectionClicked.connect(self.on_view_horizontalHeader_sectionClicked)

    @QtCore.pyqtSlot(int)
    def on_view_horizontalHeader_sectionClicked(self, logicalIndex):
        self.logicalIndex   = logicalIndex
        self.menuValues     = QtGui.QMenu(self)
        self.signalMapper   = QtCore.QSignalMapper(self)  

        self.comboBox.blockSignals(True)
        self.comboBox.setCurrentIndex(self.logicalIndex)
        self.comboBox.blockSignals(True)

        valuesUnique = [    self.model.item(row, self.logicalIndex).text()
                            for row in range(self.model.rowCount())
                            ]

        actionAll = QtGui.QAction("All", self)
        actionAll.triggered.connect(self.on_actionAll_triggered)
        self.menuValues.addAction(actionAll)
        self.menuValues.addSeparator()

        for actionNumber, actionName in enumerate(sorted(list(set(valuesUnique)))):              
            action = QtGui.QAction(actionName, self)
            self.signalMapper.setMapping(action, actionNumber)  
            action.triggered.connect(self.signalMapper.map)  
            self.menuValues.addAction(action)

        self.signalMapper.mapped.connect(self.on_signalMapper_mapped)  

        headerPos = self.view.mapToGlobal(self.horizontalHeader.pos())        

        posY = headerPos.y() + self.horizontalHeader.height()
        posX = headerPos.x() + self.horizontalHeader.sectionPosition(self.logicalIndex)

        self.menuValues.exec_(QtCore.QPoint(posX, posY))

    @QtCore.pyqtSlot()
    def on_actionAll_triggered(self):
        filterColumn = self.logicalIndex
        filterString = QtCore.QRegExp(  "",
                                        QtCore.Qt.CaseInsensitive,
                                        QtCore.QRegExp.RegExp
                                        )

        self.proxy.setFilterRegExp(filterString)
        self.proxy.setFilterKeyColumn(filterColumn)

    @QtCore.pyqtSlot(int)
    def on_signalMapper_mapped(self, i):
        stringAction = self.signalMapper.mapping(i).text()
        filterColumn = self.logicalIndex
        filterString = QtCore.QRegExp(  stringAction,
                                        QtCore.Qt.CaseSensitive,
                                        QtCore.QRegExp.FixedString
                                        )

        self.proxy.setFilterRegExp(filterString)
        self.proxy.setFilterKeyColumn(filterColumn)

    @QtCore.pyqtSlot(str)
    def on_lineEdit_textChanged(self, text):
        search = QtCore.QRegExp(    text,
                                    QtCore.Qt.CaseInsensitive,
                                    QtCore.QRegExp.RegExp
                                    )

        self.proxy.setFilterRegExp(search)

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


if __name__ == "__main__":
    import sys

    app  = QtGui.QApplication(sys.argv)
    main = myWindow()
    main.show()
    main.resize(400, 600)
    sys.exit(app.exec_())

为了获得所需的结果,单击标题会启动一个弹出菜单,并填充该列的唯一值。选择弹出菜单中的项目后,将该值传递给self.proxy.setFilterRegExp(filterString)和列self.proxy.setFilterKeyColumn(filterValue)

image


@PBLNarasimhaRao 忘记连接插槽了!我更新了代码,现在应该可以工作了。 - user1006989
我尝试使用self.view.horizontalHeader.sectionClicked(self.headerclick),但是出现了错误AttributeError: 'builtin_function_or_method' object has no attribute 'sectionClicked' - Rao
让我们在聊天中继续这个讨论:http://chat.stackoverflow.com/rooms/22034/discussion-between-pbl-narasimha-rao-and-x-jacobs - Rao
尝试使用以下代码替换:self.view.horizontalHeader().sectionClicked(self.headerclick) - user1006989
@PBLNarasimhaRao sectionClicked 返回一个数字,该数字是列的编号,请创建一个新帖子以提出任何进一步的问题,以避免延长此线程。 - user1006989
显示剩余10条评论

10

我尝试更新上面提供的答案,适用于PyQt5

from PyQt5 import QtCore, QtGui, QtWidgets

class myWindow(QtWidgets.QMainWindow):
    def __init__(self, parent=None):
        super(myWindow, self).__init__(parent)
        self.centralwidget  = QtWidgets.QWidget(self)
        self.lineEdit       = QtWidgets.QLineEdit(self.centralwidget)
        self.view           = QtWidgets.QTableView(self.centralwidget)
        self.comboBox       = QtWidgets.QComboBox(self.centralwidget)
        self.label          = QtWidgets.QLabel(self.centralwidget)

        self.gridLayout = QtWidgets.QGridLayout(self.centralwidget)
        self.gridLayout.addWidget(self.lineEdit, 0, 1, 1, 1)
        self.gridLayout.addWidget(self.view, 1, 0, 1, 3)
        self.gridLayout.addWidget(self.comboBox, 0, 2, 1, 1)
        self.gridLayout.addWidget(self.label, 0, 0, 1, 1)

        self.setCentralWidget(self.centralwidget)
        self.label.setText("Regex Filter")

        self.model = QtGui.QStandardItemModel(self)

        for rowName in range(3*5):
            self.model.invisibleRootItem().appendRow(
                [   QtGui.QStandardItem("row {0} col {1}".format(rowName, column))    
                    for column in range(3)
                    ]
                )

        self.proxy = QtCore.QSortFilterProxyModel(self)
        self.proxy.setSourceModel(self.model)

        self.view.setModel(self.proxy)
        self.comboBox.addItems(["Column {0}".format(x) for x in range(self.model.columnCount())])

        self.lineEdit.textChanged.connect(self.on_lineEdit_textChanged)
        self.comboBox.currentIndexChanged.connect(self.on_comboBox_currentIndexChanged)

        self.horizontalHeader = self.view.horizontalHeader()
        self.horizontalHeader.sectionClicked.connect(self.on_view_horizontalHeader_sectionClicked)

    @QtCore.pyqtSlot(int)
    def on_view_horizontalHeader_sectionClicked(self, logicalIndex):
        self.logicalIndex   = logicalIndex
        self.menuValues     = QtWidgets.QMenu(self)
        self.signalMapper   = QtCore.QSignalMapper(self)  

        self.comboBox.blockSignals(True)
        self.comboBox.setCurrentIndex(self.logicalIndex)
        self.comboBox.blockSignals(True)

        valuesUnique = [    self.model.item(row, self.logicalIndex).text()
                            for row in range(self.model.rowCount())
                            ]

        actionAll = QtWidgets.QAction("All", self)
        actionAll.triggered.connect(self.on_actionAll_triggered)
        self.menuValues.addAction(actionAll)
        self.menuValues.addSeparator()

        for actionNumber, actionName in enumerate(sorted(list(set(valuesUnique)))):              
            action = QtWidgets.QAction(actionName, self)
            self.signalMapper.setMapping(action, actionNumber)  
            action.triggered.connect(self.signalMapper.map)  
            self.menuValues.addAction(action)

        self.signalMapper.mapped.connect(self.on_signalMapper_mapped)  

        headerPos = self.view.mapToGlobal(self.horizontalHeader.pos())        

        posY = headerPos.y() + self.horizontalHeader.height()
        posX = headerPos.x() + self.horizontalHeader.sectionPosition(self.logicalIndex)

        self.menuValues.exec_(QtCore.QPoint(posX, posY))

    @QtCore.pyqtSlot()
    def on_actionAll_triggered(self):
        filterColumn = self.logicalIndex
        filterString = QtCore.QRegExp(  "",
                                        QtCore.Qt.CaseInsensitive,
                                        QtCore.QRegExp.RegExp
                                        )

        self.proxy.setFilterRegExp(filterString)
        self.proxy.setFilterKeyColumn(filterColumn)

    @QtCore.pyqtSlot(int)
    def on_signalMapper_mapped(self, i):
        stringAction = self.signalMapper.mapping(i).text()
        filterColumn = self.logicalIndex
        filterString = QtCore.QRegExp(  stringAction,
                                        QtCore.Qt.CaseSensitive,
                                        QtCore.QRegExp.FixedString
                                        )

        self.proxy.setFilterRegExp(filterString)
        self.proxy.setFilterKeyColumn(filterColumn)

    @QtCore.pyqtSlot(str)
    def on_lineEdit_textChanged(self, text):
        search = QtCore.QRegExp(    text,
                                    QtCore.Qt.CaseInsensitive,
                                    QtCore.QRegExp.RegExp
                                    )

        self.proxy.setFilterRegExp(search)

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


if __name__ == "__main__":
    import sys

    app  = QtWidgets.QApplication(sys.argv)
    main = myWindow()
    main.show()
    main.resize(400, 600)
    sys.exit(app.exec_())

@Behzad Jamali 来自 https://doc.qt.io/archives/qt-5.10/qsignalmapper.html 的消息称 QSignalMapper Class 已被弃用。我想知道是否有任何替代方案。action.triggered.connect(lambda: self.on_signalMapper_mapped(actionName)) 是否是一种可能的方法。谢谢。 - Alana

3
根据@user1006989和@Behzad Jamali的回答: 如果表格比当前视口更宽,则用于在标题上过滤的菜单位置不会在确切位置弹出。
为了正确定位弹出菜单,请使用以下代码:
posX = headerPos.x() + self.horizontalHeader.sectionViewportPosition(index)

0

我尝试更新PyQt6的答案。 我使用了QListWidget而不是QMenu,当你有大量数据时,它会创建一个滚动条。我创建了一个函数来读取Excel文件。对于过滤器位置问题,我添加了一个判断语句,以便过滤器不会超出屏幕。

import sys
from PyQt6.QtWidgets import *

from PyQt6 import QtWidgets
from PyQt6 import QtCore,QtGui
import pandas as pd

class Table(QWidget):
    def __init__(self):
        super().__init__()
        self.initUI()
    def initUI(self):
        self.setWindowTitle('QTableWidget')
        self.setGeometry(300, 300, 500, 300)
        self.table = QTableWidget()
        self.table.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeMode.ResizeToContents)
        self.centralwidget = QtWidgets.QWidget(self)

        self.btn1 = QPushButton('Unfilter', self)
        self.btn1.clicked.connect(self.Unfilter)

        # 创建布局
        self.filter_layout = QHBoxLayout()
        self.filter_layout.addStretch(3)
        self.filter_layout.addWidget(self.btn1)

        self.layout = QVBoxLayout()
        self.layout.addLayout(self.filter_layout)
        self.layout.addWidget(self.table)
        self.setLayout(self.layout)

        self.horizontalHeader = self.table.horizontalHeader()
        self.horizontalHeader.sectionClicked.connect(self.on_view_horizontalHeader_sectionClicked)
        filename = 'test.xlsx'
        self.df1 = pd.read_excel(filename, engine='openpyxl', sheet_name='Sheet1', header=0)
        # self.df1['Date'] =self.df1['Date'].dt.date
        self.insertData(self.df1)

    def on_view_horizontalHeader_sectionClicked(self, logicalIndex):
        self.logicalIndex   = logicalIndex
        self.menuValues     = QtWidgets.QMenu(self)
        self.signalMapper   = QtCore.QSignalMapper(self)

        valuesUnique = [self.table.item(row, self.logicalIndex).text() for row in range(self.table.rowCount()) if not self.table.isRowHidden(row)]

        # 子窗口
        self.Menudialog = QDialog(self)
        self.Menudialog.setWindowTitle('Sub Window')
        self.Menudialog.resize(200, 150)

        self.list_widget = QListWidget(self)
        self.list_widget.addItems(sorted(list(set(valuesUnique))))
        # create QScrollArea,put QListWidget in it
        scroll_area = QScrollArea(self.Menudialog)
        scroll_area.setWidgetResizable(True)
        scroll_area.setWidget(self.list_widget)

        self.list_widget.currentItemChanged.connect(self.on_list_item_clicked)
        layout = QVBoxLayout(self.Menudialog)
        layout.addWidget(scroll_area)

        self.setLayout(layout)

        headerPos = self.table.mapToGlobal(self.horizontalHeader.pos())
        posY = headerPos.y() + self.horizontalHeader.height()
        # posX = headerPos.x() + self.horizontalHeader.sectionPosition(self.logicalIndex)
        posX = headerPos.x() + self.horizontalHeader.sectionViewportPosition(self.logicalIndex)
        print(posX)
        if posX > 1700:
            posX = 1620

        self.Menudialog.setGeometry(posX +100, posY, 200, 300)
        self.Menudialog.exec()

        ''' 
        actionAll = QtGui.QAction("All", self)
        actionAll.triggered.connect(self.Unfilter)
        self.menuValues.addAction(actionAll)
        self.menuValues.addSeparator()

        for actionNumber, actionName in enumerate(sorted(list(set(valuesUnique)))):
            action = QtGui.QAction(actionName, self)
            self.signalMapper.setMapping(action, actionNumber)
            action.triggered.connect(self.signalMapper.map)
            self.menuValues.addAction(action)

        self.signalMapper.mappedInt.connect(self.on_signalMapper_mapped)

        headerPos = self.table.mapToGlobal(self.horizontalHeader.pos())

        posY = headerPos.y() + self.horizontalHeader.height()
        posX = headerPos.x() + self.horizontalHeader.sectionPosition(self.logicalIndex)

        self.menuValues.exec(QtCore.QPoint(posX, posY))
        '''
    def on_list_item_clicked(self):
        item = self.list_widget.currentItem()
        column = self.logicalIndex
        for i in range(self.table.rowCount()):
            if self.table.item(i, column).text() != item.text():
                self.table.setRowHidden(i, True)
        self.Menudialog.close()

    def on_signalMapper_mapped(self, i):
        stringAction = self.signalMapper.mapping(i).text()
        column = self.logicalIndex
        for i in range(self.table.rowCount()):
            if self.table.item(i, column).text() != stringAction:
                self.table.setRowHidden(i, True)

    def filter(self):
        column = self.filter_box.currentIndex()
        for i in range(self.table.rowCount()):
            if self.table.item(i, column).text() != self.search_box.text() and self.search_box.text() !='':
                self.table.setRowHidden(i, True)

    def search(self):
        text = self.search_box.text()
        for i in range(self.table.rowCount()):
            self.table.setRowHidden(i, False)

        for i in range(self.table.rowCount()):
            for j in range(self.table.columnCount()):
                if text in self.table.item(i, j).text():
                    break
                else:
                    self.table.setRowHidden(i, True)

    def Unfilter(self):
        for i in range(self.table.rowCount()):
                self.table.showRow(i)
    def insertData(self, df1):
        if len(df1) < 1:
            msgBox = QMessageBox(self)
            msgBox.setWindowTitle('注意')
            msgBox.setText('数据集为空,不能显示!')
            msgBox.setStandardButtons(QMessageBox.StandardButton.Yes)
            msgBox.exec()
            return
        # 获取列名
        self.header = df1.columns.tolist()
        # 获取数据
        data = df1.values.tolist()
        self.row =len(data)
        self.col = len(data[0])
        # 设置表格的行数为0,以清空之前的内容
        self.table.setRowCount(0)
        self.table.setRowCount(self.row)
        self.table.setColumnCount(self.col)
        self.table.setHorizontalHeaderLabels(self.header)
        if data:
            # 按行添加数据
            for r in range(self.row):
                for c in range(self.col):
                    # 添加表格数据
                    newItem = QTableWidgetItem(str(data[r][c]))
                    self.table.setItem(r, c, newItem)
        else:
            self.label.setText('无数据')

if __name__ == '__main__':
    app = QApplication(sys.argv)
    table = Table()
    table.show()
    sys.exit(app.exec())

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接