把本机装的所有字体,一次看全。
Windows 自带的字体查看器只会列文件名。想知道「这个字体排版出来长什么样」「这台机器到底装了多少中文字体」「注册表里那条指向已删除文件的僵尸登记怎么清掉」,它都帮不上忙。find-font 把系统里每个字体按真实字形渲染成样本,让你像翻样本册一样浏览、搜索、对比、安装与卸载。
三个具体痛点:
- 看不见字形。 字体名是「方正准圆简体」,但它到底圆到什么程度、中英文混排时基线对不对,只有渲染出来才知道。
- 分不清语言归属。 系统里同时装着西文、中文、日文、韩文上百套字体,靠名字猜和靠眼睛翻是两回事。
- 有僵尸登记。 卸载软件时常留下「注册表说有这个字体、文件却已不存在」的条目,普通界面里看不见,也不容易删。
find-font 对这三件事的处理分别是:全部字体离屏渲染成位图、按名字关键词推断语言并计数、把失效登记单独标出来并支持一键清理(只删注册表项,不动任何字体文件)。
| 功能 | 说明 |
|---|---|
| 全字体预览 | 本机 1300+ 个字体逐个按真实字形渲染,中英文混排,滚动即见 |
| 实时搜索 | 300 ms 防抖,匹配显示名 / 本地化名 / 中文名 / 家族名 / 文件名 / 样式名,任意大小写 |
| 分类与风格筛选 | 全部 / 中文 / 西文 / 其他,全部 / 常规 / 粗体 / 斜体,计数随筛选实时刷新 |
| 四种排序 | 按名称 / 按最近修改 / 按文件大小 / 按语言,排序方式进设置,下次打开照旧 |
| 命中高亮 | 搜索结果的元数据行下方单独一行,命中片段用品格红加下划线标出,一眼看出为什么它被搜出来 |
| 多字体对比 | 按住 Ctrl 选 1–3 个字体同屏对照,改对比文字立刻重渲染 |
| 自定义样本文字 | 顶部输入框改一次,整个列表与对比窗口同步 |
| 安装 / 卸载 | 装「给我」(写 HKCU + %LOCALAPPDATA%\Microsoft\Windows\Fonts,不需要管理员权限)或装「所有人」(写 HKLM + 系统字体目录,走 UAC 提权微进程);卸载时只删本工具装过的文件(进回收站),系统自带字体只清登记 |
| 可变字体坐标轴 | 读 fvar 表,把 wght 100–900、wdth 75–100 这类轴写进右栏「档案」;静态字体不显示这一段 |
| 嵌入许可位 | 读 OS/2 fsType,在「字面」一行标出「禁止嵌入 / 仅可预览与打印 / 可安装(允许嵌入) / 仅位图嵌入」,并注明是否禁止子集化;只提示不阻断,装不装由你和授权方定 |
| 拖放安装 | 把字体文件拖到窗口任意位置即可安装;拖拽悬停时就按「有没有能装的格式」切换光标,装不了的文件会说明原因 |
| 多字面容器 | .ttc/.otc 里每个字面各占一行,标「第 2/3 面」,simsun.ttc 的宋体与新宋体都能单独预览 |
| 字形缺失检测 | 拿当前样本文字去每个字体里逐个码位核对,分「cmap 里没有这个字」与「有映射但字形是空的」两类报出来,并列出缺了哪几个字 |
| 拒装网页字体 | .woff/.woff2/.eot 是浏览器格式,装进系统目录也看不见,直接明说拒绝原因而不是假装成功 |
| 失效登记清理 | 删除注册表里指向缺失文件的条目,不删除任何字体文件 |
| 单实例 | 重复双击不会开出第二个窗口,而是把已有窗口叫到前台 |
| 记忆上次状态 | 窗口位置 / 大小 / 上次搜索词 / 样本文字 / 字号 / 分类 / 风格都记在 %LOCALAPPDATA%\find-font\settings.json |
| 高分屏清晰 | 声明 PerMonitorV2 DPI 感知,位图按屏幕真实 DPI 渲染(100% / 150% / 200% 物理尺寸一致) |
| 出问题有据可查 | 未处理异常自动落盘到 %LOCALAPPDATA%\find-font\logs,保留 7 天;「关于」里一键复制环境信息 |
| 键盘优先 | Ctrl+F 搜索、Esc 逐层退出、Enter 直接对比 |
- Windows 10 1803 及以上,或 Windows 11
- .NET 8 桌面运行时(Desktop Runtime)
到 Releases 下载 find-font-<version>-win-x64.zip,解压到任意目录,双击 FindFont.exe。绿色版,不写系统目录、不需要安装程序。
压缩包里只有一个文件:FindFont.exe(约 1 MB)。没有第二个 exe、没有附带 dll、没有安装程序,解压出来双击它就能用。
没装 .NET 8 桌面运行时时,宿主会弹出系统对话框说明缺什么并给出下载链接,而不是静默失败。
| 操作 | 怎么做 |
|---|---|
| 搜索 | 顶部「搜索」框直接输入,或按 Ctrl+F |
| 筛选 | 左栏点分类行;字重是四个互斥方块;「只显示」里失效 / 缺映射 / 空字形 / 正常各收窄一类 |
| 看细节 | 单击任意一行,该行展开成大样本,右侧「档案」列出家族、本地化名、样式、字重、文件、路径、语言、状态 |
| 对比 | 按住 Ctrl 选 1–3 个字体后点「对比选中」,或直接双击某一行 |
| 改样本 | 顶部「样本文字」框,全局生效 |
| 换字号 | 右上角字号下拉(20–88) |
| 安装 | 「从文件安装」选 .ttf/.otf/.ttc/.otc,会弹 UAC;取消 UAC 视为放弃,不留半成品。选到 .woff/.woff2/.eot 会被直接拒绝并说明原因 |
| 卸载 | 选中后点「卸载字体」,同样走 UAC。本工具装过的文件进回收站;系统自带字体只清除登记、不删除文件,确认框会写清是哪一种 |
| 清理 | 「清理失效登记」只删注册表项 |
| 退出搜索 | Esc 清空搜索词,再按一次取消选中 |
| 对比窗口 | Enter,或双击列表行 |
默认 1360×880(最小 1080×700),三栏:索引栏 208px、样本瓦片网格、选中时出现的「档案」面板 300px。
字体列表是多列瓦片网格而不是单列长条:瓦片宽由列表实际可用宽度算出并向下量化到 32px 桶,列数 1–3 随宽度收敛(右栏展开时列表变窄,会主动退回两列而不是把第三列挤出血)。窗口缩放到 1080 宽时仍是两列,700 宽以下退成一列。列宽量化是因为每变一次尺寸就要重新渲染可见区,量化后拖动窗口只会落进有限几档。
视觉上只有一套灰阶 + 一个强调色:纸灰底 #ECEDEF、面板 #F6F7F8、墨色 #101215、发丝线 #CDD1D6,以及品红 #C4005E。品红是校样标记,全界面只允许出现在两处:我正在看的这一个(选中瓦片左侧那条 48px 高的 2px 裁切线)与这几个字有问题(缺字的示例字符)。其余状态一律降级成安静的文字:面数用 #646A72、失效用暖褐 #7A4A16,都不铺底色药丸——到处都是强调色时,没有任何东西是强调。
界面字体是 Constantia, Georgia, Cambria, serif(标题、面板标题)与 Segoe UI(控件、正文)。Constantia 比 Cambria 更收敛,衬线只在标题上出现一次;正文全部无衬线,14–15px。全部无圆角,靠 1px 发丝线分层而不是卡片阴影。所有间距落在 4px 网格上(24 版心 / 16 栏内 / 8 段间 / 4 行距),索引栏七个动作的图标槽统一 16px,标签因此右对齐成一列。
瓦片样本区固定 96px 高、字形垂直居中,选中不做高度动画(多列网格里行长高会连带整行重排),只把底色抬到 #FFFFFF 并在左侧点亮一条 2px 品红裁切线——选中状态在一个 340px 宽的瓦片里足够醒目,不值得为它牺牲滚动稳定性。筛选结果逐行错开入场(每行延迟 12 ms、封顶 180 ms),输入框聚焦、下拉框展开时底边从中心长出一条 2px 品红标尺。下拉框不再沿用系统默认的灰渐变箭头:它和输入框是同一张纸(1px 发丝边、2px 圆角、右侧一枚细 chevron),展开项也是同一套「2px 品红标尺 + 浅底」的选中语言,悬停与选中只动 Opacity。
左栏是四段式索引(分类 / 字重 / 只显示 / 排序)加动作区,状态栏钉在底部:滚动区和动作区互不挤压,动作区放不下时自己滚而不是把按钮裁掉。右栏「档案」按语义分三段(标识 / 文件 / 覆盖与状态),段之间用发丝线过渡,不再靠每个字段各留一截空白硬隔。索引栏与档案面板的 XAML 抽在 Themes/Rail.xaml、Themes/Colophon.xaml,MainWindow.xaml 只留三栏骨架。
窗口材质走 DWMWA_SYSTEMBACKDROP_TYPE(Win11 22H2+),旧的 DWMWA_MICA_EFFECT 与 Win10 的 SetWindowCompositionAttribute 逐级回退,全部失败时留在不透明纸色上;材质生效时内容层铺 #F0ECEDEF 半透明纸色,保证正文对比度不随壁纸变化。所有动画都读 SystemParameters.ClientAreaAnimation——系统关掉动画即全部退化为瞬时切换。
样本文字只用离屏渲染出的位图,XAML 里不会出现 FontFamily 绑定(见下节),因此界面字体与字体列表渲染互不影响。
这些约束来自一次踩坑记录,每条都对应一个真实的崩溃、卡顿或内存事故。
| 问题 | 方案 | 代码位置 |
|---|---|---|
绑定 FontFamily 会让 WPF 加载并常驻上千套字形,内存暴涨 |
XAML 里禁止出现 FontFamily 绑定;改用 FormattedText + RenderTargetBitmap 在独立 STA 线程离屏出图,缓存为冻结的 BitmapSource |
Services/FontRenderCache.cs、Converters/FontToImageConverter.cs |
| 上千个字体逐一解析会卡住界面 | 先同步读注册表立即出列表,再后台异步补全本地化名;补名直读 sfnt 表,约 0.44 ms/文件 | Services/FontScanner.cs、Helpers/FontMetadataReader.cs |
| 滚动时反复渲染导致掉帧 | VirtualizingStackPanel + VirtualizationMode=Recycling + CacheLength=2,1;瓦片样本按 96 MB 字节预算做 LRU 淘汰,大样本只在对比窗口里按需请求 |
Views/MainWindow.xaml、Themes/Templates.xaml |
| 多列网格里每张瓦片都按自己的宽度渲染,拖窗口会产生无数种尺寸 | 瓦片宽度量化到 32px 桶,列数按列表实际可用宽度收敛到 1–3 列,尺寸变化只在跨桶时发生一次 | ViewModels/MainViewModel.Layout.cs |
| 把「同一字体有更新请求」的旧渲染直接取消,会让被取消的那张图永远回不到界面 | 只尊重调用方显式传入的取消令牌,不再按请求序号丢弃;重复渲染交给在途去重与位图缓存 | Services/FontRenderCache.cs |
| 输入每个字符都过滤一次会卡顿 | 300 ms 防抖 + 单遍扫描同时产出结果与四个分类计数(ReplaceAll 只发一次 Reset)+ 预拼小写 SearchHaystack |
ViewModels/MainViewModel.cs |
| 安装/卸载需要管理员权限,但主程序不该整体提权 | 主进程普通权限,Verb=runas 拉起同一个 exe 的微进程只做文件与注册表操作 |
Services/ElevationHost.cs |
| 注册表指向不存在文件的条目会让扫描崩溃 | 缺失文件生成占位项(品红标记、可一键清理),解析失败静默捕获 | Services/FontScanner.cs |
| 卸载若直接删文件会误删系统自带字体 | 维护一份「本工具装过什么」的台账(%ProgramData%\find-font\installed.txt),台账里没有的文件一律只清登记;删除走回收站,回收站不可用就报错而不是硬删 |
Services/FontInstaller.cs |
| 发布包里两个 exe 让人不知道该点哪个 | 只发一个 FindFont.exe:程序集名就是入口名,单文件发布把托管与 WPF 依赖都打进同一个文件,build.cmd publish 末尾校验目录里除了它没有别的文件 |
src/FindFont.App/FindFont.App.csproj、build.cmd |
| 动效写在 XAML 里,用户关了系统动画却还在播 | 全部时长从 Helpers/Motion.cs 取,读 SystemParameters.ClientAreaAnimation,关闭时返回 TimeSpan.Zero |
Helpers/Motion.cs |
| 重复双击会开出两个窗口、各自扫一遍注册表 | 全局命名互斥体 + EnumWindows 找已有窗口叫到前台;拿不到互斥体时降级为允许启动,绝不让程序打不开 |
Helpers/SingleInstance.cs |
| 窗口大小、搜索词每次重启都丢 | System.Text.Json 存 %LOCALAPPDATA%\find-font\settings.json,变更后 500 ms 合并写;先写 .tmp 再原子替换,避免断电留半截 JSON |
Services/AppSettings.cs |
| 高分屏上样本偏小发虚 | app.manifest 声明 PerMonitorV2;位图按 VisualTreeHelper.GetDpi 的真实 DPI 渲染,DPI 进缓存键,跨屏拖动不会命中旧 DPI 的模糊图 |
app.manifest、Services/RenderDpi.cs |
| 崩溃后用户什么都说不清 | 未处理异常统一落盘到 %LOCALAPPDATA%\find-font\logs(不可写则依次退到 %TEMP%),保留 7 天;「关于」一键复制环境信息 |
Helpers/Diagnostics.cs |
| 「字体里有几个字是空的」和「字体里没这个字」在界面上看起来一样,都是空白 | 分开报:cmap 查不到码位记「无映射」,查到了但 loca/glyf 的轮廓长度为 0 记「空字形」;CFF 轮廓没有 loca,只报无映射并标注不支持,不猜 |
Helpers/GlyphCoverageReader.cs、Models/GlyphCoverage.cs |
更多取舍(为什么直读 sfnt 而不是 GlyphTypeface、为什么用 Motion.Hold 而不是 FillBehavior.Stop)写在 .docs/PLAN.md。
┌────────────────┐ 注册表(HKLM/HKCU × 32/64)
│ FontScanner │──────────────┐
└────────────────┘ │ 同步:立即出列表
│ 异步 ▼
│ ┌──────────────────┐
┌────────▼────────┐ │ MainViewModel │
│ FontMetadata- │ │ 搜索/筛选/多选 │
│ Reader(sfnt) │───────▶│ 分类计数/档案 │
└─────────────────┘ 补名 └────────┬─────────┘
│ 请求位图
┌───────▼─────────┐
│ FontRenderCache │
│ STA 渲染线程 │
│ LRU + 字节预算 │
└───────┬─────────┘
│ 冻结 BitmapSource
┌───────▼─────────┐
│ Image(XAML) │
└─────────────────┘
技术栈:C# 12 / .NET 8 / WPF / MVVM(CommunityToolkit.Mvvm)/ 无第三方 UI 库。
写入路径(安装、卸载、清理)不走上述链路,而是 FontInstaller → ElevationHost → runas 起同一个 exe 带 --install-font 之类参数运行 → 主程序在 App.OnStartup 里识别到操作请求就执行并立即退出。这样提权面被压缩到一个只做文件与注册表操作的分支。
本机
%TEMP%\MSBuildTemp、%USERPROFILE%\.templateengine、NuGet v3-cache 均不可写,build.cmd会把TMP/TEMP/NUGET_PACKAGES/DOTNET_CLI_HOME全部重定向到仓库内的.build、.nuget目录。
build.cmd :: 编译
build.cmd test :: 跑冒烟测试(扫描真实系统字体并实际渲染位图)
build.cmd run :: 直接启动
build.cmd publish :: 发布到 publish\ 并打包 dist\find-font-<version>-win-x64.zip手动构建等价于:
set TMP=%~dp0.build\tmp
set NUGET_PACKAGES=%~dp0.nuget\packages
set DOTNET_CLI_HOME=%~dp0.build\home
dotnet build FindFont.slnx -c Debugbuild.cmd 本身必须是纯 ASCII:cmd.exe 用 OEM 代码页解析批处理,UTF-8 中文注释会被拆成乱码命令,进而让 %TEMP% 重定向失效。
assets/find-font.ico 应用图标(16–256 七档)
src/FindFont.App/
app.manifest PerMonitorV2 DPI 感知、longPathAware、最高支持的 OS
Models/FontItem.cs 字体条目(BestName 回退链、SearchHaystack、小/大样本两个位图槽)
Models/FacetItem.cs 左栏分类索引行(名称固定、计数就地更新)
Collections/RangeObservableCollection.cs 整批替换只发一次 Reset
Helpers/NativeMethods.cs AddFontResourceEx / RemoveFontResourceEx / WM_FONTCHANGE 广播
Helpers/FontMetadataReader.cs 字体元数据入口(sfnt 优先,GlyphTypeface 回退)
Helpers/FontMetadataReader.Reading.cs 一次打开读完整个文件的全部字面
Helpers/SfntReader.cs sfnt/ttcf 容器、表目录、big-endian tag
Helpers/NameTableParser.cs name 表记录与本地化名评分
Helpers/LanguageInference.cs 按字符覆盖位判 CJK 归属,缺表时回落到名字关键词
Helpers/VariationAxesParser.cs 读 fvar 轴,输出 "wght 100–900" 这样的可读串
Helpers/CmapReader.cs cmap 子表选择与 fmt 0/4/6/12 查表
Helpers/GlyphCoverageReader.cs 逐码位核对:cmap 有无映射 + glyf 轮廓是否为空
Models/GlyphCoverage.cs 缺字结论(四类 + 缺失字符清单)
Helpers/Motion.cs 三档时长 + 遵循系统动画开关
Helpers/WindowBackdrop.cs Mica / Acrylic / 降级,三级回退
Helpers/SingleInstance.cs 命名互斥体 + 唤起已有窗口
Helpers/Diagnostics.cs 崩溃日志(可写目录回退链、保留 7 天)
Helpers/StartupTrace.cs 启动打点:从进程启动到首屏各阶段毫秒数
Services/FontScanner.cs 扫描流水线编排:注册表 → 文件探测 → 构建条目
Services/FontRegistryReader.cs 并行读 HKLM/HKCU × 32/64 四种视图,输出顺序固定
Services/FontFileIndex.cs 按文件并行探测(一次打开拿全部字面 + 大小 + 时间)
Services/FontItemBuilder.cs 探测结果 → FontItem,含坏字体与旧式 .fon 的分流
Services/FontDeduplicator.cs 同文件同家族的合并规则
Services/FontRenderCache.cs 字节预算 LRU + 并发去重 + 过期请求丢弃 + 线程池调度
Services/FontRenderCache.Pool.cs 2–4 条 STA 渲染线程,按缓存键哈希分派
Services/FontRenderCache.Rendering.cs FormattedText / RenderTargetBitmap / 每线程 Typeface 缓存
Services/FontInstaller.cs 安装 / 卸载 / 清理失效登记(全用户,提权进程执行)
Services/FontInstaller.UserScope.cs 仅当前用户的安装路径:HKCU + 用户字体目录,不提权
Services/FontInstaller.Registry.cs 注册表值名与读写(hive 由调用方指定)
Services/ElevationHost.cs --install-font 等参数协议的微进程宿主
Services/AppSettings.cs settings.json 读写、窗口几何、屏幕可见性校验
Services/RenderDpi.cs 当前监视器 DPI 的来源
Services/FontCoverageIndex.cs 字形覆盖探测:并行 + 按(路径、字面、探测集)缓存
Converters/ FontToImage(MultiBinding → BitmapSource)、可见性转换器
Themes/Tokens.xaml 色板、字体、尺寸常量(唯一配色来源)
Themes/Controls.xaml 聚合器:Inputs / Buttons / Rows 三份
Themes/Templates.xaml 瓦片模板(FontTileTemplate)与行模板(FontRowTemplate)
Themes/Rail.xaml 左栏索引:分类 / 字重 / 只显示 / 排序 / 动作 / 状态
Themes/Colophon.xaml 右栏档案:标识 / 文件 / 覆盖与状态三段
Themes/Icons.xaml 8 个 16px 线图标几何与图标槽样式
Themes/Scrollbars.xaml 4px 细滚动条
ViewModels/MainViewModel.cs 状态字段、集合、计算属性
ViewModels/MainViewModel.Filtering.cs 扫描加载、防抖搜索、筛选、分类计数
ViewModels/MainViewModel.Preview.cs 首屏预热与预览位图生命周期
ViewModels/MainViewModel.Layout.cs 多列分行、瓦片宽度、瓦片选中
ViewModels/MainViewModel.Coverage.cs 缺字检测的防抖、批量量测与结果回填
ViewModels/MainViewModel.Commands.cs 选择、安装、卸载、清理、打开目录
ViewModels/MainViewModel.Settings.cs 设置往返、DPI 刷新、关于/日志命令
ViewModels/CompareViewModel.cs 最多三款字体同屏对照
Models/FontRow.cs 一行瓦片的数据载体
Models/FontItem.Tile.cs 瓦片选中标记与请求入口
Models/GlyphCoverage.cs 缺字结果(种类、数量、示例字符)
Views/MainWindow.xaml(.cs) 主窗口
Views/MainWindow.Tiles.cs 瓦片点击、列宽刷新、全选
Views/CompareWindow.xaml(.cs) 对比窗口
Views/AboutWindow.xaml(.cs) 关于:环境信息一键复制
tests/FindFont.SmokeTests/ 无第三方依赖的控制台冒烟测试(TestRunner + TestTiers 分 unit/integration/ui + Tests/)
build.cmd test rem 全部
build.cmd test unit rem 只跑纯逻辑(不读本机字体、不开窗口)
build.cmd test integration
build.cmd test ui测试直接跑真实字体库,不 mock:扫描本机注册表、遍历全部字体文件、实际渲染位图。失败时返回非零退出码,CI 据此拦截。
用例按三层标注(TestTiers.cs):unit 是纯逻辑,integration 要读本机字体库或临时文件,ui 要构造 WPF 窗口解析主题资源。CI 先单独跑 unit 再跑全量,这样失败时能立刻分清是逻辑坏了还是环境不同;发布任务跑在 windows-2022 与 windows-latest 两档上(注册表视图、ttc 数量、DPI 回退在不同系统版本上确实有差别)。
LanguageInference关键词推断(中 / 日 / 韩 / 西文 / 空名)FontMetadataReader.ContainsCjk的 CJK 判定与 null 安全FontItem.BestName三级回退与DedupKey大小写不敏感- 真实扫描本机注册表(断言字体数 > 0、字段非空)
EnhanceAsync抽样补全,断言字重被回填;旧式.fon不得误判为异常- sfnt 直读覆盖率 100%(不退回
GlyphTypeface慢路径),抽样核对 SimSun / Arial / 微软雅黑的家族名与中文名 FontRenderCache实际出图,断言尺寸 > 0、已Freeze、二次请求命中缓存MainViewModel真实加载后验证搜索、分类筛选、风格筛选与清空恢复- 1 万条字体的压测:装载、搜索、筛选、清空恢复各自的耗时上限
CompareViewModel最多三栏上限与移除后栏数变化- 界面资源与主窗口可构造,分类索引行计数与选中分类下的可见项数一致,画布不小于 1300×860
- 动效助手遵循系统设置:
Motion.Evaluate(true/false)返回值与三档时长排序 - 窗口材质回退不抛异常:
WindowBackdrop.Apply在任意系统版本上都返回合法档位 - 尺寸与模板资源:默认画布、最小时宽、模板键存在性
- 版本号来自
version.txt:程序集元数据、AppInfo.DisplayVersion、视图模型三处一致 - 位图缓存受字节预算约束:塞入超预算的大图后总量回落到预算内
- 同键并发请求只渲染一次:10 个并发请求共用同一张图
- 未选中的行不渲染大图:构造样本行不触发 220px 渲染,选中后才出现
RangeObservableCollection只发一次 Reset- 搜索快路径与原多字段匹配等价:200 个真实字体 × 6 组查询串逐一比对
- 卸载不删系统字体文件:未经本工具安装的文件一律不算托管,只清登记
- 网页字体(WOFF)明确拒装:
.woff/.woff2/.eot在碰系统目录之前就被拒绝,且文件未被复制 - 字体注册表值名格式正确:
.ttc用「(TrueType collection)」、.otf用「(OpenType)」,后缀不重复 - 单实例互斥体只放行一个进程:第二次
Acquire必须识别出已有实例 - 设置的默认值与屏幕可见性判断(拔掉显示器后窗口坐标不会被恢复)
- 设置经视图模型往返一致:预览文字 / 字号 / 分类 / 风格 / 搜索词
- 渲染 DPI 只在合理范围生效:0 / 负数 / NaN / Infinity 一律回落到 96
- DPI 参与位图缓存键:同一字体换 DPI 必须拿到另一张图
- 崩溃日志落盘且带上下文,且不抛异常
- 源文件不超过 300 行(与 CI 里的同名检查互为双保险)
- 多字面容器展开为多条字体:
simsun.ttc解出 2 个字面,同容器内 FaceIndex 不重复 - 字符覆盖优先于名字推断语言:OS/2
ulUnicodeRange的汉字位判中文,日文假名与汉字同时出现的字体不误判 - 嵌入权限位翻译成文案:
fsType各标志位对上中文说明 - 四种排序键正确且稳定:每个模式按自己的键排序,键相同时按名称兜底(结果可复现)
- 搜索高亮切分命中片段:命中保留原大小写,前缀/后缀边界正确,空白查询清空
- 拖放安装复用同一分拣路径:空列表安全返回,网页字体在碰系统目录前被拒
- 多字面标注与渲染键:单字面无标注,多字面渲染键带
#<faceIndex> - 批量读字面与逐字面读取等价:一次打开读出的每个字面,家族名与序号都与单独读取一致
- 每个字体文件只探测一次:索引结果条数等于去重后的文件数
- 注册表并行读取顺序稳定:两次并行读取的条目顺序逐项一致
- 首屏预热覆盖可见行:预热投递不阻塞 UI(断言 <200 ms),之后位图陆续落位
- 渲染线程池并行出图:线程数夹在 2–4,不同键能同时出图且每个键只渲染一次
- cmap 无映射被识别:
流丽行书.ttf里「你」不在 cmap 中,判为无映射;同串「世」完整 - 映射到空字形被识别:
日文毛笔字体.ttf里「你」有映射但glyf轮廓为 0,判为空字形 - 两类缺字不会混淆:Arial 面对汉字全是无映射,不会误报空字形
- 多字面字形覆盖独立检测:
simsun.ttc的两个字面各自量测,覆盖集合都在 - CFF 字面不判空字形:只认「有
CFF表且没有loca」的真 CFF 字体,断言其不宣称支持 - 字形覆盖检测复用缓存:同一探测集第二次不再读盘,结果可从缓存取回
- 缺字筛选只留对应类型:「只看缺映射」「只看空字形」「只看正常」各自只留对应项,缺映射与空字形同开是交集;「正常」排除未检测项
- 多列分行与筛选顺序一致:37 条按 2/3/1 列切出的行数与末行瓦片数正确,展平后顺序与源集合逐项相同
- 列数随窗口宽度收敛:1360/1120 → 3 列,1080/852 → 2 列,700/470 → 1 列;瓦片宽度始终 ≥220 且是 32 的整数倍;宽度微调不改变结果
- 瓦片选中驱动既有筛选语义:独占、Ctrl 切换、Shift 连选、清空都落到同一个选中集合上
- 间距落在 4px 网格:尺寸 token 全部是 4 的整数倍且为正,版心台阶单调(标题 > 面板标题 > 正文 > 元信息)
- 文字对比度达到 AA:
Ink/InkMuted/InkFaint/Proof/Warn在纸色、面板、抬升面板三种底上都 ≥4.5:1 - 品红不做药丸底色:样本行模板里
ProofWash出现 0 次,品红只作为文字与裁切线颜色 - 用户级安装路径走 HKCU:用户字体目录判定只认
%LOCALAPPDATA%\Microsoft\Windows\Fonts之下,系统目录与空路径都不算 - 可变字体坐标轴被解析:在本机找到真可变字体(有
fvar表)时断言轴串非空,Arial 这类静态字体必须为空串,空表输入安全返回 - 发布包里只有一个 exe:程序集名必须是
FindFont,目录里不得出现FindFont.App.exe,publish/下只能有FindFont.exe一个文件
当前全部通过(58 项:unit 28 · integration 26 · ui 4)。
打开软件到看见第一屏字体缩略图,这条链路被拆成可以分别量测的五段。全部打点由 Helpers/StartupTrace.cs 记录,「关于」窗口底部会列出这一次启动的每一段耗时,日志里也写一份。
| 阶段 | 做什么 | 关键取舍 |
|---|---|---|
| 宿主加载 | 运行时按需把单文件里的程序集映射进内存 | 单文件关了压缩(EnableCompressionInSingleFile=false),省掉每次启动先解压一遍;PublishReadyToRun 把冷 JIT 开销挪到构建期 |
| 主程序启动 | 单实例互斥体、窗口构造、DWM 材质 | DPI 在窗口构造后立刻上报,早于任何渲染请求,避免首屏按 96 DPI 出图再被拉伸 |
| 扫描 | 读四个注册表视图 → 探测文件 → 建条目 | 注册表并行读、每个路径只 ResolvePath 一次、每个文件只打开一次(字面/大小/修改时间一次拿全) |
| 后台补名 | 给名字解析失败的条目补本地化名 | 只在必要时跑,每 50 条批量回填;扫描一结束主界面就可交互,不等补名 |
| 首屏出图 | 预热视口内的十几行小样本 | 请求一次性全投给 2–4 条渲染线程,UI 不等图;串行要 1.1 s,并发冷启动约 150 ms |
这不只是「快一点」的问题:早期版本在 UI 线程上逐张等位图,首屏 12 行要 1.1 秒,用户看到的是十二行「正在渲染…」同时定格。现在画面在第一帧就位图齐备。
本机(Windows 11 26H2、1380 个已装字体、1329 个不同字体文件,其中 23 个多字面容器展开出 70 个字体)实测:
| 场景 | 耗时 |
|---|---|
| 注册表扫描出完整列表(含字面展开、语言判定、文件时间) | 约 0.4 s(优化前 1.2 s) |
| 全量元数据解析(每文件只打开一次) | 约 270 ms(0.20 ms/文件,优化前 453 ms) |
| 首屏 12 行出图(冷启动,4 条渲染线程并发) | 约 150 ms(串行要 1.1 s) |
| 单个样本行再渲染(缓存命中) | < 0.01 ms |
| 1 万条字体的搜索 / 筛选 / 清空恢复 | 各约 1 ms |
| 字形覆盖检测(1380 个字体,24 个不同码位) | 约 140 ms,再次检测命中缓存约 1 ms |
| 内存占用(1380 个字体、可视区渲染完) | 约 160 MB,位图缓存上限 96 MB |
上面的数字来自仓库外的进程内基准(扫描 / 元数据 / 首屏三段各自计时)。测试里刻意不断言绝对毫秒——CI 机器的磁盘比开发机慢一个数量级,绝对时间断言必然假失败;性能护栏改成断言结构性保证(只打开一次、只探测一次、并发数在合理区间、预热不阻塞 UI)。
滚动性能的关键是「只有被选中的行才渲染大样本」:早期版本里每个行模板都在实例化时对 LargeSample 求值,滚过一行就多渲染一张 1500×220 的大图。现在大样本改由视图模型的 RequestLargePreviews 按选中集合主动请求,行模板只绑 PreviewBitmapLarge 属性。
版本号只写在仓库根的 version.txt 一处。Directory.Build.props 把它读成 Version,所以程序集元数据、窗口标题行、dist/find-font-<version>-win-x64.zip 的命名、CI 的打包与校验全都来自同一个值。
发一个新版本是三步:
echo 1.0.0 > version.txt # 1. 改版本号
git commit -am "release: 1.0.0" # 2. 提交
git tag v1.0.0 && git push origin main --tags # 3. 打新 tagtag 触发时 CI 会先校验 v$(cat version.txt) 与 tag 名完全一致,不一致直接失败,避免「新 tag 顶着旧版本号发出去」。每个版本用独立的新 tag——不要删掉旧 tag 再重建:复用同一个 tag 会让 Release 页面一直显示同一个版本号,也让「这次究竟改了什么」无法追溯。
- 旧式
.fon位图字体不是 sfnt 容器,无法读取家族名,列表里以文件名做显示名(不标记为异常)。 - 多字面容器按文件头声明的成员逐个展开;嵌在
.ttc里、但文件头没有独立声明的隐藏字面仍取不到。 - 语言归属优先用 OS/2
ulUnicodeRange的字符覆盖位;个别字体没写这张表,才回落到名字关键词推断,此时冷门字体可能落进「其他」。 - 注册表扫描只覆盖
HKLM/HKCU的Windows NT\CurrentVersion\Fonts,不处理通过 GDI 私有加载的临时字体。 - 主程序是框架依赖单文件,依赖目标机已装 .NET 8 桌面运行时;缺失时由宿主弹出系统对话框说明并给出下载链接。
- 首次启动(ReadyToRun 缓存未预热、系统文件缓存为空)比后续启动慢;第二次起明显更快。
- 字形缺失检测只认
glyf轮廓(真 CFF/PostScript 轮廓的.otf没有loca表),这类字体只报「无映射」,不判断字形是否为空。 - 缺字检测的依据是「当前样本文字」这一小组码位,不是整张字符表;它回答的是「我这串字能不能显示出来」,不是「这个字体完整吗」。
fsType许可位只做提示:字体文件自己说「禁止嵌入」时,安装仍会进行并附一句提醒——是否可用取决于授权,工具不做执法者。- 可变字体只读
fvar的轴范围,不解析STAT表,也不提供轴滑杆;要看具体某一档,请在样本文字里换字重后另用设计软件。
欢迎提 Issue 与 PR。提交前请确认:
build.cmd test全绿(测试跑的是真实系统字体,本机数字与 CI 会不同,断言只看覆盖率与等价性,不看绝对耗时)。- XAML 里没有新增
FontFamily绑定——这是这套架构的地基。 - 长耗时操作是异步的,
ObservableCollection只在 UI 线程改。 - 单个文件尽量控制在 300 行内,视图逻辑放视图模型。
MIT © 2026 DHA404
