Qt富文本中图像对象的最小尺寸和对齐问题

Posted

技术标签:

【中文标题】Qt富文本中图像对象的最小尺寸和对齐问题【英文标题】:Issues with minimum size and alignment of image objects in Qt rich text 【发布时间】:2022-01-13 23:50:11 【问题描述】:

我想为 QStatusBar 创建一个小的、基本的持久 QLabel,它还有一个基于字体高度的小图标。图标其实是一个base64嵌入<img>,这样我就可以使用QLabel常用的Qt富文本引擎来代替创建复合小部件了。

图像大小基于字体指标,因此它应该在技术上适合标签的最小尺寸提示。如果字体度量返回 16 像素的高度,则添加具有 16 像素高度的嵌入图像应该不会更改标签提示。不幸的是,情况似乎并非如此。

图像添加到标签后,即使图像高度等于字体度量的高度,高度也会增加,并且始终与顶部垂直对齐;尝试设置对齐似乎没有太大帮助,这可能与this qt-forum post有关。

使用 html 表格可以部分解决问题:垂直对齐得到尊重,但添加的边距仍然存在。

我知道我们只是在谈论几个像素,但我真的不喜欢当前的行为:在 有图像的文本和有 的另一个文本之间切换不是,这会导致整个布局发生变化(可能还有父窗口小部件的尺寸提示,这显然是一个问题,尤其是在标签必须在 QStatusBar 中使用时)。

虽然有可能在不显示图像时添加“幽灵”图像 (width=0),但我仍然有兴趣了解为什么会发生这种情况,并且 如果 它可以被覆盖.

我知道可以通过访问 QTextDocument 的布局来解决一些问题,但是,由于 QLabel 仅私下使用 QTextDocument,这不是一种可行的方法。

我也知道我可以忽略所有这些并创建一个 QWidget 子类,正确覆盖 sizeHintpaintEvent 并继续执行所有这些,但这不是重点。

虽然 Qt 富文本文档暗示支持对齐属性,但几乎在任何情况下,图像的垂直对齐似乎都被忽略了,除了 "middle",它实际上将图像与(可能)下一个的顶部对齐行,这对我来说没有多大意义。

为了更好地理解问题,这里有一个基本的演示来说明我的观点。 标签是布局对齐的并使用边框,因此您可以清楚地看到每个项目的边界矩形:每当添加图像时,都会添加一些边距(范围取决于操作系统和样式)。 代码基于 PyQt,但我知道问题出在 Qt 方面:

import sys
from PyQt5 import QtCore, QtGui, QtWidgets

StyleSheet = 'QLabel  border: 1px solid darkGray; '
BaseText = '<img align src="data:image/png;base64,img;"> label'
TableText = '<table><tr><td align><img src="data:image/png;base64,img;"></td><td>label</td></tr></table>'

