Important: Please read the Qt Code of Conduct - https://forum.qt.io/topic/113070/qt-code-of-conduct

Adding custom QHeaderView upon request



  • Hello Qt community,

    I used this example (as many others) to learn and elaborate on QHeaderVivew fitlers, subclassing and chopping the code.
    table.png
    I've been trying to resolve an issue with inability to update the QHeaderView for over two weeks, so I tried (my best) to produce a Minimal Usable Code:

    import sys
    
    from PyQt5.QtCore import pyqtSignal, Qt
    from PyQt5.QtGui import QStandardItemModel
    from PyQt5.QtWidgets import  QHeaderView, QWidget, QLineEdit, QApplication, QTableView, QVBoxLayout, QHBoxLayout, QPushButton
    
    
    class FilterHeader(QHeaderView):
        filterActivated = pyqtSignal()
    
        def __init__(self, parent):
            super().__init__(Qt.Horizontal, parent)
            self._editors = []
            self._padding = 4
            self.setStretchLastSection(True)
            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()
            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
                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.texts()
            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()
    
    
    class Window(QWidget):
        def __init__(self):
            super(Window, self).__init__()
            
            self.panel = QWidget()
            self.panel.setFixedHeight(42)
            
            btn = QPushButton()
            btn.setText("Set table headers")
            btn.clicked.connect(self.SetThemUp)
            lay = QHBoxLayout()
            lay.addWidget(btn)
            
            self.panel.setLayout(lay)
            
            self.view = QTableView()
            layout = QVBoxLayout(self)
            layout.addWidget(self.panel)
            layout.addWidget(self.view)
            header = FilterHeader(self.view)
            print("header instance (init) = " + str(header))
            self.view.setHorizontalHeader(header)
            model = QStandardItemModel(self.view)
            model.setHorizontalHeaderLabels("One Two Three Four Five Six Seven".split())
            self.view.setModel(model)
            self.SetThemUp()
            header.filterActivated.connect(self.handleFilterActivated)
    
        def handleFilterActivated(self):   
                print("handleFilterActivated!")
    
        def SetThemUp(self):
            print("setThemUp........")
            header = self.view.horizontalHeader()
            print("header instance (SetThemUp) = " + str(header))
            header.setFilterBoxes(6)
    
    if __name__ == "__main__":
        app = QApplication(sys.argv)
        window = Window()
        window.setGeometry(800, 100, 600, 300)
        window.show()
        sys.exit(app.exec_())
    

    If you fire the code as-is, it will show a QTableView with filter headers on all columns but 2nd. The desired behaviour is to comment out line No. 109 (self.SetThemUp()) and invoke the headers by clicking on the btn1, that in turns runs self.SetThemUp().

    The instances seem to be same - in the console I printed and can see identical memory addresses from the initiation, calling by default __init__ method and from self.SetThemUp() method. However, in my application it would only show empty header. In this MUC it doesn't work at all. In either case I don't get any errors or warnings.

    I suspect it's the inheritance and instances of the classes that needs to be tweaked, but I'm not experienced enough to see through this... I hope someone could point me into right direction? Or could someone advice how can I further debug it?

    EDIT:
    I still have no solution to this problem, the filter widgets don't get drawn when called from the button click, while they're drawn fine when the same function is called from __init__. I broke down every piece of code and made sure that:

    • I get everywhere same instances of objects, nothing is messes between the two ways of running setThemUp()
    • filter widgets have proper sizes and positions
    • filter objects are not hidden behind the QTableView (checked by hiding QTableView)
    • I added numerous logging prints in the functions (create_editor(), adjustPosition(), updateGeometries(), atd.), they behave absolutely identically.


  • Hundreds of mods and testing cycles later, I found a solution:

        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)
                editor.show()     # <<<<<<<<<<<<<<<<<<<<<<<<<
            self.adjustPositions()
    

    To my best undestanding, Ekhumoro is drawing the filter widgets directly to the window, since there are no layouts to add widgets (or widgets to add layouts to add widgets) within QHeaderView.

    When they are created from the __init__ function, they got displayed by the final window.show(). But when they are added later on demand by button click, they stay hidden. Adding editor.show() into the function displaying the filter widgets resolves the issue.


Log in to reply