将复选框列添加到自定义 QSortFilterProxyModel
adding a checkbox-column to a custom QSortFilterProxyModel
我有一个代码来表示 table 中的 pandas 数据帧,并提供过滤功能,如下所示:
我想在第一列之前插入一个复选框列,以便用户可以 select 行。
该代码使用 PandasModel(QtCore.QAbstractTableModel)
模型读取 pandas 数据,并使用 CustomProxyModel(QtCore.QSortFilterProxyModel)
为 QTableView
.
添加过滤功能
我的问题是,如果我需要添加额外的复选框列,我应该将其添加到PandasModel
还是CustomProxyModel
或其他地方?作为替代解决方案,我发现 SO solution 建议使用自定义 CheckBoxDelegate
(我在下面的代码中添加了它)但它似乎占据了第一列而不是插入新列。也不允许点击。
#!/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()
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())
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 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 CheckBoxDelegate(QtWidgets.QItemDelegate):
"""
A delegate that places a fully functioning QCheckBox cell of the column to which it's applied.
"""
def __init__(self, parent):
QtWidgets.QItemDelegate.__init__(self, parent)
def createEditor(self, parent, option, index):
"""
Important, otherwise an editor is created if the user clicks in this cell.
"""
return None
def paint(self, painter, option, index):
"""
Paint a checkbox without the label.
"""
self.drawCheck(painter, option, option.rect, QtCore.Qt.Unchecked if int(index.data()) == 0 else QtCore.Qt.Checked)
def editorEvent(self, event, model, option, index):
'''
Change the data in the model and the state of the checkbox
if the user presses the left mousebutton and this cell is editable. Otherwise do nothing.
'''
if not int(index.flags() & QtCore.Qt.ItemIsEditable) > 0:
return False
if event.type() == QtCore.QEvent.MouseButtonRelease and event.button() == QtCore.Qt.LeftButton:
# Change the checkbox-state
self.setModelData(None, model, index)
return True
return False
def setModelData (self, editor, model, index):
'''
The user wanted to change the old state in the opposite.
'''
model.setData(index, 1 if int(index.data()) == 0 else 0, QtCore.Qt.EditRole)
class TableView(QtWidgets.QTableView):
"""
A simple table to demonstrate the QComboBox delegate.
"""
def __init__(self, *args, **kwargs):
QtWidgets.QTableView.__init__(self, *args, **kwargs)
self.setItemDelegateForColumn(0, CheckBoxDelegate(self))
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 = TableView(self)
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 = CustomProxyModel(self)
self.proxy.setSourceModel(self.model)
self.view.setModel(self.proxy)
self.view.resizeColumnsToContents()
# delegate = CheckBoxDelegate(None)
# self.view.setItemDelegateForColumn(0, delegate)
@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
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)
@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)
if __name__ == "__main__":
import sys
app = QtWidgets.QApplication(sys.argv)
main = myWindow()
main.show()
main.resize(2000, 800)
sys.exit(app.exec_())
CustomProxyModel 不是一个选项,因为它不允许您正确映射复选框,因为显示的数字和行不同。一种可能的解决方案是实现一个代理(可能基于 QAbstractProxyModel 或 QIdentityProxyModel)来实现该功能,但在我的回答中,我不会提出该解决方案,因为它会使逻辑复杂化,相反,我将在源模型中实现逻辑,因此我创建了class 继承自 PandasModel,其中添加了复选框列。
逻辑是覆盖数据、setData、flags 和 headarData 方法,以便列中大于复选框列的信息取自基数 class 但减少 1。
综合以上,解决方案如下:
#!/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()
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())
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 CheckablePandasModel(PandasModel):
def __init__(self, df=pd.DataFrame(), parent=None):
super().__init__(df, parent)
self.checkable_values = set()
self._checkable_column = -1
@property
def checkable_column(self):
return self._checkable_column
@checkable_column.setter
def checkable_column(self, column):
if self.checkable_column == column:
return
last_column = self.checkable_column
self._checkable_column = column
if last_column == -1:
self.beginInsertColumns(
QtCore.QModelIndex(), self.checkable_column, self.checkable_column
)
self.endInsertColumns()
elif self.checkable_column == -1:
self.beginRemoveColumns(QtCore.QModelIndex(), last_column, last_column)
self.endRemoveColumns()
for c in (last_column, column):
if c > 0:
self.dataChanged.emit(
self.index(0, c), self.index(self.columnCount() - 1, c)
)
def columnCount(self, parent=QtCore.QModelIndex()):
return super().columnCount(parent) + (1 if self.checkable_column != -1 else 0)
def data(self, index, role=QtCore.Qt.DisplayRole):
if self.checkable_column != -1:
row, col = index.row(), index.column()
if col == self.checkable_column:
if role == QtCore.Qt.CheckStateRole:
return (
QtCore.Qt.Checked
if row in self.checkable_values
else QtCore.Qt.Unchecked
)
return QtCore.QVariant()
if col > self.checkable_column:
index = index.sibling(index.row(), col - 1)
return super().data(index, role)
def setData(self, index, value, role):
if self.checkable_column != -1:
row, col = index.row(), index.column()
if col == self.checkable_column:
if role == QtCore.Qt.CheckStateRole:
if row in self.checkable_values:
self.checkable_values.discard(row)
else:
self.checkable_values.add(row)
self.dataChanged.emit(index, index, (role,))
return True
return False
if col > self.checkable_column:
index = index.sibling(index.row(), col - 1)
return super().setData(index, value, role)
def headerData(self, section, orientation, role=QtCore.Qt.DisplayRole):
if self.checkable_column != -1:
if section == self.checkable_column and orientation == QtCore.Qt.Horizontal:
return QtCore.QVariant()
if section > self.checkable_column and orientation == QtCore.Qt.Horizontal:
section -= 1
return super().headerData(section, orientation, role)
def flags(self, index):
if self.checkable_column != -1:
col = index.column()
if col == self.checkable_column:
return QtCore.Qt.ItemIsUserCheckable | QtCore.Qt.ItemIsEnabled
if col > self.checkable_column:
index = index.sibling(index.row(), col - 1)
return super().flags(index)
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 __init__(self, parent=None):
super(myWindow, self).__init__(parent)
self.centralwidget = QtWidgets.QWidget()
self.lineEdit = QtWidgets.QLineEdit()
self.view = QtWidgets.QTableView()
self.comboBox = QtWidgets.QComboBox()
self.label = QtWidgets.QLabel()
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.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 = CheckablePandasModel(df)
self.model.checkable_column = 0
self.proxy = CustomProxyModel(self)
self.proxy.setSourceModel(self.model)
self.view.setModel(self.proxy)
self.view.resizeColumnsToContents()
@QtCore.pyqtSlot(int)
def on_view_horizontalHeader_sectionClicked(self, logicalIndex):
if logicalIndex == self.model.checkable_column:
return
self.menuValues = QtWidgets.QMenu(self)
self.comboBox.blockSignals(True)
self.comboBox.setCurrentIndex(
logicalIndex - 1
if logicalIndex > self.model.checkable_column
else logicalIndex
)
self.comboBox.blockSignals(True)
valuesUnique = set(
self.proxy.index(i, logicalIndex).data()
for i in range(self.proxy.rowCount())
)
actionAll = QtWidgets.QAction("All", self)
self.menuValues.addAction(actionAll)
self.menuValues.addSeparator()
for i, name in enumerate(valuesUnique):
action = QtWidgets.QAction(name, self)
action.setData(i)
self.menuValues.addAction(action)
headerPos = self.view.mapToGlobal(self.horizontalHeader.pos())
pos = headerPos + QtCore.QPoint(
self.horizontalHeader.sectionPosition(logicalIndex),
self.horizontalHeader.height(),
)
action = self.menuValues.exec_(pos)
if action is not None:
font = QtGui.QFont()
if action.data() is None: # all
self.proxy.setFilter("", logicalIndex)
else:
font.setBold(True)
self.proxy.setFilter(action.text(), logicalIndex)
self.model.setFont(logicalIndex - 1, font)
@QtCore.pyqtSlot(str)
def on_lineEdit_textChanged(self, text):
self.proxy.setFilter(text, self.comboBox.currentIndex() + 1)
if __name__ == "__main__":
import sys
app = QtWidgets.QApplication(sys.argv)
main = myWindow()
main.show()
main.resize(2000, 800)
sys.exit(app.exec_())
我有一个代码来表示 table 中的 pandas 数据帧,并提供过滤功能,如下所示:
我想在第一列之前插入一个复选框列,以便用户可以 select 行。
该代码使用 PandasModel(QtCore.QAbstractTableModel)
模型读取 pandas 数据,并使用 CustomProxyModel(QtCore.QSortFilterProxyModel)
为 QTableView
.
我的问题是,如果我需要添加额外的复选框列,我应该将其添加到PandasModel
还是CustomProxyModel
或其他地方?作为替代解决方案,我发现 SO solution 建议使用自定义 CheckBoxDelegate
(我在下面的代码中添加了它)但它似乎占据了第一列而不是插入新列。也不允许点击。
#!/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()
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())
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 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 CheckBoxDelegate(QtWidgets.QItemDelegate):
"""
A delegate that places a fully functioning QCheckBox cell of the column to which it's applied.
"""
def __init__(self, parent):
QtWidgets.QItemDelegate.__init__(self, parent)
def createEditor(self, parent, option, index):
"""
Important, otherwise an editor is created if the user clicks in this cell.
"""
return None
def paint(self, painter, option, index):
"""
Paint a checkbox without the label.
"""
self.drawCheck(painter, option, option.rect, QtCore.Qt.Unchecked if int(index.data()) == 0 else QtCore.Qt.Checked)
def editorEvent(self, event, model, option, index):
'''
Change the data in the model and the state of the checkbox
if the user presses the left mousebutton and this cell is editable. Otherwise do nothing.
'''
if not int(index.flags() & QtCore.Qt.ItemIsEditable) > 0:
return False
if event.type() == QtCore.QEvent.MouseButtonRelease and event.button() == QtCore.Qt.LeftButton:
# Change the checkbox-state
self.setModelData(None, model, index)
return True
return False
def setModelData (self, editor, model, index):
'''
The user wanted to change the old state in the opposite.
'''
model.setData(index, 1 if int(index.data()) == 0 else 0, QtCore.Qt.EditRole)
class TableView(QtWidgets.QTableView):
"""
A simple table to demonstrate the QComboBox delegate.
"""
def __init__(self, *args, **kwargs):
QtWidgets.QTableView.__init__(self, *args, **kwargs)
self.setItemDelegateForColumn(0, CheckBoxDelegate(self))
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 = TableView(self)
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 = CustomProxyModel(self)
self.proxy.setSourceModel(self.model)
self.view.setModel(self.proxy)
self.view.resizeColumnsToContents()
# delegate = CheckBoxDelegate(None)
# self.view.setItemDelegateForColumn(0, delegate)
@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
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)
@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)
if __name__ == "__main__":
import sys
app = QtWidgets.QApplication(sys.argv)
main = myWindow()
main.show()
main.resize(2000, 800)
sys.exit(app.exec_())
CustomProxyModel 不是一个选项,因为它不允许您正确映射复选框,因为显示的数字和行不同。一种可能的解决方案是实现一个代理(可能基于 QAbstractProxyModel 或 QIdentityProxyModel)来实现该功能,但在我的回答中,我不会提出该解决方案,因为它会使逻辑复杂化,相反,我将在源模型中实现逻辑,因此我创建了class 继承自 PandasModel,其中添加了复选框列。
逻辑是覆盖数据、setData、flags 和 headarData 方法,以便列中大于复选框列的信息取自基数 class 但减少 1。
综合以上,解决方案如下:
#!/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()
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())
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 CheckablePandasModel(PandasModel):
def __init__(self, df=pd.DataFrame(), parent=None):
super().__init__(df, parent)
self.checkable_values = set()
self._checkable_column = -1
@property
def checkable_column(self):
return self._checkable_column
@checkable_column.setter
def checkable_column(self, column):
if self.checkable_column == column:
return
last_column = self.checkable_column
self._checkable_column = column
if last_column == -1:
self.beginInsertColumns(
QtCore.QModelIndex(), self.checkable_column, self.checkable_column
)
self.endInsertColumns()
elif self.checkable_column == -1:
self.beginRemoveColumns(QtCore.QModelIndex(), last_column, last_column)
self.endRemoveColumns()
for c in (last_column, column):
if c > 0:
self.dataChanged.emit(
self.index(0, c), self.index(self.columnCount() - 1, c)
)
def columnCount(self, parent=QtCore.QModelIndex()):
return super().columnCount(parent) + (1 if self.checkable_column != -1 else 0)
def data(self, index, role=QtCore.Qt.DisplayRole):
if self.checkable_column != -1:
row, col = index.row(), index.column()
if col == self.checkable_column:
if role == QtCore.Qt.CheckStateRole:
return (
QtCore.Qt.Checked
if row in self.checkable_values
else QtCore.Qt.Unchecked
)
return QtCore.QVariant()
if col > self.checkable_column:
index = index.sibling(index.row(), col - 1)
return super().data(index, role)
def setData(self, index, value, role):
if self.checkable_column != -1:
row, col = index.row(), index.column()
if col == self.checkable_column:
if role == QtCore.Qt.CheckStateRole:
if row in self.checkable_values:
self.checkable_values.discard(row)
else:
self.checkable_values.add(row)
self.dataChanged.emit(index, index, (role,))
return True
return False
if col > self.checkable_column:
index = index.sibling(index.row(), col - 1)
return super().setData(index, value, role)
def headerData(self, section, orientation, role=QtCore.Qt.DisplayRole):
if self.checkable_column != -1:
if section == self.checkable_column and orientation == QtCore.Qt.Horizontal:
return QtCore.QVariant()
if section > self.checkable_column and orientation == QtCore.Qt.Horizontal:
section -= 1
return super().headerData(section, orientation, role)
def flags(self, index):
if self.checkable_column != -1:
col = index.column()
if col == self.checkable_column:
return QtCore.Qt.ItemIsUserCheckable | QtCore.Qt.ItemIsEnabled
if col > self.checkable_column:
index = index.sibling(index.row(), col - 1)
return super().flags(index)
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 __init__(self, parent=None):
super(myWindow, self).__init__(parent)
self.centralwidget = QtWidgets.QWidget()
self.lineEdit = QtWidgets.QLineEdit()
self.view = QtWidgets.QTableView()
self.comboBox = QtWidgets.QComboBox()
self.label = QtWidgets.QLabel()
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.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 = CheckablePandasModel(df)
self.model.checkable_column = 0
self.proxy = CustomProxyModel(self)
self.proxy.setSourceModel(self.model)
self.view.setModel(self.proxy)
self.view.resizeColumnsToContents()
@QtCore.pyqtSlot(int)
def on_view_horizontalHeader_sectionClicked(self, logicalIndex):
if logicalIndex == self.model.checkable_column:
return
self.menuValues = QtWidgets.QMenu(self)
self.comboBox.blockSignals(True)
self.comboBox.setCurrentIndex(
logicalIndex - 1
if logicalIndex > self.model.checkable_column
else logicalIndex
)
self.comboBox.blockSignals(True)
valuesUnique = set(
self.proxy.index(i, logicalIndex).data()
for i in range(self.proxy.rowCount())
)
actionAll = QtWidgets.QAction("All", self)
self.menuValues.addAction(actionAll)
self.menuValues.addSeparator()
for i, name in enumerate(valuesUnique):
action = QtWidgets.QAction(name, self)
action.setData(i)
self.menuValues.addAction(action)
headerPos = self.view.mapToGlobal(self.horizontalHeader.pos())
pos = headerPos + QtCore.QPoint(
self.horizontalHeader.sectionPosition(logicalIndex),
self.horizontalHeader.height(),
)
action = self.menuValues.exec_(pos)
if action is not None:
font = QtGui.QFont()
if action.data() is None: # all
self.proxy.setFilter("", logicalIndex)
else:
font.setBold(True)
self.proxy.setFilter(action.text(), logicalIndex)
self.model.setFont(logicalIndex - 1, font)
@QtCore.pyqtSlot(str)
def on_lineEdit_textChanged(self, text):
self.proxy.setFilter(text, self.comboBox.currentIndex() + 1)
if __name__ == "__main__":
import sys
app = QtWidgets.QApplication(sys.argv)
main = myWindow()
main.show()
main.resize(2000, 800)
sys.exit(app.exec_())