/2,240 字/阅读约 8 分钟

20.4 MB → 1.72 MB:把一个 Windows 工具从 tkinter 重写成 Win32 原生控件

一个记录"你打开过哪些应用"的桌面小工具,Python 版功能最全但体积 20.4 MB、常驻 5-8 MB。这一轮用 Rust + Windows 公共控件重写了一个原生版:exe 1.72 MB、托盘常驻约 4.5 MB。重写不是翻译——列表虚拟化与"运行中自动隐藏"、图标的三种形态、项目管理扫描、以及把工具变成"可以被 Agent 调用的接口",每一步都是跟平台重新谈条件。

眠月眠月
joker1point

项目:joker1point/ExeTrace(MIT)。本文对应原生版 v1.1.0(2026-10-16 发布),环境:Windows 11。

这个工具解决的问题很小:帮你找回"刚才那个软件"在哪。它后台记录你打开过的每一个应用,随时可以一键把它钉到桌面。技术上没有任何高深的地方,它甚至不需要联网。

但它有一个持续了三周的矛盾:Python 版的界面用的是 tkinter——功能最全、迭代最快,代价是单文件 20.4 MB、后台常驻 5 到 8 MB。对于"想让它静静地待在托盘里"的工具型软件,这个体量有点违和:用户下载一个 20 MB 的 exe,只为了找一个 200 KB 的程序的位置。

于是有了这一轮重写:Rust + Windows 公共控件,不引入任何 UI 框架。目标写得很直白:exe 不超过 6 MB、前台内存不超过 40 MB、常驻不超过 15 MB。

一、为什么是"没有 UI 框架"

第一版尝试用的是 egui(Rust 生态里最顺手的立即模式 GUI)。它能跑,但在"小工具"这个定位下不划算:立即模式框架自带一套渲染与状态基线,实测体积 6.8 MB、内存约 120 MB——对"静静待在托盘"的诉求来说,那 120 MB 全是框架的固定开销,跟业务代码没关系。

第二版换掉了框架,直接用 Windows 自带的公共控件:列表、输入框、按钮、下拉框、状态栏。没有渲染引擎、没有布局系统,界面就是操作系统的那套控件。

两个实现共用同一份 SQLite 历史库:Python/tkinter 版 v3.1(20.4 MB、后台 5-8 MB)与 Rust/Win32 原生版 v1.1.0(1.72 MB、托盘 4.5 MB、窗口 13 MB)
图 1 · 两个实现,同一份历史库:数据互通、不建议同时常驻(会双重记录)

最终的实测数字(本机 2026-10-16 实测):exe 1.72 MB、托盘常驻约 4.5 MB、窗口打开约 13 MB。三个目标全部达标,其中常驻内存只有 Python 版的六成,体积是十二分之一。

值得单独说一句:两个实现共用同一份 SQLite 历史库(schema 一致),所以这不是"新版本替换旧版本",而是"两个形态"——Python 版继续保留全部功能与最快的迭代速度;原生版给"想要更轻"的用户。唯一要提醒的是别同时常驻:两份记录会互相打架。

二、重写不是翻译:四个"长得像、做起来完全不同"的地方

功能清单可以照抄,实现路径全部要重谈。四个例子:

列表与"运行中自动隐藏"。 原生列表控件不会替你虚拟化数据,行数就是性能。而在功能上,这一版新增了一条反直觉的规则:正在运行的应用从列表里隐藏。理由是这个工具的使命是"找回还没找到的"——你已经打开的东西,不需要它来告诉你路径。实现上按"进程在跑"判定(以 exe 路径确认,路径读不出来时按同名兜底),对列表做 3 秒节流复核,状态栏标出隐藏数量。验收时的一笔账很能说明问题:近 30 天的记录 78 条 = 显示 58 + 隐藏 20,隐藏的那 20 条正是此刻开着的。

原生版项目视图真实截图:本机 git 仓库按自有、克隆、本地分档列在同一列表里
图 2 · 项目管理(真实界面):扫本机 git 仓库收进同一列表,按「自有 / 克隆 / 本地」分档

