Search code examples
pythonpyqtpyqt5qheaderview

Create a DB Browser style QHeaderview in PyQt5


I would like to create a QHeaderView in PyQt5 like the one that is used in this project (DB Browser for SQLite). The problem is that I don't know C++ and I don't understand how the classes and widgets are created from the source code that is on GitHub. Does anybody how to emulate this filter header in python?

https://github.com/sqlitebrowser/sqlitebrowser/blob/master/src/FilterTableHeader.h


Solution

  • This is a very basic implementation of that approach.

    The concept is to provide a correct sizeHint for the header (the default hint of the header plus the height of the line edit) and use setViewportMargins to ensure that the header has a bottom margin which is used by the text fields.

    class Header(QtWidgets.QHeaderView):
        filterChanged = QtCore.pyqtSignal(int, str)
        fieldsVisible = True
    
        def __init__(self, *args, **kwargs):
            super().__init__(*args, **kwargs)
            self.fields = []
            self.sectionCountChanged.connect(self.generateFields)
            self.sectionResized.connect(self.updateFields)
            self.parent().horizontalScrollBar().valueChanged.connect(self.updateFields)
            self.parent().verticalScrollBar().valueChanged.connect(self.updateFields)
    
        def setFieldsVisible(self, visible):
            if visible == self.fieldsVisible:
                return
            self.fieldsVisible = visible
            self.generateFields()
    
        def generateFields(self):
            while self.fields:
                self.fields.pop().deleteLater()
            if self.fieldsVisible:
                for s in range(self.count()):
                    edit = QtWidgets.QLineEdit(self)
                    edit.show()
                    self.fields.append(edit)
                    edit.textChanged.connect(lambda text, s=s: self.filterChanged.emit(s, text))
                self.updateFields()
            self.updateGeometries()
    
        def updateFields(self):
            offset = self.offset()
            y = QtWidgets.QHeaderView.sizeHint(self).height()
            for section, field in enumerate(self.fields):
                field.move(self.sectionPosition(section) - offset, y)
                field.resize(self.sectionSize(section), field.sizeHint().height())
    
        def updateGeometries(self):
            if self.fields:
                self.setViewportMargins(0, 0, 0, self.fields[0].sizeHint().height())
            else:
                self.setViewportMargins(0, 0, 0, 0)
            super().updateGeometries()
            # ensure that the parent view updates correctly
            self.parent().updateGeometries()
            self.updateFields()
    
        def sizeHint(self):
            hint = super().sizeHint()
            if self.fields:
                hint.setHeight(hint.height() + self.fields[0].sizeHint().height())
            return hint
    
    
    class SearchTable(QtWidgets.QTableView):
        def __init__(self):
            super().__init__()
            model = QtGui.QStandardItemModel()
            for row in range(8):
                model.appendRow([QtGui.QStandardItem('Item {} {}'.format(row, i)) for i in range(8)])
            self.setModel(model)
            self.setHorizontalHeader(Header(QtCore.Qt.Horizontal, self))
    

    Note that this example does not take into account the possibility of moving sections, and if you want to implement a multi-column filter you'll need a subclass of QSortFilterProxyModel (for example, the one provided here). I provided a basic signal (filterChanged) which will return the section and the text changed.