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 子类,正确覆盖 sizeHint
和 paintEvent
并继续执行所有这些,但这不是重点。
虽然 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
更低的位置,baseline
、sub
或bottom
之间绝对没有区别。这可能取决于文本布局引擎的性能原因(如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 便携式文本富文本编辑器中的文本/图像对齐