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