class LabelTest(QtWidgets.QMainWindow):
    def __init__(self):
        super().__init__()
        central = QtWidgets.QWidget()
        self.setCentralWidget(central)
        layout = QtWidgets.QVBoxLayout(central)
        top = QtWidgets.QHBoxLayout()
        layout.addLayout(top)

        boldFont = self.font()
        boldFont.setBold(True)

        top.addWidget(QtWidgets.QLabel('Icon theme:'))
        self.iconCombo = QtWidgets.QComboBox()
        top.addWidget(self.iconCombo)
        currentTheme = QtGui.QIcon.themeName().lower()
        themes = []
        for iconPath in QtGui.QIcon.themeSearchPaths():
            it = QtCore.QDirIterator(iconPath, ['*'], QtCore.QDir.Dirs|QtCore.QDir.NoDotAndDotDot)
            while it.hasNext():
                if QtCore.QDir(it.next()).exists('index.theme'):
                    themeName = it.fileName()
                    if themeName.lower() in themes:
                        continue
                    themes.append(themeName.lower())
                    if themeName.lower() == currentTheme:
                        index = self.iconCombo.count()
                        self.iconCombo.addItem(themeName + '*', themeName)
                        self.iconCombo.model().setData(
                            self.iconCombo.model().index(index, 0), 
                                boldFont, QtCore.Qt.FontRole)
                        self.iconCombo.setCurrentIndex(index)
                    else:
                        self.iconCombo.addItem(themeName, themeName)

        top.addWidget(QtWidgets.QLabel('Style'))
        self.styleCombo = QtWidgets.QComboBox()
        top.addWidget(self.styleCombo)
        currentStyle = self.style().objectName().lower()
        for i, styleName in enumerate(QtWidgets.QStyleFactory.keys()):
            if styleName.lower() == currentStyle:
                # automatically select the current style
                self.styleCombo.addItem(styleName + '*', styleName)
                self.styleCombo.model().setData(
                    self.styleCombo.model().index(i, 0), 
                    boldFont, QtCore.Qt.FontRole)
                self.styleCombo.setCurrentIndex(i)
            else:
                self.styleCombo.addItem(styleName, styleName)

        self.boundingRectCheck = QtWidgets.QCheckBox('Bounding rect')
        top.addWidget(self.boundingRectCheck)
        top.addStretch()

        mid = QtWidgets.QHBoxLayout()
        layout.addLayout(mid)
        self.alignCombo = QtWidgets.QComboBox()
        mid.addWidget(self.alignCombo)
        for alignment in ('', 'top', 'super', 'middle', 'baseline', 'sub', 'bottom'):
            if alignment:
                self.alignCombo.addItem(alignment.title(), alignment)
            else:
                self.alignCombo.addItem('No alignment')
        self.tableCheck = QtWidgets.QCheckBox('Table')
        mid.addWidget(self.tableCheck)
        self.labelIconCheck = QtWidgets.QCheckBox('Status icon')
        mid.addWidget(self.labelIconCheck)
        self.statusCombo = QtWidgets.QComboBox()
        mid.addWidget(self.statusCombo)

        frameLayout = QtWidgets.QGridLayout()
        layout.addLayout(frameLayout)
        frameLayout.setColumnStretch(3, 1)
        self.labelData = []
        for label in ('Information', 'Warning', 'Critical', 'Question'):
            row = frameLayout.rowCount()
            self.statusCombo.addItem(label)
            pixmapLabel = QtWidgets.QLabel(styleSheet=StyleSheet)
            frameLayout.addWidget(pixmapLabel, 
                row, 0, alignment=QtCore.Qt.AlignCenter)
            frameLayout.addWidget(QtWidgets.QLabel(label, styleSheet=StyleSheet), 
                row, 1, alignment=QtCore.Qt.AlignVCenter)
            formattedLabel = QtWidgets.QLabel(styleSheet=StyleSheet)
            frameLayout.addWidget(formattedLabel, 
                row, 2, alignment=QtCore.Qt.AlignVCenter)
            self.labelData.append((label, pixmapLabel, formattedLabel))

        mid.addStretch()

        self.editor = QtWidgets.QTextEdit(readOnly=True)
        self.editor.setMinimumHeight(1)
        frameLayout.addWidget(self.editor, 1, 3, frameLayout.rowCount(), 1)

        self.statusLabel = QtWidgets.QLabel(styleSheet=StyleSheet)
        self.statusBar().addPermanentWidget(self.statusLabel)

        self.iconCombo.currentIndexChanged.connect(self.setStatus)
        self.styleCombo.currentIndexChanged.connect(self.updateStyle)
        self.alignCombo.currentIndexChanged.connect(self.setStatus)
        self.boundingRectCheck.toggled.connect(self.setStatus)
        self.tableCheck.toggled.connect(self.setStatus)
        self.statusCombo.currentIndexChanged.connect(self.setStatus)
        self.labelIconCheck.toggled.connect(self.setStatus)

        self.setStatus()

    def setStatus(self):
        self.editor.clear()
        align = self.alignCombo.currentData()
        if self.tableCheck.isChecked():
            baseText = TableText
            if align:
                align = 'style="vertical-align: "'.format(align)
        else:
            baseText = BaseText
            if align:
                align = 'align=""'.format(align)

        statusIcon = self.labelIconCheck.isChecked()
        if not statusIcon:
            self.statusLabel.setText(self.statusCombo.currentText())
        boundingRect = self.boundingRectCheck.isChecked()
        
        pen1 = QtGui.QPen(QtCore.Qt.black)
        pen1.setDashPattern([1, 1])
        pen2 = QtGui.QPen(QtCore.Qt.white)
        pen2.setDashPattern([1, 1])
        pen2.setDashOffset(1)

        # create pixmaps from the icon theme, with size based on the font metrics
        QtGui.QIcon.setThemeName(self.iconCombo.currentData())
        iconSize = self.fontMetrics().height()
        for i, (label, pixmapLabel, formattedLabel) in enumerate(self.labelData):
            enum = getattr(QtWidgets.QStyle, 'SP_MessageBox' + label)
            icon = self.style().standardIcon(enum)
            pixmap = icon.pixmap(iconSize)
            pixmapLabel.setPixmap(pixmap)

            if boundingRect and not pixmap.isNull():
                qp = QtGui.QPainter(pixmap)
                qp.setPen(pen1)
                qp.drawRect(pixmap.rect().adjusted(0, 0, -1, -1))
                qp.setPen(pen2)
                qp.drawRect(pixmap.rect().adjusted(0, 0, -1, -1))
                qp.end()

            # create a QByteArray of the resized icon so that we can use the
            # embedded base64 data for the HTML image
            byteArray = QtCore.QByteArray()
            buffer = QtCore.QBuffer(byteArray)
            buffer.open(buffer.WriteOnly)
            pixmap.save(buffer, 'png')
            imageData = byteArray.toBase64().data().decode()
            embedText = baseText.format(
                img=imageData, 
                label=label, 
                align=align
            )
            formattedLabel.setText(embedText)
            if statusIcon:
                if i == self.statusCombo.currentIndex():
                    self.statusLabel.setText(formattedLabel.text())
                self.editor.append(embedText)
            else:
                self.editor.append(label)

        QtCore.QTimer.singleShot(50, lambda: self.resize(self.minimumSizeHint()))

    def updateStyle(self):
        QtWidgets.QApplication.setStyle(self.styleCombo.currentData())
        QtCore.QTimer.singleShot(50, lambda: self.resize(self.minimumSizeHint()))


