通过 HTML 渲染掌握 QTableView 中的行高

Get a grip of row heights in QTableView with HTML rendering

这是一个 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)

注意 OS 是 W10...在其他 OS 上可能会有所不同。

如果你 运行 这没有 sizeHint 中的行: option.rect.setWidth(400) 你会看到问题的“真实”表现。出于某种原因,sizeHintoption 参数被告知单元格比实际更窄。

那么第一个问题:这个option参数是从哪里来的?是什么构造它并设置它的 rect?这可以更改吗?

第二个问题:即使使用 option.rect.setWidth(400) 行,虽然较长的行(因此涉及分词)看起来还不错并且非常整齐地适合它们的单元格,但较短的行并非如此,适合成一行:它们似乎总是在底部有一个无关的间隙或填充或边距,就好像 table 视图的垂直 header 有一个默认的部分高度或最小部分高度,它覆盖了所需的高度。但实际上设置垂直header的setMinimumSectionSizeand/orsetDefaultSectionSize没有效果

那么是什么导致了那一点“填充”或不正确的行高,如何更正它以非常整齐地适合单元格中的单行?

PS 我已经在 paintsizeHint(以及其他地方!)中对 verticalHeader().resizeSection() 进行了试验...这可能是解决方案的一部分,但我还没找到。

您正在使用 resizeRowToContents before 设置列大小,因此行的高度基于当前列部分的大小,这是默认大小基于 header 内容。

在调整列大小后移动该调用,或者将函数连接到水平 header:

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

option 由视图的 viewOptions() 创建(它有一个空矩形),然后根据调用的函数进行“设置”。

矩形是使用与索引相对应的 header 部分设置的,不应在委托的 sizeHint() 中修改它,因为这不是它的目的。

仅显示一行时高度增加的问题是由于 QStyle,这是因为 resizeRowsToContents 使用 both 行的大小提示 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,它使用节的内容并使用样式的 sizeFromContents 函数创建适当的大小。

如果您确定希望这是默认行为,那么您需要重写 resizeRowsToContents,这样它只会考虑行的大小提示而忽略部分提示。

但是您还应该考虑双击 header 手柄。在这种情况下,问题是信号直接连接到resizeRowToContentsRow在这里是单数!)C++函数,覆盖它是行不通的,所以唯一的可能是完全断开连接发出信号并将其连接到覆盖的函数:

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)

请注意,您应该 尝试调整 sizeHint paint 函数中的部分大小,因为这可能会导致递归。