PyQT5 和使用多列过滤表
Posted
技术标签:
【中文标题】PyQT5 和使用多列过滤表【英文标题】:PyQT5 and Filtering a Table using multiple columns 【发布时间】:2020-02-26 04:56:03 【问题描述】:我正在尝试制作PyQt5
GUI,以表格形式显示 Pandas 数据框并提供列过滤选项,类似于 Microsoft Excel 过滤器。到目前为止,我设法采用了类似的SO answer。这是我在 GUI 中的表格的图片:
如上图所示,有两种过滤列的方法:Regex Filter 和点击每一列。但是,我需要帮助解决一个问题:当我过滤第二列时,当前应用的过滤器(正则表达式过滤器或列单击)会消失。我想要第二个过滤器为AND
,即满足第1列AND
第2列的过滤器。
这是我的代码:
#!/usr/bin/env python
#-*- coding:utf-8 -*-
from PyQt5 import QtCore, QtGui, QtWidgets
import pandas as pd
class PandasModel(QtCore.QAbstractTableModel):
def __init__(self, df=pd.DataFrame(), parent=None):
QtCore.QAbstractTableModel.__init__(self, parent=parent)
self._df = df.copy()
def toDataFrame(self):
return self._df.copy()
def headerData(self, section, orientation, role=QtCore.Qt.DisplayRole):
if role != QtCore.Qt.DisplayRole:
return QtCore.QVariant()
if orientation == QtCore.Qt.Horizontal:
try:
return self._df.columns.tolist()[section]
except (IndexError, ):
return QtCore.QVariant()
elif orientation == QtCore.Qt.Vertical:
try:
# return self.df.index.tolist()
return self._df.index.tolist()[section]
except (IndexError, ):
return QtCore.QVariant()
def data(self, index, role=QtCore.Qt.DisplayRole):
if role != QtCore.Qt.DisplayRole:
return QtCore.QVariant()
if not index.isValid():
return QtCore.QVariant()
return QtCore.QVariant(str(self._df.iloc[index.row(), index.column()]))
def setData(self, index, value, role):
row = self._df.index[index.row()]
col = self._df.columns[index.column()]
if hasattr(value, 'toPyObject'):
# PyQt4 gets a QVariant
value = value.toPyObject()
else:
# PySide gets an unicode
dtype = self._df[col].dtype
if dtype != object:
value = None if value == '' else dtype.type(value)
self._df.set_value(row, col, value)
return True
def rowCount(self, parent=QtCore.QModelIndex()):
return len(self._df.index)
def columnCount(self, parent=QtCore.QModelIndex()):
return len(self._df.columns)
def sort(self, column, order):
colname = self._df.columns.tolist()[column]
self.layoutAboutToBeChanged.emit()
self._df.sort_values(colname, ascending= order == QtCore.Qt.AscendingOrder, inplace=True)
self._df.reset_index(inplace=True, drop=True)
self.layoutChanged.emit()
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.load_sites()
self.comboBox.addItems(["0".format(col) for col in self.model._df.columns])
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)
def load_sites(self):
df = pd.DataFrame('site_codes': ['01', '02', '03', '04'],
'status': ['open', 'open', 'open', 'closed'],
'Location': ['east', 'north', 'south', 'east'],
'data_quality': ['poor', 'moderate', 'high', 'high'])
self.model = PandasModel(df)
self.proxy = QtCore.QSortFilterProxyModel(self)
self.proxy.setSourceModel(self.model)
self.view.setModel(self.proxy)
self.view.resizeColumnsToContents()
@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._df.iloc[:, self.logicalIndex].unique()
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(800, 600)
sys.exit(app.exec_())
【问题讨论】:
当应用2个或更多过滤器时,操作是AND还是OR?,即如果应用过滤器F1和过滤器F2,那么只有满足F1 AND F2或至少满足的行应该显示 F1 或 F2 运算为AND。好点,我应该清除它。 好的,我了解弹出窗口的工作原理,因为过滤器应用于列,但是 QLineEdit 文本过滤器应用于哪个列?而且那个过滤器也应用了 AND 函数? 我计划稍后为每一列添加一个单独的 QLineEdit 文本过滤器,但现在请考虑将它用于第一列。它是一个 AND 函数 @BehzadJamali 所以你想应用 多个 文本过滤器,每个过滤器到不同的列? 【参考方案1】:如果要实现自定义过滤流程,则必须重写filterAcceptsRow方法,获取每一列的文本并验证是否满足条件,如果满足则返回True,否则返回False。要重新计算过滤器,您必须调用 invalidateFilter 方法:
class CustomProxyModel(QtCore.QSortFilterProxyModel):
def __init__(self, parent=None):
super().__init__(parent)
self._filters = dict()
@property
def filters(self):
return self._filters
def setFilter(self, expresion, column):
if expresion:
self.filters[column] = expresion
elif column in self.filters:
del self.filters[column]
self.invalidateFilter()
def filterAcceptsRow(self, source_row, source_parent):
for column, expresion in self.filters.items():
text = self.sourceModel().index(source_row, column, source_parent).data()
regex = QtCore.QRegExp(
expresion, QtCore.Qt.CaseInsensitive, QtCore.QRegExp.RegExp
)
if regex.indexIn(text) == -1:
return False
return True
class myWindow(QtWidgets.QMainWindow):
# ...
def load_sites(self):
# ...
self.model = PandasModel(df)
self.proxy = CustomProxyModel(self)
self.proxy.setSourceModel(self.model)
self.view.setModel(self.proxy)
self.view.resizeColumnsToContents()
print("finished loading sites")
# ...
@QtCore.pyqtSlot()
def on_actionAll_triggered(self):
filterColumn = self.logicalIndex
self.proxy.setFilter("", filterColumn)
@QtCore.pyqtSlot(int)
def on_signalMapper_mapped(self, i):
stringAction = self.signalMapper.mapping(i).text()
filterColumn = self.logicalIndex
self.proxy.setFilter(stringAction, filterColumn)
@QtCore.pyqtSlot(str)
def on_lineEdit_textChanged(self, text):
self.proxy.setFilter(text, self.proxy.filterKeyColumn())
@QtCore.pyqtSlot(int)
def on_comboBox_currentIndexChanged(self, index):
self.proxy.setFilterKeyColumn(index)
加: 如果要更改 QHeaderView 的字体,则必须返回 headerData 中的字体,如下所示:
class PandasModel(QtCore.QAbstractTableModel):
def __init__(self, df=pd.DataFrame(), parent=None):
QtCore.QAbstractTableModel.__init__(self, parent=parent)
self._df = df.copy()
self.bolds = dict()
def toDataFrame(self):
return self._df.copy()
def headerData(self, section, orientation, role=QtCore.Qt.DisplayRole):
if orientation == QtCore.Qt.Horizontal:
if role == QtCore.Qt.DisplayRole:
try:
return self._df.columns.tolist()[section]
except (IndexError,):
return QtCore.QVariant()
elif role == QtCore.Qt.FontRole:
return self.bolds.get(section, QtCore.QVariant())
elif orientation == QtCore.Qt.Vertical:
if role == QtCore.Qt.DisplayRole:
try:
# return self.df.index.tolist()
return self._df.index.tolist()[section]
except (IndexError,):
return QtCore.QVariant()
return QtCore.QVariant()
def setFont(self, section, font):
self.bolds[section] = font
self.headerDataChanged.emit(QtCore.Qt.Horizontal, 0, self.columnCount())
# ...
class myWindow(QtWidgets.QMainWindow):
# ...
@QtCore.pyqtSlot()
def on_actionAll_triggered(self):
filterColumn = self.logicalIndex
self.proxy.setFilter("", filterColumn)
font = QtGui.QFont()
self.model.setFont(filterColumn, font)
@QtCore.pyqtSlot(int)
def on_signalMapper_mapped(self, i):
stringAction = self.signalMapper.mapping(i).text()
filterColumn = self.logicalIndex
self.proxy.setFilter(stringAction, filterColumn)
font = QtGui.QFont()
font.setBold(True)
self.model.setFont(filterColumn, font)
【讨论】:
谢谢。是否可以标记/签署那些被过滤的列? @BehzadJamali 什么意思,解释清楚 比如我想让已经过滤的列名变成粗体。以上是关于PyQT5 和使用多列过滤表的主要内容,如果未能解决你的问题,请参考以下文章