项目管理。 这是"列表复用"带来的意外收获:既然能记录应用,为什么不记录项目?于是加了一个扫描本机 git 仓库的入口——列表顶部切换"应用 / 项目",项目行支持双击打开文件夹、右键打开 GitHub 页、一键更新。两个细节值得记:打开 GitHub 页之前要从仓库配置里剥掉凭据(远程地址里可能带 token);路径扫描默认只覆盖文档、工作目录、桌面这几个根,深度有限制,避免对着整个磁盘做遍历。

图标的三种形态。 一个 exe 图标要出现在三个地方:资源管理器里的文件图标(编译进 PE 资源段)、窗口与托盘的运行时图标(从可执行文件里提取 HICON)、以及界面列表里的 PNG。最后一步的转换交给系统图形接口完成缩放、掩码与透明合成,而不是自己写像素处理——平台成熟的路径往往比"自己控制每一步"更省事。

交付形态的"隐形需求"。 单实例唤醒(双击第二次时让已有窗口浮起来)、全局热键、托盘、以及一个非 GUI 自检模式:打包成窗口子系统后没有控制台,自检结果只能写进文件再回读——验收要打在交付形态上,而不是开发形态的终端里。

三、把"能交给 Agent"做成一等接口

这一版最像"新产品"的部分,是数据出口。工具本身不做 AI 相关的事,但它把"数据和动作"都开了一个机器可读的口子,任何 Agent 都能直接拿它用:

面命令输出
读--export [路径]全量 JSON:应用列表 + 项目列表(schema 版本化)
读--find <关键词>stdout JSON;退出码 0 表示有结果、1 表示无结果
做--launch <路径>打开应用或文件夹
做--make-shortcut <路径>钉到桌面(成功后读回校验)
交接--agent-guide生成一份随本机路径的接口说明文本,整段转发给 Agent 即可
ExeTrace.exe --find chrome 的真实 stdout JSON:应用条目含名称、路径、打开次数、来源
图 3 · 查询接口的真实输出(JSON 截断、用户名路径已打码):同样的内容 Agent 直接可读

这份接口里最容易忽略、也最值得写进文档的是一条契约:这是图形子系统的 exe,在 PowerShell 里直接以调用运算符执行时不会等它跑完——命令立刻返回,文件还没生成。正确的调用姿势是经由命令解释器执行、或者显式等待进程,并且以产物文件为准(标准输出可能晚到)。这条契约是从真实踩坑里写出来的:自动化脚本里遇到过"命令成功了但文件不存在"的假失败。

四、两个实现的边界怎么划

Python 版主界面真实截图:应用历史列表(应用、位置、分类、打开次数、使用时长)与底部操作区
图 4 · Python 版主界面(真实截图):功能最全、发布版本仍走它的下载入口

发布策略也按"两个形态"对齐:原生版走 README 的直达链接,下载按钮背后的"最新版"标签仍然留给 Python 版——不是为了保守,而是因为两个实现的功能集不是严格包含关系(原生版有"运行中隐藏""项目管理",Python 版有更完整的分类与统计显示)。标签指向哪一边,取决于用户更需要什么;把它写清楚,比"最新即最好"更诚实。

多步交互的功能照旧用真机录制动图来展示(搜索到钉桌面的完整流程),这也是这个项目一贯的交付标准:宣称的能力要有真实运行的画面。

真机操作录制:搜索过滤列表、右键菜单、一键钉到桌面的完整流程
图 5 · 一键钉到桌面(真机操作录制):搜索 → 右键 → 创建,创建后立即读回校验

五、关于验证:把它当"交付物"而不是"代码"来验

这一轮重写顺带把验证也收紧了三层:

  1. 自检成为命令:非 GUI 自检覆盖路径模型、数据库读写与去重、注册表回填扫描、图标提取、噪音过滤、窗口枚举等 33 项,源码形态与打包形态各跑一遍。
  2. 端到端走真实数据:本机实测 361 个应用、58 个项目,验证查询/启动的成败分支与交接文本的生成。
  3. 推送即门禁:仓库挂了工作流,每次 push 自动跑语法检查、非 GUI 自检、README 素材规范与对外材料扫描;原生版另有一个编译检查任务。绿了才谈发布。

20.4 MB 到 1.72 MB,缩的不只是体积——是"这个工具要不要活得像个系统组件"这个问题的答案。功能可以慢慢加,但一个常驻小工具的内存与体积,是它每天都要替你付的账。