Regarding great answer by @ekhumoro and modification by @Oak_3260548 and @eyllanesc, I would like to first have a normal view without filters below the QHeaderView and a button to activate (show) the filters:
After I click the Filter button (which has property button.setCheckable(True)) I want to get:
Notice: the Filter button is pressed. Now I want the row with filters below the QHeaderView to disappear when I click again to Filter button (unpress it):
How can I do that, in other words how to get QHeaderView of the QTableView to be the old plain one? It would be very convenient to have that kind of feature in the app that user can toggle filter row below QHeaderView with button (or any other way) whenever he wants. The behavior of the app would be to cancel all filters in view when button is unpressed and filter row disappears, but that is not important for this question.
Edit: Here is MRE:
import sys
from PyQt5.QtCore import pyqtSignal, Qt
from PyQt5.QtGui import QStandardItemModel, QStandardItem
from PyQt5.QtWidgets import (
QHeaderView,
QWidget,
QLineEdit,
QApplication,
QTableView,
QVBoxLayout,
QHBoxLayout,
QComboBox,
QPushButton,
QCheckBox,
QMessageBox,
)
class Widget(QWidget):
def __init__(self, parent=None):
super().__init__(parent)
self.btn = QPushButton()
self.btn.setText("=")
self.btn.setFixedWidth(20)
self.linee = QLineEdit()
self.linee.setPlaceholderText("Filter")
lay = QHBoxLayout(self)
lay.setContentsMargins(0, 0, 0, 0)
lay.setSpacing(0)
lay.addWidget(self.btn)
lay.addWidget(self.linee)
class FilterHeader(QHeaderView):
filterActivated = pyqtSignal()
def __init__(self, parent):
super().__init__(Qt.Horizontal, parent)
self._editors = []
self._padding = 4
self.setStretchLastSection(True)
# self.setResizeMode(QHeaderView.Stretch)
self.setDefaultAlignment(Qt.AlignLeft | Qt.AlignVCenter)
self.setSortIndicatorShown(False)
self.sectionResized.connect(self.adjustPositions)
parent.horizontalScrollBar().valueChanged.connect(self.adjustPositions)
def setFilterBoxes(self, count):
while self._editors:
editor = self._editors.pop()
editor.deleteLater()
for index in range(count):
editor = self.create_editor(self.parent(), index)
self._editors.append(editor)
self.adjustPositions()
def create_editor(self, parent, index):
if index == 1: # Empty
editor = QWidget()
elif index == 2: # Number filter (>|=|<)
editor = Widget(parent)
editor.linee.returnPressed.connect(self.filterActivated)
editor.btn.clicked.connect(self.changebuttonsymbol)
elif index == 3:
editor = QComboBox(parent)
editor.addItems(["", "Combo", "One", "Two", "Three"])
editor.currentIndexChanged.connect(self.filterActivated)
elif index == 4:
editor = QPushButton(parent)
editor.clicked.connect(self.filterActivated)
editor.setText("Button")
elif index == 5:
editor = QCheckBox(parent)
editor.clicked.connect(self.filterActivated)
editor.setTristate(True)
editor.setCheckState(Qt.Checked)
editor.setText("CheckBox")
else:
editor = QLineEdit(parent)
editor.setPlaceholderText("Filter")
editor.returnPressed.connect(self.filterActivated)
return editor
def sizeHint(self):
size = super().sizeHint()
if self._editors:
height = self._editors[0].sizeHint().height()
size.setHeight(size.height() + height + self._padding)
return size
def updateGeometries(self):
if self._editors:
height = self._editors[0].sizeHint().height()
self.setViewportMargins(0, 0, 0, height + self._padding)
else:
self.setViewportMargins(0, 0, 0, 0)
super().updateGeometries()
self.adjustPositions()
def adjustPositions(self):
for index, editor in enumerate(self._editors):
if not isinstance(editor, QWidget):
continue
height = editor.sizeHint().height()
compensate_y = 0
compensate_x = 0
if type(editor) is QComboBox:
compensate_y = +2
elif type(editor) in (QWidget, Widget):
compensate_y = -1
elif type(editor) is QPushButton:
compensate_y = -1
elif type(editor) is QCheckBox:
compensate_y = 4
compensate_x = 4
editor.move(
self.sectionPosition(index) - self.offset() + 1 + compensate_x,
height + (self._padding // 2) + 2 + compensate_y,
)
editor.resize(self.sectionSize(index), height)
def filterText(self, index):
for editor in self._editors:
if hasattr(editor, "text") and callable(editor.text):
return editor.text()
return ""
def setFilterText(self, index, text):
for editor in self._editors:
if hasattr(editor, "setText") and callable(editor.setText):
editor.setText(text)
def clearFilters(self):
for editor in self._editors:
editor.clear()
def changebuttonsymbol(self):
nbtn = self.sender()
if nbtn.text() == "=":
nbtn.setText(">")
elif nbtn.text() == ">":
nbtn.setText("<")
else:
nbtn.setText("=")
class Window(QWidget):
def __init__(self):
super(Window, self).__init__()
self.filter_button = QPushButton("Filter")
self.filter_button.setCheckable(True)
self.filter_button.setChecked(True)
self.filter_button.clicked.connect(self.on_button_clicked)
self.view = QTableView()
self.view.horizontalHeader().setStretchLastSection(True)
button_layout = QHBoxLayout()
button_layout.addStretch()
button_layout.addWidget(self.filter_button)
layout = QVBoxLayout(self)
layout.addLayout(button_layout)
layout.addWidget(self.view)
header = FilterHeader(self.view)
self.view.setHorizontalHeader(header)
self.view.verticalHeader().setVisible(False)
#model = QStandardItemModel(self.view)
model = QStandardItemModel(5, 7, self.view)
for i in range(5):
for j in range(7):
item = QStandardItem(str(i+j))
model.setItem(i, j, item)
model.setHorizontalHeaderLabels("One Two Three Four Five Six Seven".split())
self.view.setModel(model)
header.setFilterBoxes(model.columnCount())
header.filterActivated.connect(self.handleFilterActivated)
def handleFilterActivated(self):
header = self.view.horizontalHeader()
print()
for index in range(header.count()):
if index != 4:
print(index, header.filterText(index))
else:
print("Button")
def on_button_clicked(self):
if self.filter_button.isChecked():
QMessageBox.information(None, "", "Now I want the row with filters below the QHeaderView to appear again.")
else:
QMessageBox.information(None, "", "Now I want the row with filters below the QHeaderView to disappear.")
if __name__ == "__main__":
app = QApplication(sys.argv)
window = Window()
#window.setGeometry(800, 100, 600, 300)
window.resize(950, 220)
window.show()
sys.exit(app.exec_())
EDIT: The accepted solution works fine in many cases but in some cases it freezes the GUI when the window is closed, so I found a workaround:
def closeEvent(self, event):
self.view.horizontalHeader().visible_editors = False
You have to create a property that handles the visibility of the editors:
class FilterHeader(QHeaderView):
filterActivated = pyqtSignal()
def __init__(self, parent):
super().__init__(Qt.Horizontal, parent)
self._visible_editors = False
self._editors = []
self._padding = 4
self.setStretchLastSection(True)
# self.setResizeMode(QHeaderView.Stretch)
self.setDefaultAlignment(Qt.AlignLeft | Qt.AlignVCenter)
self.setSortIndicatorShown(False)
self.sectionResized.connect(self.adjustPositions)
parent.horizontalScrollBar().valueChanged.connect(self.adjustPositions)
@property
def visible_editors(self):
return self._visible_editors
@visible_editors.setter
def visible_editors(self, is_visible):
self._visible_editors = is_visible
for editor in self._editors:
editor.setVisible(self.visible_editors)
self.updateGeometries()
def setFilterBoxes(self, count):
while self._editors:
editor = self._editors.pop()
editor.deleteLater()
for index in range(count):
editor = self.create_editor(self.parent(), index)
editor.setVisible(self.visible_editors)
self._editors.append(editor)
self.adjustPositions()
def create_editor(self, parent, index):
if index == 1: # Empty
editor = QWidget()
elif index == 2: # Number filter (>|=|<)
editor = Widget(parent)
editor.linee.returnPressed.connect(self.filterActivated)
editor.btn.clicked.connect(self.changebuttonsymbol)
elif index == 3:
editor = QComboBox(parent)
editor.addItems(["", "Combo", "One", "Two", "Three"])
editor.currentIndexChanged.connect(self.filterActivated)
elif index == 4:
editor = QPushButton(parent)
editor.clicked.connect(self.filterActivated)
editor.setText("Button")
elif index == 5:
editor = QCheckBox(parent)
editor.clicked.connect(self.filterActivated)
editor.setTristate(True)
editor.setCheckState(Qt.Checked)
editor.setText("CheckBox")
else:
editor = QLineEdit(parent)
editor.setPlaceholderText("Filter")
editor.returnPressed.connect(self.filterActivated)
return editor
def sizeHint(self):
size = super().sizeHint()
if self._editors and self.visible_editors:
height = self._editors[0].sizeHint().height()
size.setHeight(size.height() + height + self._padding)
return size
def updateGeometries(self):
if self._editors and self.visible_editors:
height = self._editors[0].sizeHint().height()
self.setViewportMargins(0, 0, 0, height + self._padding)
else:
self.setViewportMargins(0, 0, 0, 0)
super().updateGeometries()
self.adjustPositions()
def adjustPositions(self):
for index, editor in enumerate(self._editors):
if not isinstance(editor, QWidget):
continue
height = editor.sizeHint().height()
compensate_y = 0
compensate_x = 0
if type(editor) is QComboBox:
compensate_y = +2
elif type(editor) in (QWidget, Widget):
compensate_y = -1
elif type(editor) is QPushButton:
compensate_y = -1
elif type(editor) is QCheckBox:
compensate_y = 4
compensate_x = 4
editor.move(
self.sectionPosition(index) - self.offset() + 1 + compensate_x,
height + (self._padding // 2) + 2 + compensate_y,
)
editor.resize(self.sectionSize(index), height)
def filterText(self, index):
for editor in self._editors:
if hasattr(editor, "text") and callable(editor.text):
return editor.text()
return ""
def setFilterText(self, index, text):
for editor in self._editors:
if hasattr(editor, "setText") and callable(editor.setText):
editor.setText(text)
def clearFilters(self):
for editor in self._editors:
editor.clear()
def changebuttonsymbol(self):
nbtn = self.sender()
if nbtn.text() == "=":
nbtn.setText(">")
elif nbtn.text() == ">":
nbtn.setText("<")
else:
nbtn.setText("=")
class Window(QWidget):
def __init__(self):
super(Window, self).__init__()
self.filter_button = QPushButton("Filter")
self.filter_button.setCheckable(True)
self.filter_button.setChecked(True)
self.filter_button.clicked.connect(self.on_button_clicked)
self.view = QTableView()
self.view.horizontalHeader().setStretchLastSection(True)
button_layout = QHBoxLayout()
button_layout.addStretch()
button_layout.addWidget(self.filter_button)
layout = QVBoxLayout(self)
layout.addLayout(button_layout)
layout.addWidget(self.view)
header = FilterHeader(self.view)
self.view.setHorizontalHeader(header)
self.view.verticalHeader().setVisible(False)
# model = QStandardItemModel(self.view)
model = QStandardItemModel(5, 7, self.view)
for i in range(5):
for j in range(7):
item = QStandardItem(str(i + j))
model.setItem(i, j, item)
model.setHorizontalHeaderLabels("One Two Three Four Five Six Seven".split())
self.view.setModel(model)
header.setFilterBoxes(model.columnCount())
header.filterActivated.connect(self.handleFilterActivated)
self.on_button_clicked()
def handleFilterActivated(self):
header = self.view.horizontalHeader()
for index in range(header.count()):
if index != 4:
print(index, header.filterText(index))
else:
print("Button")
def on_button_clicked(self):
self.view.horizontalHeader().visible_editors = self.filter_button.isChecked()