C#开发点阵字库查看工具:HZK16区位码与字模转换实战

📅 发布时间:2026/10/16 10:45:49
C#开发点阵字库查看工具:HZK16区位码与字模转换实战
简介这份资源是一套基于 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 点阵字库体系。下面是常见字库文件参数后面扩展支持其他字号时可以直接对照字库文件点阵尺寸每字字节数全库大小GB2312HZK1616 x 1632约 276 KBHZK2424 x 2472约 621 KBHZK1212 x 1218约 155 KBASC168 x 1616约 1.5 KB2.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应用入口启动主窗体MainMainForm.cs界面事件绑定、预览刷新、导出调用TextChanged、ExportBtn_ClickFontFileParser.cs读取字库文件按区位码定位字模GetFontDataDotMatrixRenderer.cs把字节数组渲染为点阵位图RenderCArrayExporter.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 SdkMicrosoft.NET.Sdk PropertyGroup OutputTypeWinExe/OutputType TargetFrameworknet6.0-windows/TargetFramework UseWindowsFormstrue/UseWindowsForms Nullableenable/Nullable /PropertyGroup ItemGroup PackageReference IncludeSystem.Text.Encoding.CodePages Version7.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 等于 150xA1 减 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) { Dictionarychar, byte[] fontCache new Dictionarychar, 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 (KeyValuePairchar, byte[] pair in fontCache) { sb.AppendLine($// [{index}] {pair.Key}); sb.AppendLine(FormatBytes(pair.Value)); } return sb.ToString(); }验证改造效果的方法很简单输入一串包含重复汉字的文本导出的数组里每个汉字只出现一次且渲染预览显示的内容和输入完全一致。再拿“啊”这个 GB2312 首字做交叉验证因为它的字模值是固定的任何改动能保证“啊”字渲染不变基本就能确认解析和渲染逻辑没有破坏。从那以后我每次写点阵字库相关工具都会先拿“啊”字验证偏移和取模方向再继续写后面的逻辑。这个习惯帮我少踩了很多坑希望帮到你。本文还有配套的精品资源点击获取