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

Posted

技术标签:

【中文标题】通过 HTML 渲染掌握 QTableView 中的行高【英文标题】:Get a grip of row heights in QTableView with HTML rendering 【发布时间】:2021-09-06 19:14:40 【问题描述】:

这是一个 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 操作系统是 W10...在其他操作系统上可能会有所不同。

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

所以第一个问题:这个option 参数从何而来?是什么构造它并设置它的rect?这可以改变吗?

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

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

PS 我在paintsizeHint(以及其他地方!)中尝试过verticalHeader().resizeSection()...这可能是解决方案的一部分,但我还没有设法找到它。

【问题讨论】:

【参考方案1】:

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

在调整列大小后移动该调用,或者将函数连接到水平标题的sectionResized 信号:

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

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

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

只显示一行时高度增加的问题是由于QStyle,这是因为resizeRowsToContents使用两个行的大小提示标题sectionSizeHint。节大小提示是该节的 headerData 的 SizeHintRole 的结果,或者,如果未设置(这是默认设置),则为 sectionSizeFromContents,它使用节的内容并使用样式的sizeFromContents 函数。

如果您确定希望这是默认行为,则需要覆盖 resizeRowsToContents,以便它只考虑行的大小提示而忽略节提示。

但您也应该考虑双击标题句柄。在这种情况下,问题是信号直接连接到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 函数,因为这可能会导致递归。

【讨论】:

谢谢...这种信号的使用消除了在sizeHint 中包含option.rect.setWidth() 的需要,这是完成的一件事。不幸的是,我的机器上仍然存在单行(无断字)字符串下的莫名其妙的边距/填充。你的机器呢? PS 我发现我终于可以做到这一点,但这绝对是可怕的,并且确实涉及在paint 中粘贴不属于那里的行,其参数取决于字体尺寸。查看问题的编辑。 @mikerodent 对不起,我忘了解决这个问题,请参阅更新。请记住,正如已经建议的那样,在绘制函数中永远不应该发生几何变化,因为它可能导致递归:最好的情况是,在更改之前,您将有几十个不必要的重绘和函数调用尊重预期的结果,但在这些复杂的情况下(包括富文本布局和自动换行)可能会变成无限递归。 超级棒,万岁!是的,我明白这一点,这就是为什么我将我讨厌的解决方法描述为“可怕的”。除了递归之外,以错误的方式干扰paint() 似乎也会导致闪烁和其他恐怖。我已经在 J​​ava 和 Swing 中看到了这一点,因此很高兴改用您的(正确的)解决方案。 顺便说一句,在我的机器上,如果我注释掉v_header.setMinimumSectionSize(5)(或一些非常低的值),仍然会出现单行填充问题。至少在我的机器上似乎需要这个。

以上是关于通过 HTML 渲染掌握 QTableView 中的行高的主要内容,如果未能解决你的问题,请参考以下文章

将按钮添加到 QTableview

QTableView 固定列宽度(鼠标拖动后,仍可固定)

QTableView + QAbstractTableModel:通过拖放移动行

在QTableView中某列中添加Button的导致滚动条滚动的时候消失的问题

如何通过在 PyQt5 中按 Enter 键从 QTableView 获取数据

通过循环不断更新QTableView