Question

So I have a QTableView and I only want to let column sorting on column 1 but not column2.

Naturally I tried to installEventFilter on QHeaderView or QTableView, but MouseButtonPress event is not being passed unless you installEventFilter on QApplication

Now if when eventFilter is called, the target object is always the top level widget although event.pos() is actually relative to the header or tablecell depending on where you click.

So we cannot use QHeaderView.rect().contains(event.pos()) to find out if the user clicks on the header because you get false positive when you click on the top edge of the very first table cell.

You can still however calculate this using globalPos but then your eventFilter's logic has to change when you change layout or add more widgets above the tableview.

I believe it is a bug that event.pos() returns the relative pos even the object argument always refer to the same top level widget.

A more logical API would be that there is a event.target() method to return the target where it calculates the relative pos.

But I don't see a target() method or a way to find the target in this event filter.

Maybe I'm missing something?

# -*- coding: utf-8 -*-
# pyqt windows 4.10.3
# python 2.7.5 32 bits
from PyQt4.QtCore import *
from PyQt4.QtGui import *

app = None
tableHeader = None

class MyModel(QAbstractTableModel):
    def rowCount(self, QModelIndex_parent=None, *args, **kwargs):
        return 2

    def columnCount(self, QModelIndex_parent=None, *args, **kwargs):
        return 2

    def data(self, modelIndex, role=None):
        if modelIndex.isValid():
            row = modelIndex.row()
            col = modelIndex.column()
            if role == Qt.DisplayRole:
                return "%02d,%02d" % (row, col)

    def flags(self, index):
        if index.isValid():
            return Qt.ItemIsEnabled

    def headerData(self, section, Qt_Orientation, role=None):
        if role == Qt.DisplayRole and Qt_Orientation == Qt.Horizontal:
            return "Column " + str(section+1)

class MyEventFilter(QObject):
    def eventFilter(self, object, event):
        if event.type() == QEvent.MouseButtonPress:
            # object is always app/top level widget
            print 'MouseButtonPress target :' + repr(object)
            # even though event.pos() gives pos relative to the header when you click on header,
            # and pos relative to table cells when you click on table cell
            print repr(event.pos())
            # however we can get the mouse's global position
            print repr(event.globalPos())
            # given the top level widget's geometry
            print repr(app.activeWindow().geometry())
            # and the table header's left, top and height
            print repr(tableHeader.rect())
            # we can find out whether mouse click is targeted at the header
            print repr(event.globalPos().y() - app.activeWindow().geometry().y())
            # BUT WHAT IF THE LAYOUT CHANGE OR WE ADD MORE WIDGETS ABOVE THE TABLEVIEW?
            # WE HAVE TO ADJUST THE CALCULATION ABOVE!
        return False


if __name__ == "__main__":
    import sys
    app = QApplication(sys.argv)
    w = QMainWindow()
    t = QTableView()
    tableHeader = t.horizontalHeader()
    t.setModel(MyModel())
    w.setCentralWidget(t)
    ef = MyEventFilter()
    # installing in QMainWindow or QTableView won't catch MouseButtonPress
    # https://qt-project.org/forums/viewthread/9347
    #w.installEventFilter(ef)
    #t.installEventFilter(ef)
    app.installEventFilter(ef)
    w.show()
    sys.exit(app.exec_())
Was it helpful?

Solution

There's a much easier solution: reimplement the sort method of the model, and only permit sorting for the appropriate column.

Also, as an added refinement, use the sortIndicatorChanged signal of the header to restore the current sort indicator when appropriate.

Here's a demo script:

from PyQt4 import QtGui, QtCore

class TableModel(QtGui.QStandardItemModel):
    _sort_order = QtCore.Qt.AscendingOrder

    def sortOrder(self):
        return self._sort_order

    def sort(self, column, order):
        if column == 0:
            self._sort_order = order
            QtGui.QStandardItemModel.sort(self, column, order)

class Window(QtGui.QWidget):
    def __init__(self, rows, columns):
        QtGui.QWidget.__init__(self)
        self.table = QtGui.QTableView(self)
        model = TableModel(rows, columns, self.table)
        for row in range(rows):
            for column in range(columns):
                item = QtGui.QStandardItem('(%d, %d)' % (row, column))
                item.setTextAlignment(QtCore.Qt.AlignCenter)
                model.setItem(row, column, item)
        self.table.setModel(model)
        self.table.setSortingEnabled(True)
        self.table.horizontalHeader().sortIndicatorChanged.connect(
            self.handleSortIndicatorChanged)
        layout = QtGui.QVBoxLayout(self)
        layout.addWidget(self.table)

    def handleSortIndicatorChanged(self, index, order):
        if index != 0:
            self.table.horizontalHeader().setSortIndicator(
                0, self.table.model().sortOrder())

if __name__ == '__main__':

    import sys
    app = QtGui.QApplication(sys.argv)
    window = Window(5, 5)
    window.show()
    window.setGeometry(600, 300, 600, 250)
    sys.exit(app.exec_())
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top