惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

T
Tailwind CSS Blog
The GitHub Blog
The GitHub Blog
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
B
Blog
Microsoft Security Blog
Microsoft Security Blog
Stack Overflow Blog
Stack Overflow Blog
量子位
Martin Fowler
Martin Fowler
月光博客
月光博客
P
Proofpoint News Feed
博客园_首页
Y
Y Combinator Blog
I
InfoQ
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
V
Visual Studio Blog
H
Help Net Security
U
Unit 42
GbyAI
GbyAI
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
博客园 - 司徒正美
MongoDB | Blog
MongoDB | Blog
F
Fortinet All Blogs
罗磊的独立博客
酷 壳 – CoolShell
酷 壳 – CoolShell

博客园 - crockery

13-心一网络助手-XinYiNetTool 11-核痕-KeHen 10-烽燧-FengSui 09-弈局-ChessForge 08-立方之蛇-CubeSerpent 06-纯粹数独-PureSudoku 05-砂漏-Grainfall 04-回响回廊-EchoHalls 03-烬塔-EmberTower 02-胶囊医生-DrCapsule 01-离阵-ArrowExodus 一文看懂--什么是typescript 一个项目从 0 到上线的技术路线图 windows批量部署nodeJS项目 Vue + Node + Nginx 从 0 到上线 github上有哪些star非常多的项目 一篇文章搞懂所有前端后端概念图 【基于智能监控系统的配置问题】1、zabbix部署与配置 【基于智能监控系统的配置问题】2、zabbix如何部署centos和stream9 【基于智能监控系统的配置问题】3、仪表盘的界面设定 【基于智能监控系统的配置问题】4、与grafana的连接问题 【基于智能监控系统的配置问题】5、grafana上遇到的问题 【基于智能监控系统的配置问题】6、如何生成单页的监控界面 【基于智能监控系统的配置问题】7、zabbix如何实现免登录预览监控 【基于智能监控系统的配置问题】8、关于grafana的汉化问题 【基于各种多模态文件的在线预览系统】1、多模态预览 【基于各种多模态文件的在线预览系统】2、部署问题 【基于各种多模态文件的在线预览系统】3、本地磁盘挂载虚拟机 【基于各种多模态文件的在线预览系统】4、其他值得推荐的应用 【基于各种多模态文件的在线预览系统】5、出现无法访问的情况
12-奥赛题库-OlympiadMath
crockery · 2026-08-25 · via 博客园 - crockery

Tkinter 里怎么把 LaTeX 公式和中文排在同一行——奥赛题库 OlympiadMath

