向自定义QSortFilterProxyModel添加复选框列

问题描述 投票:1回答:1

我有一个代码来代表表格中的熊猫数据框,并提供了如下所示的过滤功能:

enter image description here

我想在第一列之前插入一个复选框列,以便用户可以选择行。该代码使用PandasModel(QtCore.QAbstractTableModel)模型读取熊猫数据,并使用CustomProxyModel(QtCore.QSortFilterProxyModel)QTableView添加过滤功能。

我的问题是是否需要添加其他复选框列,是否应该将其添加到PandasModelCustomProxyModel或其他位置?作为一种替代解决方案,我发现了一个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_())
python pyqt pyqt5 qsortfilterproxymodel
1个回答
3
投票
CustomProxyModel不是一个选项,因为显示的数字和行不同,它不允许您正确映射复选框。一种可能的解决方案是实现实现该功能的代理(可能基于QAbstractProxyModel或QIdentityProxyModel),但在我的回答中,我不会提出该解决方案,因为它会使逻辑复杂化,相反,我将在源模型中实现逻辑,因此我创建了从PandasModel继承的类,其中添加了复选框的列。

逻辑是重写数据,setData,标志和headarData方法,以便该列中大于复选框列的信息取自基类,但减少了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_())

enter image description here
© www.soinside.com 2019 - 2024. All rights reserved.