Search code examples
pyqtpyqt5qtableview

Value based gridlines / divider in QTableView


I have a QTableView, which I feed with data from ODBC/SQL, where I sort the values firstly in "Team" and in "Person":

Team Person RO
A PA 11111
A PB 11112
A PC 11113
B PD 11114
B PE 11115
C PF 11116

In the QTableView I know, that I can set the gridline via

tableWidget.setShowGrid(False)
tableWidget.setStyleSheet('QTableView::item {border-right: 1px solid #d6d9dc;}')

But I want to make a thick line between different "Teams", so between Person PC/PD and PE/PF should be a thick horizontal seperator on the full row.

How to do that? - I searched the web and dont find anything...

I thought I cloud do it with

QtCore.QAbstractTableModel

but I dont find a Role to give another borderline.

Thanks


Solution

  • A possible solution could be to override the paint event of the table and draw the "thicker" lines based on the data. For optimization reasons, the rows of those lines can be "cached", so that the only computation needed for painting is to get the coordinates for the lines.

    The code below will give the following result:

    screenshot of the result

    class TableSplitLines(QTableWidget):
        linesDirty = False
        penWidth = 3
        referenceColumn = 0
        def __init__(self, *args, **kwargs):
            super().__init__(*args, **kwargs)
            self.splitLines = []
            self.itemChanged.connect(self.markDirty)
    
        def markDirty(self):
            if not self.linesDirty:
                self.linesDirty = True
                self.update()
    
        def updateLines(self):
            '''
            Create a list of rows that correspond to the last rows that have common
            and contiguous values for the choosen column
            '''
            self.splitLines.clear()
            last = None
            for row in range(self.rowCount()):
                first = self.item(row, self.referenceColumn)
                if first is None:
                    value = None
                else:
                    value = first.text()
                if row == 0:
                    last = value
                elif value != last:
                    self.splitLines.append(row)
                    last = value
            self.linesDirty = False
    
        def paintEvent(self, event):
            if self.linesDirty:
                self.updateLines()
    
            if self.splitLines:
                # get the right edge of the last visible column
                hHeader = self.horizontalHeader()
                lastSection = -1
                for i in range(self.columnCount()):
                    logical = hHeader.logicalIndex(i)
                    if not hHeader.isSectionHidden(logical):
                        lastSection = logical
    
                right = min(self.viewport().width(), 
                    hHeader.sectionPosition(lastSection)
                    + hHeader.sectionSize(lastSection)
                    - hHeader.offset() 
                )
    
                extent = self.penWidth // 2 + 1
                top = event.rect().y() - extent
                bottom = event.rect().bottom() + extent
    
                # get the grid color from the current style
                opt = QStyleOption()
                opt.initFrom(self)
                gridColor = QColor.fromRgb(
                    self.style().styleHint(
                        QStyle.SH_Table_GridLineColor, opt, self
                    ) & 0xffffff)
    
                qp = QPainter(self.viewport())
                qp.setPen(QPen(gridColor, self.penWidth, cap=Qt.FlatCap))
    
                # draw lines as long as they're included in the event rect
                vHeader = self.verticalHeader()
                offset = vHeader.offset() + 1
                for row in self.splitLines:
                    y = vHeader.sectionPosition(row) - offset
                    if y < top:
                        continue
                    elif y > bottom:
                        break
                    qp.drawLine(0, y, right, y)
    
            # call the base implementation
            super().paintEvent(event)
    

    Note: the 0xffffffff mask is caused by the fact that the value returned by styleHint() is an unsigned int, but due to the dynamic typing of Python it can be interpreted as a signed (so, possibly negative).