本系列:把这两年用 Python 写的十几个小项目挨个开源、挨个说清楚。不讲"我做了什么",讲"做的过程中被什么打脸、最后怎么想明白的"。
第 12 篇 · 奥赛题库 OlympiadMath(上一篇:核痕 KeHen
源码:github.com/ghostgorge/OlympaidMath
Python + Tkinter 桌面程序,单文件 exe,小/初/高三档共 72 道题


一、先说这是个什么东西

小学 / 初中 / 高中三档数学奥赛题库,公式用 LaTeX 显示,全部题目笔算即可(不需要计算器、不需要编程),每题给提示 / 思路 / 完整解答 / 答案四层帮助。

学段 题数 难度分布 覆盖专题
小学 24 ★1 / ★★11 / ★★★9 / ★★★★3 巧算、行程、工程、数论、周期、抽屉、计数、几何、数阵图、逻辑推理、最值
初中 24 ★★9 / ★★★10 / ★★★★4 / ★★★★★1 代数变形、数论、平面几何、组合、不等式
高中 24 ★★5 / ★★★10 / ★★★★6 / ★★★★★3 三角、数列、复数、函数方程、数论、组合几何、立体几何、解析几何、概率、不等式

难度对照:★★ 及以下是热身/课内拔高,★★★ 是联赛一试水平,★★★★ 是二试水平,★★★★★ 是 IMO / 二试压轴(比如 IMO 1990-3、IMO 1995-2、Sylvester–Gallai)。

界面默认只显示题面,提示、思路、解答、答案全部要手动展开。这是刻意的——先动笔,再看。一个把答案默认摊开的题库,会让人不知不觉变成"看解答"而不是"做题"。

二、这个项目真正的技术活:中文和公式排在一行

Tkinter 没有公式排版能力。常见的几种做法都有硬伤:

做法 问题
整段丢给 matplotlib 渲染成图 中文要给 matplotlib 配中文字体(打包后经常缺);整段变成图,不能选中、不能复制、不能自动换行
嵌一个浏览器控件跑 MathJax 拖进来一个几十 MB 的运行时,单文件 exe 直接废掉
公式退化成纯文本 稍微复杂一点就没法看

我的方案是混排,一句话概括:

中文不进 matplotlib。

具体做法:

  1. 整行文本按 $ 切开
  2. 中文和普通字符用 Tk 原生字体直接写进 Text 控件——可选中、可复制、自动换行
  3. 只有 $...$ 里的公式交给 matplotlib 的 mathtext 光栅化,贴成行内图片
  4. 用 mathtext 返回的 depth 和当前字体的 descent基线对齐

第 4 步是关键。行内公式如果不做基线对齐,分数线和汉字底部会错开一两个像素——看起来说不出哪里怪,但就是很怪

这个方案的连带好处:

  • 不需要 matplotlib 去找中文字体(打包后最容易出问题的一环直接绕开)
  • 图片只包含公式,体积小、缓存命中率高(同一个公式在提示和解答里出现两次,只渲染一次)
  • 不依赖 Pillow——自己拼 PNG 转 Tk PhotoImage,少一个依赖

三、mathtext 只支持 LaTeX 的一个子集

matplotlib 的 mathtext 不是完整的 LaTeX,很多常见写法它不认:\ge\le\pmod\frac1x(不带花括号)、\underbrace\displaystyle\tfrac\bigl……

两个选择:要求题库按 mathtext 的规矩写,或者写一个兼容层

我选了后者(app/latexfix.py),理由很实际:题库是要给人写的,而人会按正常 LaTeX 习惯写。 一个"必须记住哪些命令不能用"的题库格式,会在第 10 道题的时候被放弃。

兼容层的成本是一次性的,而"要求所有人记住限制"的成本是每次都要付的。

四、降级要能降得下去

matplotlib 装不上也能跑——公式会退化成可读的纯文本((1)/(2)√(3) 这种),程序不崩。

这条看起来是小事,但它决定了这个 exe 能不能随手发给一个不懂技术的人。一个"缺了某个库就白屏"的工具,等于要求每个用户都会配环境。

五、打包这一节,可能是全系列最有复用价值的

这个项目的 build_exe.bat 和 spec 里预先规避的坑,几乎是我这两年所有 PyInstaller 经验的合集。挨条列一下:

关于 matplotlib(这条是这个项目独有的)

MPLCONFIGDIR 必须固定到 %APPDATA%\OlympiadMath\mplcache
否则单文件 exe 每次启动都要重建 matplotlib 字体缓存,首次渲染公式要等十几秒——用户会以为程序卡死了。

单文件 exe 每次运行都解压到一个新的临时目录,matplotlib 找不到上次的缓存,于是每次都重建。这个问题只在打包后出现,源码运行时完全正常。

关于 pywin32

spec 里排除 pythoncom / pywintypes / win32com / win32api,并用 pyi_hooks/ 里的空 hook 覆盖官方 hook。原因:机器上 pywin32 装了但没跑 postinstall 时,官方 hook 会在 Analysis 阶段直接崩(报 Failed to retrieve attribute __file__ from module pythoncom),跟你的程序毫无关系

关于 excludes 的边界

不能排除标准库和 setuptools——PyInstaller 的运行时 hook 依赖它们,排掉后 exe 一启动就崩在 pyi_rth_pkgres.py
excludes 只用来排 matplotlib/scipy/PyQt 这类重型第三方库。

关于"双击没反应"

console=Falsesys.stdout 可能是 None,入口处任何一句 print() 都会 AttributeError——又一种"双击没反应"。所以入口装了黑洞流兜底,启动全程包在 try 里,出错写日志(%APPDATA%\OlympiadMath\startup.log)并弹 MessageBox。

排查手段是打一个 build_exe_debug.bat(onedir + 带控制台):能跑而单文件不能 → 问题在解压到 %TEMP%;调试版也挂 → 控制台直接给 traceback。

关于挑解释器和装依赖

py -3.12 → 3.11 → 3.9 → 3.8 → python 的顺序挑(PATH 里的 python 通常是 Anaconda,排最后),可用 set OM_PY= 覆盖。

依赖判断import 而不是 pip 的退出码——某些机器上 pip 会因为坏掉的包元数据直接崩溃(比如 pyodbc 的版本号写成 4.0.0-unsupported,不合 PEP 440,导致该环境下任何 pip 命令在"列出已安装包"阶段就退出码 2),跟你的项目毫无关系。

装依赖先试 --user 再试普通安装——python 装在系统目录、当前账号没写权限时会报 WinError 5。失败了给出三条能照着做的修法,而不是甩一段 traceback。

关于产物校验

打完校验体积,低于 8MB 直接判失败。(这条的来历见烬塔那篇.bat 收尾横幅里一个裸的 > 被 cmd 当成重定向,把刚打好的 exe 覆盖成了 11 字节文本。)

六、一个很划算的自检项

打包前的题库自检里有一条:真的光栅化一个样例公式。

它守的不是题库,是依赖——matplotlib 换个版本、mathtext 的接口变了,这条会在打包之前就红,而不是等到用户双击 exe、点开第一道题、发现公式全变成红叉。

凡是依赖第三方渲染/解析的功能,自检里要有一个真正跑通端到端的样例。 只检查"能 import"是不够的。

自检也能在程序里跑(帮助 → 题库自检),它会把所有渲染不了的公式列出来——所以自己加题的人不用等报错,写完直接自检一遍。

七、其他功能

  • 筛选:学段 / 专题 / 难度 / 状态四个维度 + 全文搜索(搜题面、标题、标签、思路)
  • 组卷:按学段和难度区间随机抽 N 题,生成"题目卷 + 解答卷"两个文件。可选 Markdown 或可打印 HTML——HTML 版能留答题空白,公式用 MathJax 渲染,直接 Ctrl+P 打印
  • 错题本:标记错题后,「练习 → 只看错题」,或组卷时把抽题范围选成"错题",就是一份错题重做卷
  • 笔记:每题可以记自己的做法和卡住的地方
  • 自己加题:把 .json 丢进 %APPDATA%\OlympiadMath\我的题库\,按 F5 就加载进来,和内置题库一起筛选、组卷

做题记录存在 %APPDATA%\OlympiadMath\progress.json——换版本、重新打包都不会丢

快捷键:←/→ 翻题 · R 随机 · H/I/S/A 提示/思路/解答/答案 · F 收藏 · 1/2/3 标记已掌握/待复习/错题 · Ctrl+P 组卷 · F5 重载题库

pip install matplotlib
python main.py
python main.py --selftest    # 题库结构 + 全部公式能否渲染
双击 build_exe.bat           # 出 dist\OlympiadMath.exe

尾巴

两条带得走的:

一、"哪部分交给谁"比"用什么库"更重要。
公式排版这件事,整段丢给 matplotlib 是最省事的写法,但它同时把中文字体问题、可选中、自动换行全都赔进去了。$ 切开、各交各的,才是那个刚好的边界。

二、兼容层的成本是一次性的,"要求所有人记住限制"的成本是每次都要付的。

另外,这篇里的打包清单如果你在用 PyInstaller,建议单独存一份——尤其 MPLCONFIGDIR 那条,它只在打包后出现、只表现为"首次渲染要等十几秒",很难从现象反推到原因。

📦 源码https://github.com/ghostgorge/OlympaidMath


下一篇:心一网络助手 XinYiNetTool——Python/tkinter 网络运维工具箱。回复「继续」。