0

Here's an MRE:

import sys
from PyQt5 import QtWidgets, QtCore, QtGui

class MainWindow(QtWidgets.QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle('Get a grip of table view row height MRE')
        self.setGeometry(QtCore.QRect(100, 100, 1000, 800))
        self.table_view = SegmentsTableView(self)
        self.setCentralWidget(self.table_view)
        # self.table_view.horizontalHeader().setSectionResizeMode(QtWidgets.QHeaderView.Fixed)
        rows = [
         ['one potatoe two potatoe', 'one potatoe two potatoe'],
         ['Sed ut perspiciatis, unde omnis iste natus error sit voluptatem accusantium doloremque',
          'Sed ut <b>perspiciatis, unde omnis <i>iste natus</b> error sit voluptatem</i> accusantium doloremque'],
         ['Nemo enim ipsam voluptatem, quia voluptas sit, aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos, qui ratione voluptatem sequi nesciunt, neque porro quisquam est, qui do lorem ipsum, quia dolor sit amet consectetur adipiscing velit, sed quia non numquam do eius modi tempora incididunt, ut labore et dolore magnam aliquam quaerat voluptatem.',
          'Nemo enim ipsam <i>voluptatem, quia voluptas sit, <b>aspernatur aut odit aut fugit, <u>sed quia</i> consequuntur</u> magni dolores eos</b>, qui ratione voluptatem sequi nesciunt, neque porro quisquam est, qui do lorem ipsum, quia dolor sit amet consectetur adipiscing velit, sed quia non numquam do eius modi tempora incididunt, ut labore et dolore magnam aliquam quaerat voluptatem.'
          ],
         ['Ut enim ad minima veniam',
          'Ut enim ad minima veniam'],
         ['Quis autem vel eum iure reprehenderit',
          'Quis autem vel eum iure reprehenderit'],
         ['At vero eos et accusamus et iusto odio dignissimos ducimus, qui blanditiis praesentium voluptatum deleniti atque corrupti, quos dolores et quas molestias excepturi sint, obcaecati cupiditate non provident, similique sunt in culpa, qui officia deserunt mollitia animi, id est laborum et dolorum fuga.',
          'At vero eos et accusamus et iusto odio dignissimos ducimus, qui blanditiis praesentium voluptatum deleniti atque corrupti, quos dolores et quas molestias excepturi sint, obcaecati cupiditate non provident, similique sunt in culpa, qui officia deserunt mollitia animi, id est laborum et dolorum fuga.'
         ]]
        # if the column widths are set before populating the table they seem to be ignored
        # self.table_view.setColumnWidth(0, 400)
        # self.table_view.setColumnWidth(1, 400)
        
        for n_row, row in enumerate(rows):
            self.table_view.model().insertRow(n_row)
            self.table_view.model().setItem(n_row, 0, QtGui.QStandardItem(row[0]))
            self.table_view.model().setItem(n_row, 1, QtGui.QStandardItem(row[1]))
        self.table_view.resizeRowsToContents()
        self.table_view.setColumnWidth(0, 400)
        self.table_view.setColumnWidth(1, 400)
        # if you try to resize the rows after setting the column widths the columns stay 
        # the desired width but completely wrong height ... and yet the point size and option.rect.width in 
        # delegate .paint() and .sizeHint() seem correct
        print('A') # this printout is followed by multiple paints and sizeHints showing that repainting occurs 
        # when the following line is uncommented 
        # self.table_view.resizeRowsToContents()
        
class SegmentsTableView(QtWidgets.QTableView):
    def __init__(self, parent):
        super().__init__(parent)
        self.setItemDelegate(SegmentsTableViewDelegate(self))
        self.setModel(QtGui.QStandardItemModel())
        v_header =  self.verticalHeader()
        # none of the below seems to have any effect:
        v_header.setMinimumSectionSize(5)
        v_header.sectionResizeMode(QtWidgets.QHeaderView.Fixed)
        v_header.setDefaultSectionSize(5)
        
class SegmentsTableViewDelegate(QtWidgets.QStyledItemDelegate):
    def paint(self, painter, option, index):
        doc = QtGui.QTextDocument()
        doc.setDocumentMargin(0)
        print(f'option.font.pointSize {option.font.pointSize()}')
        doc.setDefaultFont(option.font)
        self.initStyleOption(option, index)
        painter.save()
        doc.setTextWidth(option.rect.width())                
        doc.setHtml(option.text)
        option.text = ""
        option.widget.style().drawControl(QtWidgets.QStyle.CE_ItemViewItem, option, painter)
        painter.translate(option.rect.left(), option.rect.top())
        clip = QtCore.QRectF(0, 0, option.rect.width(), option.rect.height())
        print(f'row {index.row()} option.rect.width() {option.rect.width()}')
        print(f'... option.rect.height() {option.rect.height()}')
        
        # has a wild effect: rows gradually shrink to nothing as successive paints continue!
        # self.parent().verticalHeader().resizeSection(index.row(), option.rect.height())
        
        painter.setClipRect(clip)
        ctx = QtGui.QAbstractTextDocumentLayout.PaintContext()
        ctx.clip = clip
        doc.documentLayout().draw(painter, ctx)
        painter.restore()
            
    def sizeHint(self, option, index):
        self.initStyleOption(option, index)
        doc = QtGui.QTextDocument()
        # this indicates a problem: columns option.rect.width is too narrow... e.g. 124 pixels: why?
        print(f'sizeHint: row {index.row()} option.rect.width() |{option.rect.width()}|')

        # setting this to the (known) column width ALMOST solves the problem
        option.rect.setWidth(400)
        
        doc.setTextWidth(option.rect.width())
        print(f'... option.font.pointSize {option.font.pointSize()}')
        doc.setDefaultFont(option.font)
        doc.setDocumentMargin(0)
        # print(f'... option.text |{option.text}|')
        doc.setHtml(option.text)
        doc_height_int = int(doc.size().height())
        print(f'... doc_height_int {doc_height_int}')

        # NB parent is table view        
        # has no effect:
        # self.parent().verticalHeader().resizeSection(index.row(), doc_height_int - 20)
        
        return QtCore.QSize(int(doc.idealWidth()), doc_height_int)
                
app = QtWidgets.QApplication([])
default_font = QtGui.QFont()
default_font.setPointSize(12)
app.setFont(default_font)
main_window = MainWindow()
main_window.show()
exec_return = app.exec()
sys.exit(exec_return)

NB OS is W10... things may work differently on other OSs.

If you run this without the line in sizeHint: option.rect.setWidth(400) you see the problem in its "real" manifestation. For some reason, sizeHint's option parameter is being told the cells are narrower than they are.

So first question: where does this option parameter come from? What constructs it and sets its rect? And can this be altered?

Second question: even with the option.rect.setWidth(400) line, although the longer lines, thus involving word breaks, look OK and fit very neatly into their cells, this is not the case with the shorter lines, which fit into a single line: they always seem to have an extraneous gap or padding or margin at the bottom, as though the table view's vertical header has a default section height or minimum section height which is overriding the desired height. But in fact setting the vertical header's setMinimumSectionSize and/or setDefaultSectionSize has no effect.

So what's causing that bit of "padding", or incorrect row height, and how can it be corrected to fit the single lines very neatly in their cells?

PS I have experimented with verticalHeader().resizeSection() in paint and sizeHint (and elsewhere!)... This might be some part of the solution, but I haven't managed to find it.

mike rodent
  • 14,126
  • 11
  • 103
  • 157

1 Answers1

1

You're using resizeRowToContents before setting the column sizes, so the height of the rows is based on the current column section size, which is the default size based on the header contents.

Move that call after resizing the columns, or, alternatively, connect the function to the sectionResized signal of the horizontal header:

class SegmentsTableView(QtWidgets.QTableView):
    def __init__(self, parent):
        # ...
        self.horizontalHeader().sectionResized.connect(self.resizeRowsToContents)

The option is created by the view's viewOptions() (which has an empty rectangle), then it's "setup" depending on the function that is being called.

The rectangle is set using the header sections that correspond to the index, and it shouldn't be modified in the sizeHint() of the delegate, since that's not its purpose.

The problem with the increased height when only one line is shown is due to the QStyle, and it's because resizeRowsToContents uses both the size hint of the row and the header sectionSizeHint. The section size hint is the result of the SizeHintRole of the headerData for that section or, if it's not set (which is the default), the sectionSizeFromContents, which uses the content of the section and creates an appropriate size using the style's sizeFromContents function.

If you're sure that you want this to be the default behavior, then you need to override resizeRowsToContents, so that it will only consider the size hint of the row while ignoring the section hint.

But you should also consider double clicking on the header handle. In this case, the problem is that the signal is directly connected to the resizeRowToContents (Row is singular here!) C++ function, and overriding it will not work, so the only possibility is to completely disconnect the signal and connect it to the overridden function:

class SegmentsTableView(QtWidgets.QTableView):
    def __init__(self, parent):
        # ...
        v_header =  self.verticalHeader()
        v_header.sectionHandleDoubleClicked.disconnect()
        v_header.sectionHandleDoubleClicked.connect(self.resizeRowToContents)
        self.horizontalHeader().sectionResized.connect(self.resizeRowsToContents)

    def resizeRowToContents(self, row):
        self.verticalHeader().resizeSection(row, self.sizeHintForRow(row))

    def resizeRowsToContents(self):
        header = self.verticalHeader()
        for row in range(self.model().rowCount()):
            hint = self.sizeHintForRow(row)
            header.resizeSection(row, hint)

Note that you should not try to resize sections in the sizeHint nor paint functions, because that could cause recursion.

musicamante
  • 41,230
  • 6
  • 33
  • 58
  • Thanks... This use of the signal obviates the need to have `option.rect.setWidth()` in `sizeHint`, which is one thing accomplished. Unfortunately the inexplicable margin/padding under one-line (no word break) strings is still there on my machine. What about your machine? – mike rodent Sep 07 '21 at 15:56
  • PS I find I can finally accomplish this, but it is absolutely horrible, and does involve sticking a line which doesn't belong there in `paint`, and the parameter of which is dependent on the font size. See edit of question. – mike rodent Sep 07 '21 at 16:03
  • @mikerodent Sorry, I forgot to address that, see the update. Remember, as already suggested, no change in geometries should **ever** happen in a paint function, because it could lead to recursion: best case scenario, you'll have dozens of unnecessary repaints and function calls until the change respects the expected result, but with complex situations like these (including rich text layout and word wrapping) that could become *infinite* recursion. – musicamante Sep 07 '21 at 17:08
  • Superbo, mille grazie! Yes, I've understood that, and that's why I described my nasty workaround as "horrible". Apart from recursion, interfering with `paint()` in the wrong way also seems to lead to flicker and other horrors. I've seen this with Java and Swing, so am quite delighted to use your (proper) solution instead. – mike rodent Sep 07 '21 at 18:25
  • By the way, on my machine, if I comment out `v_header.setMinimumSectionSize(5)` (or some very low value), the one-liner padding problem still occurs. It seems like this is needed, on my machine at least. – mike rodent Sep 07 '21 at 18:30
  • @mikerodent Basically: a paint function should only care about drawing, doing anything else there is conceptually wrong and possibly dangerous. Btw, I commented all vertical header settings you used and I don't reproduce the problem at least on Linux, but it's possible that on other platforms the default minimum section size is different (see the note on the [documentation](https://doc.qt.io/qt-5/qheaderview.html#minimumSectionSize-prop)) due to different global strut setting. You could just set it to the default metrics: `v_header.setMinimumSectionSize(QApplication.fontMetrics().height())`. – musicamante Sep 07 '21 at 18:44