5

I would like to display widgets between the QHeaderView and the rest of the QTableView, like in the example picture below (created with Photoshop), as this seems like a natural way to enable input for filtering columns.


Does anybody have any ideas of how to inject widgets inbetween?


enter image description here

ekhumoro
  • 115,249
  • 20
  • 229
  • 336
timmwagener
  • 2,368
  • 2
  • 19
  • 27

2 Answers2

9

Below is a demo of a FilterHeader class that I wrote for one of my own projects. You will probably need to adapt it to suit your own needs, but it should already do most what you want. The padding around the filter boxes is unlikely to work the same on all platforms, so you may need to tweak the code in the adjustPositions method.

screenshot

import sys
from PyQt4 import QtCore, QtGui

class FilterHeader(QtGui.QHeaderView):
    filterActivated = QtCore.pyqtSignal()

    def __init__(self, parent):
        super().__init__(QtCore.Qt.Horizontal, parent)
        self._editors = []
        self._padding = 4
        self.setStretchLastSection(True)
        self.setResizeMode(QtGui.QHeaderView.Stretch)
        self.setDefaultAlignment(
            QtCore.Qt.AlignLeft | QtCore.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 = QtGui.QLineEdit(self.parent())
            editor.setPlaceholderText('Filter')
            editor.returnPressed.connect(self.filterActivated.emit)
            self._editors.append(editor)
        self.adjustPositions()

    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):
            height = editor.sizeHint().height()
            editor.move(
                self.sectionPosition(index) - self.offset() + 2,
                height + (self._padding // 2))
            editor.resize(self.sectionSize(index), height)

    def filterText(self, index):
        if 0 <= index < len(self._editors):
            return self._editors[index].text()
        return ''

    def setFilterText(self, index, text):
        if 0 <= index < len(self._editors):
            self._editors[index].setText(text)

    def clearFilters(self):
        for editor in self._editors:
            editor.clear()


class Window(QtGui.QWidget):
    def __init__(self):
        super(Window, self).__init__()
        self.view = QtGui.QTableView()
        layout = QtGui.QVBoxLayout(self)
        layout.addWidget(self.view)
        header = FilterHeader(self.view)
        self.view.setHorizontalHeader(header)
        model = QtGui.QStandardItemModel(self.view)
        model.setHorizontalHeaderLabels('One Two Three Four Five'.split())
        self.view.setModel(model)
        header.setFilterBoxes(model.columnCount())
        header.filterActivated.connect(self.handleFilterActivated)

    def handleFilterActivated(self):
        header = self.view.horizontalHeader()
        for index in range(header.count()):
            print((index, header.filterText(index)))


if __name__ == '__main__':

    app = QtGui.QApplication(sys.argv)
    window = Window()
    window.setGeometry(600, 100, 600, 300)
    window.show()
    sys.exit(app.exec_())
ekhumoro
  • 115,249
  • 20
  • 229
  • 336
  • I tested it (with PySide) and it works and is a great start. However, I noticed some _minor_ display annoyances with hoverEvents _(basically simply that the focus vanishes/slightly flickers without an apparent reason when the mouse is over the QLineEdit)_. So I logged the event delivery to the QLineEdits and it seems all events are caught by the FilterView. (Which probably silently distributes it down to the QLineEdits as typing etc. still works). However, it makes me wonder if this is related to the sizeHint() of FilterView, spanning the area of the QLineEdits and if this could cause issues? – timmwagener Jun 07 '17 at 14:32
  • @timmwagener. It all works perfectly fine for me with both PyQt4 and PySide (on Linux). I think the problem must be at your end. There is nothing in the code I posted that would cause the issue you describe. – ekhumoro Jun 07 '17 at 18:11
  • Ok, the issue is happening on Windows 7 with Aero desktop turned on (default). The line edits in the header view are displaying a different highlighting behaviour than line edits added to a normal layout. [Here is a slight mod. of your example to mock it up](https://gist.github.com/timmwagener/58449d508b028f714279c2b4647b60e7). When no Aero desktop is on, there is no highlighting of line edits and so there is no difference visible. My concern is *less* about just visual noise _(although I couldn't use it like that)_, and more about that possibly hinting at some update/paintEvent order issue!? – timmwagener Jun 07 '17 at 19:36
  • @timmwagener. I'm afraid I don't have much to add, since I cannot test on Windows. Maybe try checking how often `updateGeometries` is being called. You seem to have identified a possible Qt bug in the Aero style-plugin. But you need to pare down the example a lot more so that you can isolate the real cause. For a start, I would want to see if it's possible to reproduce the problem outside of a header-view. – ekhumoro Jun 07 '17 at 20:07
  • I could fix that highlighting issue by parenting the LineEdits under the TableView not the HeaderView. They now get all the updating from the signals and are placed correctly but display the same highlighting behaviour as usual. I suspect it had something to do with `updateGeometries()` triggering events/updates on all children of the `HeaderView` causing the line edits loose focus or so. But just guessing here. – timmwagener Jun 07 '17 at 21:28
  • 1
    @timmwagener. I've adjusted my demo code according to your suggestion as it does not affect the way it works on linux, and should therefore provide a more generic answer. The problem is certainly caused by a bug in the Aero style-plugin, since focus behaviour should not arbitrarily depend on which parent widget is used. – ekhumoro Jun 08 '17 at 15:38
  • Did you ever try to set `view.setSortingEnabled(True)` on the table view that holds the new header view instance? I'm experiencing an issue, that the view doesn't want to sort anymore. As if the signal from the new header view is not connected to the sorting changed slot of the view after calling `view.setHorizontalHeader(header)`. This also seems not to be specific to `FilterHeader` but happens on just setting new vanilla instances of `QtGui.QHeaderView` too. The sort indicator is shown, but no sorting happens whatsoever. If no new header view is set, sorting works as expected. – timmwagener Jun 10 '17 at 12:31
  • I posted the sorting issue, with specific demo code, as a new question [here](https://stackoverflow.com/questions/44474135/qtableview-sorting-fails-after-sethorizontalheader) – timmwagener Jun 10 '17 at 13:59
1

Here is @ekhumoro's answer ported to PyQt5 and with a little change - added a combobox on 4th column.

enter image description here

    import sys
    from PyQt5 import QtCore, QtGui
    from PyQt5.QtWidgets import QHeaderView, QWidget, QLineEdit, QApplication, QTableView, QVBoxLayout, QLineEdit, QComboBox
    from PyQt5.QtCore import pyqtSignal
    
    class FilterHeader(QHeaderView):
        filterActivated = QtCore.pyqtSignal()
    
        def __init__(self, parent):
            super().__init__(QtCore.Qt.Horizontal, parent)
            self._editors = []
            self._padding = 4
            self.setStretchLastSection(True)
            #self.setResizeMode(QHeaderView.Stretch)
            self.setDefaultAlignment(QtCore.Qt.AlignLeft | QtCore.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):
                if index == 3:
                    editor = QComboBox(self.parent())
                    #editor.returnPressed.connect(self.filterActivated.emit)
                    editor.addItems(["One","Two"])
                else:
                    editor = QLineEdit(self.parent())
                    editor.setPlaceholderText('Filter')
                    editor.returnPressed.connect(self.filterActivated.emit)                
                self._editors.append(editor)
            self.adjustPositions()
    
        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):
                height = editor.sizeHint().height()
                editor.move( self.sectionPosition(index) - self.offset() + 2, height + (self._padding // 2))
                editor.resize(self.sectionSize(index), height)
    
        def filterText(self, index):
            if 0 <= index < len(self._editors):
                return self._editors[index].text()
            return ''
    
        def setFilterText(self, index, text):
            if 0 <= index < len(self._editors):
                self._editors[index].setText(text)
    
        def clearFilters(self):
            for editor in self._editors:
                editor.clear()
    
    
    class Window(QWidget):
        def __init__(self):
            super(Window, self).__init__()
            self.view = QTableView()
            layout = QVBoxLayout(self)
            layout.addWidget(self.view)
            header = FilterHeader(self.view)
            self.view.setHorizontalHeader(header)
            model = QtGui.QStandardItemModel(self.view)
            model.setHorizontalHeaderLabels('One Two Three Four Five'.split())
            self.view.setModel(model)
            header.setFilterBoxes(model.columnCount())
            header.filterActivated.connect(self.handleFilterActivated)
    
        def handleFilterActivated(self):
            header = self.view.horizontalHeader()
            for index in range(header.count()):
                print((index, header.filterText(index)))
    
    
    if __name__ == '__main__':
    
        app = QApplication(sys.argv)
        window = Window()
        window.setGeometry(600, 100, 600, 300)
        window.show()
        sys.exit(app.exec_())
Oak_3260548
  • 1,882
  • 3
  • 23
  • 41