sdbbs
sdbbs

Reputation: 5492

Getting the size of an item (QStyledItemDelegate?) in a QTableView in PyQt5?

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:

  1. First, if the code below does not do the DecorationRole image for empty cells/items (self.do_decoration = False), then upon start we get this GUI state:
    no DecorationRole, QTableView start
    ... and after clicking "resizeToContents", we get this GUI state instead:
    no DecorationRole, QTableView after "resizeToContents" click
    ... that is, the cell "shrinks" (gets its width reduced). However, on subsequent clicks on "resizeToContents", this second GUI state remains unchanged (constant).
  2. However, if we do enable the DecorationRole image for empty cells/items (self.do_decoration = True), then the first GUI state after program start looks like this:
    DecorationRole, QTableView start
    ... and on every subsequent click on "resizeToContents", the cell/item grows - so after 5 clicks on "resizeToContents" the GUI state is like this:
    DecorationRole, QTableView after 5 clicks on "resizeToContents"

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 QWidgets - 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

Answers (1)

sdbbs
sdbbs

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:

  • create a CustomItemDelegate class,
  • apply this class to the entire (all cells/items of) QTableView
  • and check for data empty for the cell/item in the 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)

corrected code - table view start

... and then when you click on "resizeToContents" for the first time after start, the GUI state changes to this:

corrected code - table view after resizeToContents

... 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

Related Questions