app = QtWidgets.QApplication(sys.argv)
w = LabelTest()
w.show()
app.exec()

上面的代码基本上是这样显示的:

【问题讨论】:

对我来说,顶部对齐效果很好——也就是说,右侧标签的高度与左侧标签的高度完全相同(并且状态栏标签的高度不会改变) .这似乎是有道理的,给定how vertical-align is defined for CSS2 - 即如果图像高度与行高相同,对齐它们的顶部边缘应该使它们完全重叠。中间对齐看起来很棘手,因为它是基于 x 高度的。 PS:从您的屏幕截图中不完全清楚的一件事是图标的形式。可见像素是否居中对齐,它们是否填充了图像的整个区域? @ekhumoro 感谢您的意见!我改进了测试代码(见更新)以显示图像范围,同时我有机会做进一步的测试。事实证明,使用更新的 Qt 版本和改进的代码top 对齐确实尊重给定的高度。但是,对齐仍然存在一些问题:出于某种原因,middle 将图像置于比bottom 更低的位置,baselinesubbottom 之间绝对没有区别。这可能取决于文本布局引擎的性能原因(如middle),但我只是猜测。 @ekhumoro 也就是说,我无法追踪 何时 发生更改(介于 5.7 和 5.13 之间),这仍然是我感兴趣的事情。虽然我知道 5.7 已经很老了,我不应该考虑那么多,但我仍然想知道在哪里(以及何时/如何)发生了变化。无论如何,我会在进一步研究后的几天内添加答案。再次感谢您。 Middle 部分相对于 x 高度(这是特定于字体的),而 bottom 只是与 line-box 的下边缘对齐。鉴于此,它可能会被渲染得更低是有道理的。对于 sub 和 super ,文本会自动以较小的字体呈现并在行框中 within 对齐,因此对图像进行不同的处理也就不足为奇了。然而,除此之外,图像的整体行为实际上比文本更一致(即与现代浏览器相比)。对于图像,只有真正的 sub 不能正常工作(它的行为应该与底部相同)。 【参考方案1】:

以下是 Firefox、Chrome 和 QLabel 的一些测试输出:

AFAICS,QLabel 唯一明显不正确的行为是 text/middle(应将文本在行框中对齐较低)和 image/sub(应将行框与底部对齐)。两个浏览器的行为并不相当相同 - 但我不知道哪个更正确。

HTML 测试页:

<html>
<head><title>Test</title>
</head>
<body style="font: 12pt dejavu sans">
<br>
<hr>
:: xX<span style="background-color: red">Text</span>Xx
 jyf xX<img src="red-small.png">Xx Default
<hr>
:: xX<span style="background-color: red; vertical-align: top">Text</span>Xx
 jyf xX<img style="vertical-align: top" src="red-small.png">Xx Top
<hr>
:: xX<span style="background-color: red; vertical-align: bottom">Text</span>Xx
 jyf xX<img style="vertical-align: bottom" src="red-small.png">Xx Bottom
<hr>
:: xX<span style="background-color: red; vertical-align: middle">Text</span>Xx
 jyf xX<img style="vertical-align: middle" src="red-small.png">Xx Middle
<hr>
:: xX<span style="background-color: red; vertical-align: sub">Text</span>Xx
 jyf xX<img style="vertical-align: sub" src="red-small.png">Xx Sub
<hr>
:: xX<span style="background-color: red; vertical-align: super">Text</span>Xx
 jyf xX<img style="vertical-align: super" src="red-small.png">Xx Super
<hr>
:: xX<span style="background-color: red; vertical-align: baseline">Text</span>Xx
 jyf xX<img style="vertical-align: baseline" src="red-small.png">Xx Baseline
<hr>
</body>
</html>

red-small.png

【讨论】:

以上是关于Qt富文本中图像对象的最小尺寸和对齐问题的主要内容,如果未能解决你的问题,请参考以下文章

Sanity.io 便携式文本富文本编辑器中的文本/图像对齐

Qt中富文本处理

Qt QLabel 富文本制表

Qt - 如何在 QComboBox 中使用富文本?

为啥 Qt::mightBeRichText() 不能将 HTML 表格标签识别为富文本?

Qt富文本编辑器QTextDocument