Search code examples
pythonpyqtpyqt5qtableviewqheaderview

How to enable and disable QHeaderView filters with button


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:

enter image description here

After I click the Filter button (which has property button.setCheckable(True)) I want to get:

enter image description here

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):

enter image description here

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

Solution

  • 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()