Reputation: 5492
In the example below (at end of the post), what I want to do, is to
Now, with the code below, things generally work:
The problem is with the starting "empty" state, the DecorationRole drawing, and resizeToContents - let me illustrate:
self.do_decoration = False
), then upon start we get this GUI state:self.do_decoration = True
), then the first GUI state after program start looks like this:The behavior I otherwise wanted (and expected) is: if a QTableView starts off with showing a single cell with empty data, and there are no changes to the cell data, then no matter how many times I click on "resizeToContents", I should have no change in the GUI state.
The fact that the GUI state changes in the "no DecorationRole" case at start is not a problem: I guess "resizeToContents" takes into account the contents of the labels "1" in the row and column headers as well, and resizes everything according to that - which can be solved by running "resizeToContents" once in init; then the user would see no change when "resizeToContents" is clicked after program start.
However, the constant growing of the cell size in the "yes DecorationRole" case is a problem; in my opinion, it is due to the fact that I cannot really get the size of the cell/item, since in a QTableView
, the cells/items are not even QWidget
s - they are QStyledItemDelegate
, which have no methods to get or set item width or height. And therefore I have attempted a workaround in the code by using QTableView.rowHeight
and QTableView.columnWidth
- but these apparently return sizes that are slightly larger than the actual cell/item dimensions, and so the DecorationRole pixmap ends up slightly larger than the cell/item actual size - so next time "resizeToContents", it tries to accomodate this, but then a new, even larger, decoration pixmap is generated - and we end up in a sort of a recursive increase of cell size.
Which is why my question is ultimately - how do I get the actual size of a QStyledItemDelegate item (cell) in a QTableView? My guess is, if I could generate a pixmap with the actual size of the cell, then subsequent calls to "resizeToContents" would not increase the cell size anymore, and things would work as I had imagined them.
Here is the 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 TableModel(QtCore.QAbstractTableModel):
def __init__(self, data, parview):
super(TableModel, self).__init__()
self._data = data
self._parview = parview # parent table view
self.do_decoration = True
#
def create_pixmap(self, pic_qsize): # SO: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 data(self, index, role):
if role == Qt.DisplayRole:
return self._data[index.row()][index.column()]
if self.do_decoration and role == Qt.DecorationRole: # SO: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])
class MainWindow(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
self.centralw = QWidget()
self.setCentralWidget(self.centralw)
self.vlayout = QVBoxLayout(self.centralw)
#
self.btn = QPushButton("resizeToContents")
self.btn.clicked.connect(self.resizeToContents)
self.vlayout.addWidget(self.btn)
#
self.table_view = QtWidgets.QTableView()
data = [ [ "" ] ]
self.model = TableModel(data, self.table_view)
self.table_view.setModel(self.model)
self.vlayout.addWidget(self.table_view)
#
#
def resizeToContents(self):
self.table_view.resizeColumnsToContents()
self.table_view.resizeRowsToContents()
app=QtWidgets.QApplication(sys.argv)
window=MainWindow()
window.show()
window.resize(280, 140)
app.exec_()
Upvotes: 0
Views: 58
Reputation: 5492
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])
Upvotes: 0