2

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
eyllanesc
  • 235,170
  • 19
  • 170
  • 241
nenad
  • 164
  • 6
  • 17

1 Answers1

3

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()
eyllanesc
  • 235,170
  • 19
  • 170
  • 241
  • this is not complete MRE, it doesn't work when I add the rest of the program (class Widget(QWidget) and main), when I close the app immediately after I start the program, it hangs (doesn't close). @eyllanesc, can you please post a complete program that works. – nenad Sep 04 '20 at 11:54
  • @nenad 1) I have created a gist where my complete code is, I will not fill my answer with unnecessary code: https://gist.github.com/eyllanesc/3eaf841a8feadd38aca2f0d128f304b4 2) Creating a question that does not contribute anything is just noise, please avoid it. 3) Every time you accept an answer you must have tested it as it is annoying to have to pay attention to an old question if the OP pointed out that it worked a day before. – eyllanesc Sep 04 '20 at 19:06
  • my apology, I can delete the other question if it's ok. The problem is that the code from gist still hangs when I close the program immediately after I start it (and I didn't notice it in the beginning). Interestingly, if I click on a Filter button and then close the program it closes ok. – nenad Sep 04 '20 at 21:17
  • @nenad 1) what is your environment: Python version, PyQt5 version, OS, etc ?, 2) where are you executing the command? Try launching from the CMD and avoid using IDEs – eyllanesc Sep 04 '20 at 21:20
  • 1) Python 3.8.3, PyQt5==5.13.2, Win7 2) cmd.exe, command line. Please don't bother if it works for you (on your platform), because then I know that code is ok and I can try it and test it on other platforms. – nenad Sep 04 '20 at 21:34
  • It works when i put a method in Window class: def closeEvent(self, event): self.view.horizontalHeader().visible_editors = False – nenad Sep 04 '20 at 21:44