PySide6 中用 QTextObjectInterface 绘制 LaTeX 公式为何不显示?
可以通过 QTextObjectInterface 在 QTextEdit 中绘制公式。现有代码的基本流程符合 Qt 的接口设计,建议先确认自定义对象的 handler 是否生效,以及 intrinsicSize() 和 drawObject() 是否被调用。仅凭“PyQt6 能显示、PySide6 不显示”,还不能确定具体的 PySide6 缺陷或受影响版本。
补充信息中确认 PyQt6 版本能够显示公式,因此可以作为当前环境下的替代方案。如果继续使用 PySide6,可以通过下面的检查缩小故障范围。
先检查对象注册和绘制回调
Qt 自定义文本对象的类型应从 UserObject 范围内选取。处理类需要同时继承 QObject 和 QTextObjectInterface,并将 QObject 放在前面;handler 需要注册到当前文档布局中。插入时使用对应格式和 \uFFFC 对象替换字符,布局随后通过两个回调取得尺寸并绘制对象。参见 QTextObjectInterface 文档。
原代码已经包含这些步骤,也通过 self.renderer 保留了处理对象。可以先在注册后检查 handler:
layout = self.document().documentLayout()
layout.registerHandler(LATEX_FORMAT, self.renderer)
print("handler:", layout.handlerForObject(LATEX_FORMAT))
再临时将两个回调改为绘制不依赖 Matplotlib 的固定矩形:
def intrinsicSize(self, doc, posInDocument, format):
print("intrinsicSize:", posInDocument, format.objectType())
return QSizeF(160, 50)
def drawObject(self, painter, rect, doc, posInDocument, format):
print("drawObject:", posInDocument, rect)
painter.fillRect(rect, QColor("#f0f0f0"))
显示窗口后,根据输出判断:
handlerForObject()返回None:优先检查注册对象、当前文档布局和绑定版本。- handler 存在,但没有回调输出:检查插入字符是否为
\uFFFC,以及它的objectType()是否等于注册类型。 - 固定矩形能够显示:继续检查公式渲染、图片解码和返回尺寸。
- 固定矩形也不显示:继续排查文本对象的布局和绘制路径,此时调整 TeX 配置帮助有限。
registerHandler() 不会接管 handler 的所有权,因此需要持续保留对象引用。替换文档或文档布局后,也要在新的布局上注册。相关接口见 QAbstractTextDocumentLayout 文档。
检查公式图片,并使用自定义属性编号
原代码使用 setProperty(1, latex) 保存公式。建议改用 QTextFormat.Property.UserProperty 范围内的编号,避免与 Qt 内置属性编号冲突。这是对数据存储方式的规范,不能单凭这一点认定它是空白显示的原因。枚举定义见 QTextFormat 文档。
下面的代码可以替换原来的常量和 LatexRenderer。它保留了原来的 PNG 渲染方式,增加了图片加载检查,并为应用自己的缓存设置容量上限:
LATEX_FORMAT = QTextFormat.ObjectTypes.UserObject.value + 1
LATEX_PROPERTY = QTextFormat.Property.UserProperty.value + 1
class LatexRenderer(QObject, QTextObjectInterface):
def __init__(self):
super().__init__()
self._cache = {}
self._cache_limit = 128
def intrinsicSize(self, doc, posInDocument, format):
latex = format.property(LATEX_PROPERTY)
image = self.render_latex(latex)
return QSizeF(image.width(), image.height())
def drawObject(self, painter, rect, doc, posInDocument, format):
latex = format.property(LATEX_PROPERTY)
image = self.render_latex(latex)
painter.drawImage(rect.topLeft(), image)
def render_latex(self, latex):
if latex in self._cache:
return self._cache[latex]
fig = Figure(figsize=(1, 1), dpi=100)
FigureCanvas(fig)
fig.patch.set_alpha(0)
fig.text(
0.5, 0.5, f"${latex}$",
fontsize=20,
ha="center",
va="center",
color="black",
usetex=False,
)
with BytesIO() as buf:
fig.savefig(
buf,
format="png",
bbox_inches="tight",
pad_inches=0.1,
transparent=True,
)
image = QImage()
loaded = image.loadFromData(buf.getvalue(), "PNG")
if not loaded or image.isNull():
raise RuntimeError(f"无法加载公式图片:{latex!r}")
if len(self._cache) >= self._cache_limit:
oldest_key = next(iter(self._cache))
del self._cache[oldest_key]
self._cache[latex] = image
return image
128 是示例容量,需要按公式数量和图片大小调整。这里按插入顺序淘汰缓存,并非 LRU。排查时可以保留前面的回调日志。这段修改不能绕过 handler 未生效的问题,也没有在题述环境中实际运行验证。
对应的插入方法也要使用同一个属性编号,并恢复后续文字的格式:
def insert_latex(self, cursor, latex):
previous_format = cursor.charFormat()
fmt = QTextCharFormat()
fmt.setObjectType(LATEX_FORMAT)
fmt.setProperty(LATEX_PROPERTY, latex)
cursor.insertText("\uFFFC", fmt)
cursor.setCharFormat(previous_format)
恢复格式可以避免后续普通文字继续继承公式对象的字符格式,但这同样不应被当作已经确认的根因。
这里默认使用 Mathtext,无须外部 TeX
在 Matplotlib 默认的 text.usetex=False 配置下,f"${latex}$" 使用内置 Mathtext 渲染。Mathtext 支持一部分 TeX 数学语法,不需要安装 TeX distribution。上面的示例显式设置了 usetex=False,便于排查时固定渲染路径。参见 Matplotlib 数学表达式文档。
如果连灰色测试矩形都没有显示,问题尚未定位到公式解析阶段。若矩形正常、公式异常,再检查 Matplotlib 抛出的异常、PNG 加载结果和图片尺寸。
自定义文本对象仍然需要管理缓存
改用自定义对象后,示例不会把每张公式图片添加到 QTextDocument 的图片资源表中,但仍会通过 _cache 保存 QImage。原来的字典没有淘汰逻辑,只要出现新的公式字符串,就会继续增加条目;删除文档中的公式不会自动删除这些条目。
可以使用上面的容量限制,也可以在业务层根据当前文档和撤销历史管理缓存。如果只清理“不在当前可见文本中的公式”,要考虑撤销后公式可能重新出现。缓存被淘汰后可以重新渲染,不必为了保留图片而让缓存无限增长。
如果以后允许调整字号、颜色或 DPI,也应把这些参数纳入缓存键,避免同一个公式字符串复用错误的图片。
Python 和 Qt 绑定应如何选择?
现有信息不足以指定一个能保证解决问题的 Python 版本。目前缺少 PySide6、Shiboken6、Qt、Matplotlib 的具体版本,也没有说明使用哪个 Gemini SDK 和哪个功能。
如果使用的是 google-genai,其官方仓库当前的项目配置声明 Python 要求为 >=3.11,并列出了 Python 3.13。这不能说明某项 Gemini 功能必须使用 Python 3.13;具体要求仍应以实际安装的 SDK 版本及相关依赖为准。参见 Google Gen AI SDK 项目配置。
针对当前情况,可以先保留已能运行 PyQt6 版本的 Python 3.13 环境,并记录成功组合的依赖版本。随后在独立环境中排查 PySide6,减少同时更换 Python 和 Qt 绑定带来的变量。
补充信息中的安装命令为:
python3.13 -m pip install PyQt6 --only-binary=:all:
--only-binary=:all: 要求使用二进制包,不会改变包本身的兼容性。Ubuntu 20.04 上还需要匹配 Python、系统架构和 wheel 的系统要求,因此一次安装成功不能说明所有版本都能安装运行。
如果纯矩形示例在 PySide6 下仍然失败,而相同逻辑在 PyQt6 下正常,可以携带最小复现代码和完整版本信息报告问题。在获得版本对照或缺陷记录之前,应将“PySide6 绑定或版本问题”保留为待验证方向。
备注:内容仅供参考。
