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

问题描述 投票:0回答:2

这是 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)
中的行的情况下运行此命令,您会看到问题的“真实”表现。由于某种原因,
sizeHint
option
参数被告知单元格比实际更窄。

所以第一个问题:这个

option
参数来自哪里?是什么构造了它并设置了它的
rect
?这可以改变吗?

第二个问题:即使使用

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

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

PS我已经在

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

python html pyqt5 rendering qtableview
2个回答
1
投票

您正在使用

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

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

sectionResized
信号:

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

option
是由视图的
viewOptions()
(有一个空矩形)创建的,然后它的“设置”取决于正在调用的函数。

矩形是使用与索引相对应的标题部分设置的,并且不应在委托的

sizeHint()
中对其进行修改,因为这不是其目的。

仅显示一行时高度增加的问题是由于 QStyle 造成的,这是因为

resizeRowsToContents
使用了 both 行的大小提示 and 标题
sectionSizeHint
。部分大小提示是该部分的 headerData 的
SizeHintRole
的结果,或者,如果未设置(这是默认设置),则为
sectionSizeFromContents
,它使用该部分的内容并创建适当的大小使用样式的
sizeFromContents
函数。

如果您确定希望这是默认行为,那么您需要覆盖

resizeRowsToContents
,这样它只会考虑行的大小提示,而忽略部分提示。

但您还应该考虑双击标题手柄。在这种情况下,问题是信号直接连接到

resizeRowToContents
(这里
Row
是单数!)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

 
nor paint
 函数中的部分大小,因为这可能会导致递归。


0
投票
Musicamante 是所有 PyQt 事物的大师。所以我在在这里给出另一个答案之前犹豫了一下。但我终于找到了一种实现可变行高表的方法,它也可以呈现中等复杂度的 HTML。

Musicamante 上面给出的解决方案以及他对覆盖

resizeRowToContents

resizeRowsToContents
 的建议在此 MRE 中没有问题。但是,当我实现稍微复杂一点的 HTML(每个单元格包含几个 
<span>...</span>
,实际上是用多种荧光笔颜色突出显示单词)时,出现了一个问题:调整窗口大小后,会出现一种令人讨厌的现象,即文本被“写在”文本的顶部。如果您只需单击单元格,文本就会自行清理。但我想以编程方式做到这一点。

我的建议是对 Musicamante 的代码进行非常细微的调整:

def resizeRowsToContents(self): header = self.verticalHeader() def resize_rows_again(): for row in range(self.model().rowCount()): hint = self.sizeHintForRow(row) header.resizeSection(row, hint) QtCore.QTimer.singleShot(1, resize_rows_again)
...其他变体似乎也有效,例如:

def resizeRowsToContents(self): super().resizeRowsToContents() def resize_rows_again(): for row in range(self.model().rowCount()): self.resizeRowToContents(row) QtCore.QTimer.singleShot(1, resize_rows_again)
...通过上面的示例,实际上似乎不再需要覆盖 

resizeRowToContents

(“行”单数)...

© www.soinside.com 2019 - 2024. All rights reserved.