OK, thanks to the comment by @musicamante, I finally got to a solution.
The trick is basically to avoid creating and returning pixmaps based on QTableView
cell/item size in TableModel.data(...)
for Qt.DecorationRole
(in this example, conditional on data for the cell/item being empty string) - and instead:
CustomItemDelegate
class,QTableView
CustomItemDelegate.paint(...)
method - if the condition is a match, generate pixmap there, as .paint(...)
has access to the .rect
and thus the size of the cell / item - and then also, appropriately, paint the pixmap there as well.With this, handling for Qt.DecorationRole
in TableModel.data(...)
is not needed anymore...
And, the behavior with these changes (code at end of post) is exactly the same as described in OP for "no DecorationRole": at program start, you get this GUI state: (I've added a BackgroundRole and one more cell with text, just to make sure painting is OK)
... and then when you click on "resizeToContents" for the first time after start, the GUI state changes to this:
... and after this, you can click to your heart's content on "resizeToContents", and the cell size will not change!
The corrected code:
import sys
from PyQt5 import QtCore, QtGui, QtWidgets
from PyQt5.QtCore import (Qt, QSize, QPointF)
from PyQt5.QtGui import (QColor, QPalette, QPixmap, QBrush, QPen, QPainter)
from PyQt5.QtWidgets import (QWidget, QVBoxLayout, QPushButton, QStyleOptionViewItem, QStyledItemDelegate)
# starting point from https://www.pythonguis.com/tutorials/qtableview-modelviews-numpy-pandas/
class CustomItemDelegate(QStyledItemDelegate):
def initStyleOption(self, opt, index): # https://stackoverflow.com/q/79129869
super().initStyleOption(opt, index)
print(f"initStyleOption {opt.index=} {opt.text=} {opt.rect=} {opt.backgroundBrush=} {int(opt.features)=:#010b} {opt.decorationPosition=} {opt.decorationSize=}")
#
def create_pixmap(self, pic_qsize): # https://stackoverflow.com/q/62799632
pixmap = QPixmap(pic_qsize)
pixmap.fill(QColor(0,0,0,0))
painter = QPainter()
painter.begin(pixmap)
#painter.setBrush(QtGui.QBrush(QtGui.QColor("blue")))
painter.setPen(QPen(QColor("#446600"), 4, Qt.SolidLine))
painter.drawLine(pixmap.rect().bottomLeft(), pixmap.rect().center()+QPointF(0,4))
painter.drawLine(pixmap.rect().bottomRight(), pixmap.rect().center()+QPointF(0,4))
painter.drawLine(pixmap.rect().topLeft(), pixmap.rect().center()+QPointF(0,-4))
painter.drawLine(pixmap.rect().topRight(), pixmap.rect().center()+QPointF(0,-4))
painter.end()
return pixmap
#
def paint(self, painter, opt, index): # https://stackoverflow.com/q/70487748
# for paint, see also https://stackoverflow.com/q/32032379
# NOTE: opt.text is always "" here - options.text is actual cell/item text!
options = QtWidgets.QStyleOptionViewItem(opt)
self.initStyleOption(options, index)
print(f"paint {opt.text=} {options.text=} {opt.rect=} {options.rect=}")
# super.paint has to run first, if we want the pixmap on top of the BackgroundRole
super(CustomItemDelegate, self).paint(painter, options, index)
if options.text == "":
pixmap = self.create_pixmap(options.rect.size())
painter.drawPixmap(options.rect, pixmap)
#
## NOTE: https://stackoverflow.com/q/9782553
## > sizeHint is useful only when resizeRowsToContents, ...
## > ..., resizeColumnsToContents, ... are called
def sizeHint(self, opt, index): # https://stackoverflow.com/q/70487748
# for sizeHint, see also https://stackoverflow.com/q/71358160
options = QtWidgets.QStyleOptionViewItem(opt)
self.initStyleOption(options, index) # options.text ...
print(f"sizeHint {opt.rect=} {options.rect=}")
return super(CustomItemDelegate, self).sizeHint(options, index)
class TableModel(QtCore.QAbstractTableModel):
def __init__(self, data, parview):
super(TableModel, self).__init__()
self._data = data
self._parview = parview # parent table view
#
def data(self, index, role):
if role == Qt.DisplayRole:
return self._data[index.row()][index.column()]
if role == Qt.BackgroundRole: # https://stackoverflow.com/q/57321588
return QColor("#AAFF" + 2*str(index.column()*8))
## NO more need for DecorationRole for empty cell;
## CustomItemDelegate takes over that now!
#if role == Qt.DecorationRole: # https://stackoverflow.com/q/74203503
# value = self._data[index.row()][index.column()]
# if not(value):
# row_height = self._parview.rowHeight(index.row()) #-5
# column_width = self._parview.columnWidth(index.column()) #-13
# #item = self._parview.itemDelegate(index) # QStyledItemDelegate
# print(f"{row_height=} {column_width=}")
# pic_qsize = QSize(column_width, row_height)
# return self.create_pixmap(pic_qsize)
#
def rowCount(self, index):
return len(self._data)
#
def columnCount(self, index):
return len(self._data[0])