Yozh2
Yozh2

Reputation: 3

Unexpected padding pyqt in qtableview cell

I'm trying to create a custom TableModel class for QTableView. The cells containing 1 as data must have red outlining. Outlining is made by returning a pixmap (with red borders and text drawn on top) from TableModel instead of returning a simple string.

The problem is in the unexpected padding of the pixmap, which I return as DecorationRole. I checked if the pixmap is drawn correctly (and it actually is 21x21 px with well done outlining, without padding, just as planned) right before the return pixmap line.

Here is the image describing the problem

Here is the right drawn pixmap, which was saved just before the return from TableModel:

pixmap

Eventually, something shifts the returned pixmap by exactly 3px from the left border of the QTableView cell. I didn't set any padding in QtDesigner for the QTableView and didn't change it later in my code. I also tried to manually set up padding to zero using stylesheet, but it gave no different result.

Any ideas how to fix it? Thank you.

Here is the sample of my TableModel:

class TableModel(QtCore.QAbstractTableModel):
def __init__(self, topology=None):
    super().__init__()
    ...
    # Hardcode cell size and path to rectangle image
    self.cell_width, self.cell_height = 21, 21
    self.fpath_red_rect = './path/to/red_rect.png'

def rowCount(self, parent=QtCore.QModelIndex()):
    return self.data.shape[0]

def columnCount(self, parent=QtCore.QModelIndex()):
    return self.data.shape[1]

def headerData(self, section, orientation, role=QtCore.Qt.DisplayRole):
    ...

def size(self):
    return QtCore.QSize((self.columnCount() + 1) * self.cell_width,
                        (self.rowCount() + 1) * self.cell_height)

def data(self, index, role=QtCore.Qt.DisplayRole):
    if not index.isValid():
        return QtCore.QVariant()

    i = index.row()
    j = index.column()

    if role == QtCore.Qt.DisplayRole:
        if self.data[i, j] == 0:      # empty
            return ''
        elif self.data[i, j] == 1:    # cell with red rectangle
            # the text will be drawn on pixmap manually later
            return None
        else:
            return '{0}'.format(self.data[i, j])    # display default data

    if role == QtCore.Qt.DecorationRole:
        # Create pixmap, draw the rectangle on it and then draw text on top
        pixmap = QtGui.QPixmap(self.cell_width, self.cell_height)
        image = QtGui.QImage(self.fpath_red_rect).scaled(self.cell_width, self.cell_height)
        painter = QtGui.QPainter(pixmap)
        painter.drawImage(pixmap.rect().topLeft(), image)
        painter.drawText(pixmap.rect(), QtCore.Qt.AlignCenter, '{0}'.format(self.data[i, j]))
        painter.end()

        # If we save the pixmap to PNG image here (see the link above),
        # we get the expected 21 x 21 px image, with nice
        # and properly drawn rectangle and centered text.
        # But something goes wrong after returning

        return pixmap

    if role == QtCore.Qt.BackgroundRole:
        return QtGui.QBrush(self.getQtColor(self.data[i, j]))

    if role == QtCore.Qt.TextAlignmentRole:
        return QtCore.Qt.AlignCenter

    return QtCore.QVariant()

Upvotes: 0

Views: 550

Answers (1)

eyllanesc
eyllanesc

Reputation: 243897

DecorationRole is used to draw the icon for that reason you observe the displacement, in your case you should not use that role, besides the painting task should not be done in the model since he only has to provide the data, if you want to modify the drawing a better option is use a delegate as I show below:

import sys

import numpy as np

from PyQt5 import QtCore, QtGui, QtWidgets

ValueRole = QtCore.Qt.UserRole + 1
max_val = 4

colors = [QtGui.QColor(*np.random.randint(255, size=3)) for i in range(max_val)]


class TableModel(QtCore.QAbstractTableModel):
    def __init__(self, parent=None):
        QtCore.QAbstractTableModel.__init__(self, parent)
        self.data = np.random.randint(max_val, size=(10, 10))

    def rowCount(self, parent=QtCore.QModelIndex()):
        return self.data.shape[0]

    def columnCount(self, parent=QtCore.QModelIndex()):
        return self.data.shape[1]

    def data(self, index, role=QtCore.Qt.DisplayRole):
        if not index.isValid():
            return QtCore.QVariant()

        i = index.row()
        j = index.column()
        val = self.data[i, j]

        if role == QtCore.Qt.DisplayRole:
            return str(val)

        elif role == QtCore.Qt.TextAlignmentRole:
            return QtCore.Qt.AlignCenter

        elif role == QtCore.Qt.BackgroundRole:
            return colors[val]

        if role == ValueRole:
            return val

        return QtCore.QVariant()

class Delegate(QtWidgets.QStyledItemDelegate):
    def paint(self, painter, option, index):
        QtWidgets.QStyledItemDelegate.paint(self, painter, option, index)
        if index.data(ValueRole) == 1:
            painter.save()
            pen = painter.pen()
            pen.setColor(QtCore.Qt.red)
            painter.setPen(pen)
            r = QtCore.QRect(option.rect)
            r.adjust(0, 0, -pen.width(), -pen.width())
            painter.drawRect(r)
            painter.restore()

if __name__ == '__main__':
    app = QtWidgets.QApplication(sys.argv)
    w = QtWidgets.QTableView()
    w.setItemDelegate(Delegate(w))
    model = TableModel()
    w.setModel(model)

    w.verticalHeader().setSectionResizeMode(QtWidgets.QHeaderView.Fixed)
    w.horizontalHeader().setSectionResizeMode(QtWidgets.QHeaderView.Fixed)

    for i in range(model.rowCount()):
        w.verticalHeader().resizeSection(i, 21)

    for j in range(model.columnCount()):
        w.horizontalHeader().resizeSection(j, 21)
    w.show()
    sys.exit(app.exec_())

enter image description here

Upvotes: 1

Related Questions