简介:这份资源是一套基于 C# 开发的点阵字体文件查看工具源码,面向嵌入式/MCU 开发者和对 HZK16 字库结构感兴趣的初学者,用于快速查看与解析 16x16 点阵字模数据。项目采用 Windows Forms 构建,包含文件读取、二进制解析、图形显示、用户交互等模块,可帮助理解点阵字体的底层显示原理,也为进一步扩展或移植到其他字库格式提供了参考。压缩包共 15 个文件,主要由 8 个 C# 源码文件、3 个 resx 资源文件及工程配置文件组成,整体仅 9KB,结构小巧、便于直接阅读和二次开发。已有 935 人学习下载。对于研究 UCDOS 汉字显示机制、或需要手工检查字库数据的开发者来说,这是一份轻量且实用的工程样例,作者博客中还提供了实现细节说明,可作为学习 C# 桌面编程与数据可视化结合的入门参考。
1. 点阵字体查看工具拆解:一个 C# 小工具如何啃下 HZK16 字库
在嵌入式屏显调试现场,最磨人的往往不是 SPI 时序,而是“一个字渲染成两半”这种玄学问题。某次我调一块 12864 点阵屏,进度卡在字库上:同一个 HZK16 文件,换一台电脑打开就乱码,比对半天才发现是区位码偏移算法整体差了一行。这个用 C# 写的点阵字体查看工具,就是专门解决这类问题的。它能加载 HZK16、ASC16 这类二进制点阵字库,输入汉字实时渲染 16x16 点阵,支持放大预览、叠加网格核对取模方向,还能一键导出 C 语言字模数组。适合做 LCD 屏驱动、LED 点阵屏、嵌入式 GUI 的开发者,以及所有被“字模取反”折磨过的单片机选手。
2. 项目定位与文件格式:先搞懂点阵字库的区位码和取模方式
2.1 点阵字库的本质:一个字 32 字节的二进制协议
16x16 点阵汉字等于 256 个像素。每个像素只占 1 比特,值为 1 表示点亮,值为 0 表示空白。一行 16 个点就是 2 字节,16 行一共 32 字节。HZK16 字库文件就是按照 GB2312 区位码顺序,把成千上万个汉字的这 32 字节顺序排列而成。
GB2312 的汉字区是 16 到 87 区,每区固定 94 个字符位,一个汉字由“区号 + 位号”唯一确定。比如“啊”是 16 区 01 位,它在文件里的 GB2312 编码字节是 0xB0 0xA1。解析器拿到这两个字节后,分别减去基准值得到逻辑区号和位号,再按公式offset = (区号 * 94 + 位号) * 32定位到文件偏移。这个 94 是每区固定字符数,32 是每个汉字字模字节数,两者都是 GB2312 体系里的硬性约定,不能拍脑袋改。
整个 HZK16 全库大小是 94 × 94 × 32 = 282752 字节,约 276KB。如果加上 ASC16 的 ASCII 半角字符部分,就构成了一套完整的 GB2312 点阵字库体系。下面是常见字库文件参数,后面扩展支持其他字号时可以直接对照:
| 字库文件 | 点阵尺寸 | 每字字节数 | 全库大小(GB2312) |
|---|---|---|---|
| HZK16 | 16 x 16 | 32 | 约 276 KB |
| HZK24 | 24 x 24 | 72 | 约 621 KB |
| HZK12 | 12 x 12 | 18 | 约 155 KB |
| ASC16 | 8 x 16 | 16 | 约 1.5 KB |
2.2 技术选型:为什么是 C# WinForms 而不是 Python 或 C
这个工具的定位是“查看 + 导出”,不追求跨平台,也不追求极致性能。C# 在这里有一个天然优势:System.Drawing 的 Bitmap 可以直接按像素操作,从文件读取到界面画点是一条龙,不需要拼接胶水代码。
对比其他方案:Python 加 Pillow 确实能快速写解析脚本,但要做桌面交互预览就得套 PyQt 或者 Tkinter,壳子比业务逻辑还重;C/C++ 做嵌入式验证是强项,但要弹窗、加滑块、支持拖拽打开文件,界面成本会直线上升。C# WinForms 从 BinaryReader 读文件到 Graphics.FillRectangle 画点,一个链路走完,按 F5 直接跑。
我一般会把“解析”和“渲染”拆成两个独立类,一个是 FontFileParser,一个是 DotMatrixRenderer,界面层只做事件绑定。这样后面就算把界面换成 WPF,或者改成命令行工具,核心逻辑都不用动,只需要换调用方式。
2.3 源码目录结构:轻量分层的单窗体布局
这是典型的单窗体小工具结构,没有引入 MVVM 框架,也没有分层容器,核心路径非常短:MainForm 收到输入字符,调用 FontFileParser.GetFontData 取出 32 字节字模,再调用 DotMatrixRenderer.Render 生成 Bitmap,最后赋给 PictureBox 显示。导出时走 CArrayExporter 拼字符串。
| 文件 | 职责 | 关键方法 |
|---|---|---|
| Program.cs | 应用入口,启动主窗体 | Main |
| MainForm.cs | 界面事件绑定、预览刷新、导出调用 | TextChanged、ExportBtn_Click |
| FontFileParser.cs | 读取字库文件,按区位码定位字模 | GetFontData |
| DotMatrixRenderer.cs | 把字节数组渲染为点阵位图 | Render |
| CArrayExporter.cs | 将字模格式化为 C 语言数组文本 | ExportToCArray |
整个项目净代码量不大,但对一个专门用来“啃字库”的工具来说刚刚好。模块边界清楚,哪个环节出问题直接看对应类,不需要满项目找。
3. 核心模块拆解:从二进制字节到屏幕点阵的完整渲染链路
3.1 字库解析器:区位码换算与 32 字节定位
解析器的核心就是 GetFontData 方法。输入一个字符,输出该字符对应的 32 字节字模。这里最容易出错的是 GB2312 编码的获取方式,必须显式指定编码,不能依赖系统默认编码。
public class FontFileParser { private readonly byte[] _fontData; public FontFileParser(string fontFilePath) { // 一次性读入整个字库文件,后续用偏移量取数据 _fontData = File.ReadAllBytes(fontFilePath); } public byte[] GetFontData(char ch) { // 显式使用 GB2312 编码,避免 UTF-8 环境下字节数不对 byte[] gbBytes = Encoding.GetEncoding("GB2312").GetBytes(ch.ToString()); if (gbBytes.Length != 2) { return null; // 非 GB2312 字符直接返回空 } // 区号 = 第一个字节 - 0xA1,位号 = 第二个字节 - 0xA1 int qu = gbBytes[0] - 0xA1; int wei = gbBytes[1] - 0xA1; // 偏移公式:((区号 * 94) + 位号) * 32 int offset = (qu * 94 + wei) * 32; byte[] fontData = new byte[32]; Array.Copy(_fontData, offset, fontData, 0, 32); return fontData; } }这段代码里有两个关键参数需要解释。第一个是0xA1,它是 GB2312 区位码在文件中存储时的基准值。文件第一个汉字的字节是 0xB0A1,对应 16 区 01 位,减去 0xA1 后得到逻辑区号 15、位号 0,代入公式后偏移量正好是 0。第二个是94,每区固定 94 个字符,这个数字直接参与地址计算,错一个字模就整体错位。
拿到 32 字节只是第一步,它本身是一串二进制位,没有任何可读性。要把它变成人能看懂的汉字,还需要渲染器逐位解析。
3.2 点阵渲染:按位与运算把字节变成像素
渲染器的任务是把 32 个字节按位展开,在 Bitmap 上逐点绘制。这里的关键是搞清楚字节和像素的对应关系。16x16 点阵中,每行 16 个像素占用 2 字节,高位在前,第一个字节的第 7 位对应行首最左边的像素。
public Bitmap RenderDotMatrix(byte[] fontData, int width = 16, int height = 16, int scale = 10) { Bitmap bmp = new Bitmap(width * scale, height * scale); using (Graphics g = Graphics.FromImage(bmp)) { g.Clear(Color.White); for (int row = 0; row < height; row++) { for (int col = 0; col < width; col++) { // 横向取模:每行 width/8 个字节,col/8 定位到当前字节 int byteIndex = row * (width / 8) + col / 8; // 高位在前:bitIndex = 7 - (col % 8) int bitIndex = 7 - (col % 8); bool isDot = (fontData[byteIndex] >> bitIndex & 0x01) == 1; if (isDot) { // 按放大倍数填充矩形,scale 越大点阵越清晰 g.FillRectangle(Brushes.Black, col * scale, row * scale, scale, scale); } } } } return bmp; }这段代码里byteIndex和bitIndex是两个最值得细看的参数。byteIndex的计算方式是row * (width / 8) + col / 8,它假设字模是横向取模排列的,也就是先存第 0 行的 2 字节,再存第 1 行的 2 字节。bitIndex = 7 - (col % 8)表示每个字节的最高位先画。如果渲染后发现汉字左右镜像,把bitIndex改成col % 8再试,往往立刻恢复正常。
提示:以上渲染按“横向取模、高位在前”处理。遇到左右颠倒或上下分离的现象,先别急着改代码,检查一下字库文件本身的取模方向。
3.3 界面联动:输入即预览的交互闭环
界面层做的事情非常简单:监听输入框的 TextChanged 事件,每次用户输入一个字符,就重新解析、渲染、刷新。这里要注意性能问题,每次新建 Bitmap 并赋给 PictureBox 是可以的,但如果后续要做批量预览,就得考虑复用 Bitmap。
private void InputTextBox_TextChanged(object sender, EventArgs e) { if (string.IsNullOrEmpty(InputTextBox.Text)) { return; } // 只取第一个字符做单字预览 char ch = InputTextBox.Text[0]; byte[] fontData = _parser.GetFontData(ch); if (fontData == null) { StatusLabel.Text = "当前字符不在 GB2312 字库中"; return; } Bitmap dotMatrix = _renderer.RenderDotMatrix(fontData, 16, 16, scale); PreviewPictureBox.Image = dotMatrix; // 同步显示字模的十六进制文本,方便对照 HexTextBox.Text = ByteArrayToHex(fontData); }这段逻辑里有一个值得注意的设计:预览和十六进制文本是同步刷新的。在实际调屏显的时候,光看画面不够,还要看字模原始字节,两边对照才能快速确认是解析问题还是渲染问题。scale是界面上的放大倍数,默认 10 倍时 16x16 的点阵会显示成 160x160 像素,格子和笔画都看得清楚。
3.4 导出 C 数组:把字模变成单片机可直接引用的头文件
查看工具只是手段,最终目的是拿到能在单片机上用的字模数据。导出模块负责把 32 字节格式化为 C 语言数组文本,每行 8 个字节,方便直接复制进头文件。
public string ExportToCArray(char ch, byte[] fontData) { StringBuilder sb = new StringBuilder(); sb.AppendLine($"// 字符 '{ch}' 的 16x16 点阵字模"); sb.AppendLine("const unsigned char fontData[32] = {"); for (int i = 0; i < fontData.Length; i++) { sb.Append($"0x{fontData[i]:X2}"); sb.Append(i == fontData.Length - 1 ? " " : ", "); // 每 8 个字节换一行,保持和字库文件排版一致 if ((i + 1) % 8 == 0) { sb.AppendLine(); } } sb.AppendLine("};"); return sb.ToString(); }导出的数组格式直接决定单片机端好不好用。这里按每行 8 字节排版是故意的,因为 HZK16 的文件排列本身就是每 8 字节一组对应 4 行点阵,这样在代码里对照十六进制文本检查时非常直观。C 数组的内容和文件字节完全一致,没有做位序转换,如果单片机屏要求低位在前,需要在导出时另加一个选项处理。
4. 跑通与使用:从 Visual Studio 编译到字模导出全流程
4.1 源码工程结构与编译环境准备
项目是一个标准的 WinForms 解决方案,入口文件是 Program.cs,主窗体是 MainForm.cs。用 Visual Studio 2022 打开解决方案文件后直接按 F5 就能跑。工程文件里最关键的是目标框架配置,代码里用到了 System.Drawing 和 GB2312 编码,需要把框架指到 Windows 专用配置。
<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <OutputType>WinExe</OutputType> <TargetFramework>net6.0-windows</TargetFramework> <UseWindowsForms>true</UseWindowsForms> <Nullable>enable</Nullable> </PropertyGroup> <ItemGroup> <PackageReference Include="System.Text.Encoding.CodePages" Version="7.0.0" /> </ItemGroup> </Project>这里有一个多数新手会踩的配置点:.NET 6默认不携带 GB2312 代码页,必须在 Main 方法里注册 CodePagesEncodingProvider,否则运行到 GetEncoding("GB2312") 时会直接抛异常。如果用的是 .NET Framework 4.8 则没有这个问题,不需要额外装包。
Program.cs 的入口里需要加上一行注册代码,顺序在 Application.Run 之前。注册之后 Encoding.GetEncoding("GB2312") 才能正常工作,否则项目能编译但一点预览就崩溃,属于典型的运行期翻车。
4.2 从零跑通:加载字库、输入汉字、预览、导出
假设你已经用 Visual Studio 打开了项目,下面是一套完整验证流程:
第一步,准备字库文件。从任意字库资源站下载 HZK16,放到项目目录下的 Fonts 文件夹里,或者放到程序运行目录下。程序启动时会在几个常见路径里查找字库,找不到时会让用户通过文件对话框手动指定。
第二步,运行程序。主界面上方是汉字输入框,中间是点阵预览区,下方是十六进制字模文本。在输入框里输入“啊”,预览区应该出现一个清晰的 16x16 汉字点阵。如果显示全空白或者乱码,先检查字库文件是否完整,再检查偏移公式。
第三步,调整放大倍率。界面上的倍率滑块可以控制点阵缩放,从 1 倍到 20 倍。倍率越大,单个像素格越明显,适合核对笔画边界。核对取模方向时可以开启网格叠加层,每个小格对应一个像素,能直观看出字节位序是否正确。
第四步,导出字模。点击“导出 C 数组”按钮,弹窗中出现格式化好的头文件文本,直接复制粘贴到单片机的工程里即可。导出的数组按每行 8 字节排版,注释里带字符信息和点阵尺寸,方便后续维护。
4.3 没有字库文件怎么办:内置模拟字模兜底
很多第一次接触点阵字库的人并没有 HZK16 文件,程序一启动就提示找不到字库,预览功能完全没法验证。这个工具的兜底方案是在程序目录下没有字库文件时,用系统字体临时生成一份“模拟字模”,保证界面流程能跑通。
public byte[] GenerateFallbackFontData(char ch, int size = 16) { Bitmap bmp = new Bitmap(size, size); using (Graphics g = Graphics.FromImage(bmp)) { g.Clear(Color.White); using (Font font = new Font("宋体", 14, FontStyle.Regular, GraphicsUnit.Pixel)) { g.TextRenderingHint = TextRenderingHint.SingleBitPerPixelGridFit; g.DrawString(ch.ToString(), font, Brushes.Black, 0, 0); } } byte[] fontData = new byte[size * size / 8]; for (int row = 0; row < size; row++) { for (int col = 0; col < size; col++) { Color pixel = bmp.GetPixel(col, row); if (pixel.R < 128) { int byteIndex = row * (size / 8) + col / 8; int bitIndex = 7 - (col % 8); fontData[byteIndex] |= (byte)(1 << bitIndex); } } } return fontData; }这段代码的逻辑是先用 GDI+ 把字符画到位图上,再按像素灰度阈值转成 1 和 0,最后封装成和 HZK16 相同的 32 字节格式。注意其中的 TextRenderingHint 设置成 SingleBitPerPixelGridFit,这会让字体渲染变成黑白像素模式,去掉抗锯齿的灰色过渡层,转出来的点阵更接近真实字库的硬边缘效果。
模拟字模生成的字节格式和真实 HZK16 一致,所以渲染、导出模块都不需要改动。实际做嵌入式项目时,最终还是要换成标准字库文件,但用模拟字模先跑通工具流程、验证导出格式,是完全没有问题的。
5. 常见问题与避坑:区位码偏移、取模方向和字节序翻车记录
5.1 偏移差一行:读出的字模全是 0
现象:输入“啊”字,预览区全白,十六进制文本全是一串 00。
原因:偏移公式写错了。有人直接用 GB2312 的区码 0xB0 去乘 94,没有减去基准值,结果定位到的是 16 区开头的缓冲区空白位置,自然读不到数据。实际上“啊”字的区号是 16,位号是 1,逻辑下标应该是 15 和 0,而 0xB0 减 0xA1 等于 15,0xA1 减 0xA1 等于 0,正好落在 0 偏移。
解决:把偏移计算固定成“减 0xA1”这一种写法,不要混用减 0xA0 再减 1 的等价写法。两种写法结果是一样的,但混着用特别容易在改代码时把基准值搞丢。我现在的习惯是用 0xA1 直接减,少一层减法就少一个出错的点。
5.2 左右两半互换:取模方式选错
现象:汉字正常渲染,但左右两半明显分离,左边内容跑到了右边,右边内容跑到了左边。
原因:HZK16 字库文件存在横向取模和纵向取模两种版本。横向取模按行存储,每行 2 字节连续排列;纵向取模按列存储,先存左半部分 16 字节,再存右半部分 16 字节。渲染代码按横向取模解析,遇到纵向取模的文件就会错位。
解决:在解析器里增加一个“取模方式”配置项,横向时byteIndex = row * 2 + col / 8,纵向时换成byteIndex = col * 2 + row / 8。改动只有三行,但要验证当前字库文件到底属于哪种,最直接的方法是查文件的来源说明,或者输入一个左右结构明显的汉字看渲染结果。
5.3 GetEncoding 抛异常:.NET 6 下 GB2312 不可用
现象:程序一启动或者第一次输入汉字时,抛异常提示 System.Text.Encoding.CodePages 没有注册。
原因:.NET Core 和 .NET 5+ 默认不包含 GB2312 代码页,和 .NET Framework 不同,后者自带全部代码页。没装 NuGet 包直接调用 Encoding.GetEncoding,运行时找不到对应编码提供程序。
解决:安装 System.Text.Encoding.CodePages 包,在 Main 方法最前面调用Encoding.RegisterProvider(CodePagesEncodingProvider.Instance)。这段代码只加一次,位置必须在任何 Encoding 调用之前。项目里写上这一行之后,GB2312 的解析逻辑完全不用改。
5.4 拖动窗口时预览区疯狂闪烁
现象:输入汉字时一切正常,但拖动窗口或连续输入时预览区不断闪烁,画面一黑一白反复刷新。
原因:每次 TextChanged 事件都创建新的 Bitmap 并赋给 PictureBox 的 Image 属性,这会导致控件触发布局操作和全量重绘。高频率触发时,GDI+ 的重绘开销完全暴露出来。
解决:把 PictureBox 换成双缓冲的自定义控件,在 OnPaint 里画缓存好的 Bitmap。缓存指的就是只在文本变化时重建 Bitmap,平时鼠标移动、窗口拖动只做 Bitmap 的再绘制,不重新解析字模。这也是为什么解析和渲染要拆成两个类,界面刷新时只需要重绘已有 Bitmap,不需要再走一遍文件读取。
5.5 导出数组在单片机上花屏
现象:工具里预览完全正常,但导出的数组复制到单片机工程里,屏上显示的文字左右镜像,或者整体颠倒。
原因:预览和导出用的位序假设是高位在前,但单片机屏的驱动 IC 可能要求低位在前。有些屏的驱动还会按字库字节顺序整列颠倒,数据本身没问题,是位序和屏的规格不一致。
解决:在导出弹窗里加两个选项,一个选字节内位序“MSB-first / LSB-first”,一个选取模方向。导出时按选项重新调整字节序再格式化文本。这样同一个字库文件可以在不同屏之间复用,不需要改解析器代码。这个选项看着不起眼,实际项目里能省掉大量联调时间。
6. 二次开发:从单字预览到批量字模导出的改造思路
如果只是看一个汉字、导一个字模,这个工具已经够用了。但真实场景往往是整段文本、若干汉字一次提取。改造的第一步是把解析器抽象成接口,这样后面接 HZK24、ASC16 或者其他自定义字库时,界面和渲染层完全不用动。
public interface IDotMatrixProvider { string Name { get; } int Width { get; } int Height { get; } byte[] GetFontData(char ch); }HZK16、HZK24、ASC16 各自实现这个接口,解析逻辑只需要改宽度、高度和字节数。HZK24 每个字是 72 字节,宽度高度都是 24,偏移公式不变。界面层加一个字库选择下拉框,选中的 provider 直接替换当前的 _parser 实例,其他代码都不用改。
批量导出的核心是遍历文本,逐个字符调用 provider 的 GetFontData,然后拼接成一个大的 C 数组。注意要去重,同一个汉字在文本中出现多次时只提取一次,对应的编号在注释里标出来。
public string ExportBatchToCArray(string text, IDotMatrixProvider provider) { Dictionary<char, byte[]> fontCache = new Dictionary<char, byte[]>(); StringBuilder sb = new StringBuilder(); // 遍历文本,对每个唯一字符提取字模并缓存 foreach (char ch in text) { if (fontCache.ContainsKey(ch)) { continue; } byte[] data = provider.GetFontData(ch); if (data != null) { fontCache[ch] = data; } } // 按字符顺序输出数组,注释里带字符和索引号 int index = 0; foreach (KeyValuePair<char, byte[]> pair in fontCache) { sb.AppendLine($"// [{index++}] {pair.Key}"); sb.AppendLine(FormatBytes(pair.Value)); } return sb.ToString(); }验证改造效果的方法很简单:输入一串包含重复汉字的文本,导出的数组里每个汉字只出现一次,且渲染预览显示的内容和输入完全一致。再拿“啊”这个 GB2312 首字做交叉验证,因为它的字模值是固定的,任何改动能保证“啊”字渲染不变,基本就能确认解析和渲染逻辑没有破坏。从那以后我每次写点阵字库相关工具,都会先拿“啊”字验证偏移和取模方向,再继续写后面的逻辑。这个习惯帮我少踩了很多坑,希望帮到你。
本文还有配套的精品资源,点击获取