Custom QStyledItemDelegate and QTableView



  • I'm trying to customize my QTableView to have the following:

    1. Hover event that spans entire row, when hovering over a cell.
    2. Selection event that paints the rect of the last cell in a selected row(any cells in a row can trigger this).

    I'm starting to read up on delegates and views. I'm still abit unclear on who, the delegate or view, is responsible to call the update. For example I figured I need to get the current hovered index and retrieve the current row from that. I'm just not sure if the view needs to tell the delegate to update all items in that row or does the delegate collect the items in the row when I'm testing the State_MouseOver. As for the model my assumptions so far is that the model is responsible just for the data.

    I've found a similar thread for #1 in the cpp forum:
    https://forum.qt.io/topic/12794/mousehover-entire-row-selection-in-qtableview
    I'm having a bit of trouble translating the working code to Python. It looks like they set a setMouseOver(), disableMouseOver(), and a mouseMoveEvent(). In the code the Delegate has access to the view to call the custom setMouseOver() and pass the row. How can I get a similar behavior? or is the a better way to achieve my two goals?

    Cheers

    from PySide2 import QtGui, QtCore, QtWidgets
    import sys
    
    class CustomTableDelegate(QtWidgets.QStyledItemDelegate):
        def __init__(self, parent=None):
            super(CustomTableDelegate, self).__init__(parent)
    
        def paint(self, painter, option, index):
            if option.state & QtWidgets.QStyle.State_MouseOver:
                painter.fillRect(option.rect, QtGui.QColor(143,143,143))
                QtWidgets.QStyledItemDelegate.paint(self, painter, option, index)
            else:
                painter.fillRect(option.rect, QtGui.QColor(38,38,38))
                QtWidgets.QStyledItemDelegate.paint(self, painter, option, index)
    
    
    class CustomTableView(QtWidgets.QTableView):
        def __init__(self, parent=None):
            super(CustomTableView, self).__init__(parent)
    
        def setMouseOver(self, row):
            # print(self.model.columnCount())
            print(row)
    
    if __name__ == '__main__':
        app = QtWidgets.QApplication(sys.argv)
        data = [["v001", "stuff", "fixing source", '2020-01-14', ''],
                ["v002", "more stuff", "currently broken", '2020-06-30', ''],
                ["v003", "too much stuff", "scaling issues", '2020-02-08', ''],
                ["v088", "still missing stuff", "not coment", '2020-11-13', ''],]
    
        table_view = CustomTableView()
        table_view.setMouseTracking(True)
        table_view.setItemDelegate(CustomTableDelegate())
        table_view.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows)
        table_view.setSelectionMode(QtWidgets.QAbstractItemView.SingleSelection)
    
        header = table_view.horizontalHeader()
        header.setStretchLastSection(True)
        vertical_header = table_view.verticalHeader()
        vertical_header.hide()
    
        model = QtGui.QStandardItemModel()
        model.setHorizontalHeaderLabels(['version', 'file', 'comment', 'date'])
        table_view.setModel(model)
    
    
        for r in range(len(data)):
            for c in range(4):
                model.setItem(r, c, QtGui.QStandardItem(data[r][c]))
    
        table_view.show()
        sys.exit(app.exec_())
    
    


  • @alom

    In the code the Delegate has access to the view to call the custom setMouseOver() and pass the row. How can I get a similar behavior?

    Just answering this bit. There is a lot of code/alternatives in the link you quote! But glancing through I see:

    TableView::TableView(QWidget *parent) : QTableView(parent), currHovered(-1)
    {
        Delegate *delegate = new Delegate;
        delegate->setView(this);
    

    So that is (one way) to have Delegate have access to the QTableView, which I think is what you asked in the quoted question.



  • @JonB
    Thanks, I should have mentioned that I was referencing the post from 26 Jan 2012, 03:17.

    So currently I'm assigning my custom delegate to the treeview

    table_view.setItemDelegate(CustomTableDelegate())
    

    My cpp is very minimal, but are they sub classing the tableview and creating a delegate instance inside of it? If so would I get rid of the setItemDelegate() on the treeview aswell?

    That was the only example I could find of someone trying something similar to my needs.



  • So I was able to get a "working" solution. If someone could take a look and see if this is the right idea that would really help me out. Qt sometimes give me a false sense that I've done something right then later realize I've completely butchered it :P

    I ended up struggling to find a way for the view to update the delegate and found the mouseMoveEvent to trigger a repaint. I passed the hovered row from the mouse to the delegate before the repaint.

    As for the paint() in the delegate, I've noticed in some examples uses the following:

    painter.save()
    ~some painter codes
    painter.restore()
    

    From the documentation: painter.save()
    "Saves the current painter state (pushes the state onto a stack)"
    That doesn't really help me under stand why this is needed, as if i comment it out, the code seems to still work. Can someone elaborate the use cases for the painter.save()/restore() please?

    from PySide2 import QtGui, QtCore, QtWidgets
    import sys
    
    class CustomTableDelegate(QtWidgets.QStyledItemDelegate):
        def __init__(self, parent=None):
            super(CustomTableDelegate, self).__init__(parent)
            self.__current_row = -1
    
        def paint(self, painter, option, index):
            painter.save()
            value = index.data(QtCore.Qt.DisplayRole)
            if self.__current_row  == index.row():
                if index.column() == 3:
                    painter.fillRect(option.rect, QtGui.QColor(143,143,143))
                    painter.setPen(QtGui.QColor(0,255,00))
                    painter.drawText(option.rect, QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter, value)
                else:
                    painter.fillRect(option.rect, QtGui.QColor(100,100,100))
                    painter.setPen(QtGui.QColor(0, 255, 255))
                    painter.drawText(option.rect, QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter, value)
            else:
                painter.fillRect(option.rect, QtGui.QColor(38, 38, 38))
                painter.setPen(QtGui.QColor(200, 200, 200))
                painter.drawText(option.rect, QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter, value)
            painter.restore()
    
        def sizeHint(self, *args, **kwargs):
            pass
    
        def set_current_row(self, row):
            self.__current_row = row
    
    
    
    class CustomTableView(QtWidgets.QTableView):
        def __init__(self, parent=None):
            super(CustomTableView, self).__init__(parent)
    
        def mouseMoveEvent(self, event):
            indx = self.indexAt(event.pos())
            self.itemDelegate().set_current_row(indx.row())
            self.viewport().repaint()
    
    
    if __name__ == '__main__':
        app = QtWidgets.QApplication(sys.argv)
        data = [["v001", "stuff", "fixing source", '2020-01-14', ''],
                ["v002", "more stuff", "currently broken", '2020-06-30', ''],
                ["v003", "too much stuff", "scaling issues", '2020-02-08', ''],
                ["v088", "still missing stuff", "not coment", '2020-11-13', ''],]
    
        table_view = CustomTableView()
        table_view.setShowGrid(False)
        table_view.setMouseTracking(True)
        table_view.setItemDelegate(CustomTableDelegate(table_view))
        table_view.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows)
        table_view.setSelectionMode(QtWidgets.QAbstractItemView.SingleSelection)
    
        header = table_view.horizontalHeader()
        header.setStretchLastSection(True)
        vertical_header = table_view.verticalHeader()
        vertical_header.hide()
    
        model = QtGui.QStandardItemModel()
        model.setHorizontalHeaderLabels(['version', 'file', 'comment', 'date'])
        table_view.setModel(model)
        for r in range(len(data)):
            for c in range(4):
                model.setItem(r, c, QtGui.QStandardItem(data[r][c]))
    
        table_view.show()
        sys.exit(app.exec_())
    

  • Lifetime Qt Champion

    Hi,

    The idea behind the painter save/restore combo is to keep said painter "clean" i.e. you return it in the same state as you received it. The fact that it seems to work the same way without it just means that there's no change that influence the next painting step.

    You could be using a dynamic chain of helper functions that modifies the painter, if you modify the transformation matrix in one of them and do not restore the painter before the next function, then this one will not paint the way you expect it.



  • @SGaist
    Thanks that definitely makes sense, I haven't done any complicated painting yet, just starting to read up on it now.

    Is using the mouseMoveEvent a good use to update the delegate in this case or is there a better way?
    Cheers


  • Lifetime Qt Champion

    Are you in fact moving the selection along the mouse move ?



  • the mouse move is meant to control just the highlighted text, I added some more code from above that tests the selection state to repaint the selection a bit differently.

    if option.state & QtWidgets.QStyle.State_Selected:
    # do selection repainting...
    

    I think it's working, at least visually it is :) i should print out the current selection just to be sure though.


  • Lifetime Qt Champion

    Since it's only meant for highlighting, another possibility would be to use QRubberBand to draw a rectangle on top of your QTableView.



  • interesting, I'll have a play with QRubberBand, Cheers!


Log in to reply