#!/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 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_())