简介:点阵字体文件查看工具是一份用C#语言编写的桌面应用源码,面向单片机嵌入式开发和点阵字库学习人群。它针对HZK16格式的十六乘十六汉字点阵数据,提供文件加载、二进制解析和图形化显示功能,能将枯燥的位图数据转换为直观的字符轮廓,辅助开发者在汉字取模、显示驱动调试中快速核对字形。资源压缩包为7z格式,大小仅九KB,共十五个文件,包含八个C#源文件、三个界面资源文件以及解决方案、项目配置等,结构紧凑,适合用Visual Studio直接编译阅读。源码涵盖文件读取、数据解析、图像绘制和交互操作等核心功能,读者可借此掌握C#输入输出流操作、二进制位运算、窗口界面自绘等技术,并体会如何封装一个实用的字库查看工具。目前已有九百三十一人浏览学习,对于嵌入式显示器开发者、低资源环境下的文本显示方案设计者,以及希望从底层理解汉字点阵渲染原理的爱好者,都是一份值得参考的源码范例。 做了几年嵌入式相关的上位机开发,经常要和字库文件打交道。前阵子调一块12864液晶屏,发现要反复把HZK16里的字模数据抠出来,每次都是对着二进制数据数半天0和1,效率低到离谱。索性用C#写了个点阵字体文件查看工具,直接拖文件进去、输入汉字就能看到点阵预览,还能一键导出C语言数组。正好把源码和踩过的坑一并整理出来,给做单片机、墨水屏、LED点阵屏的朋友做个参考。
1. 项目概述:点阵字体查看工具到底解决什么问题
数码管、LCD1602、OLED、墨水屏这类设备,显示汉字靠的是字模数据——每个汉字对应一串二进制位,1亮0灭。但由于这类设备通常没有内置字库或者需要自定义字库,开发时要把字模数据手动提取出来,再塞进芯片的Flash里。问题就出在这:每次想确认字模对不对,就得打开Hex编辑器,对着十六进制字节在脑海里脑补像素排列,实在折磨人。这个工具的核心价值就是把这些二进制位“画出来”,所见即所得。
1.1 点阵字体的核心应用场景
点阵字体在嵌入式GUI、工业HMI、LED显示屏、电子价签、点阵打印机中仍然有一席之地。尤其是在低成本的MCU方案里,没有足够的ROM运行OpenType这类矢量字体引擎,点阵字库因为结构简单、渲染速度快,依然是主流选择。HZK16(16x16像素、每个汉字32字节)和HZK24(24x24像素、每个汉字72字节)是最常见的两种格式。
实际开发中还有一个高频场景是取模工具的校验。网上能下载到很多字库文件,但来源五花八门,有的是标准GB2312排列,有的做了偏移处理,有的字节序还不一样。写好设备驱动之后发现显示乱码,你无法确定是驱动问题、字库问题还是取模方式问题。这个查看工具另一个作用就是直接对照字库文件内容,用肉眼验证取模方向、字节序是否匹配你的屏幕驱动。
1.2 工具的功能定位
我做的这个版本定位足够轻量,核心功能就三点。
- 加载HZK16、HZK24点阵字库文件,解析并显示指定汉字的点阵图案
- 输入汉字或英文字符,实时预览点阵效果,支持缩放显示
- 一键导出当前汉字字模数据,生成可直接复制的C语言数组
有人可能会问,这类工具市面上不是有现成的吗?确实有,比如PCtoLCD2002,功能很全。但作为开发工具链的一部分,很多时候需要批量处理,或者集成到自己的自动构建脚本里,这时候有一个提供源码、可二次开发的版本就会方便很多。我自己就把它接到了一个串口屏项目的上位机里,字库更新时自动生成头文件,省了一道手工操作。
2. 前置知识:HZK16文件格式与汉字编码原理
不先搞懂HZK16的存储结构,后面写代码一定会踩坑。这里花点篇幅把原理讲透,代码实现其实就是几行数学运算。
2.1 区位码与机内码的关系
GB2312编码里,每个汉字由两个字节表示,范围是0xA1到0xFE。这两个字节分别叫“区码”和“位码”,取值范围都是01到94,这就是区位码。汉字在字库文件里的存放顺序,严格按区位码从小到大排列。
关键一步是区分“机内码”和“区位码”。文本文件里读到的是机内码,比如“汉”字的GB2312编码是0xBA 0xBA,这是给计算机看的;而区位码是0x1A 0x1A(对应十进制26区26位),这是给字库查表用的。两者换算关系很简单:
int quCode = rawBytes[0] - 0xA0; // 区码 int weiCode = rawBytes[1] - 0xA0; // 位码要注意这里减的是0xA0而不是0xA1,因为区位码从1开始编号。GB2312编码范围0xA1A1对应的是1区1位,而ASCII字符通常不放在这个字库里,所以直接用GB2312解码再转区位码,是最稳妥的方式。
2.2 字模偏移与字节排列规则
HZK16文件总共267KB,按94区乘94位排列,每个汉字固定32字节。要找到某个汉字的字模数据,偏移量计算公式是:
long offset = ((quCode - 1) * 94 + (weiCode - 1)) * 32;这里的32是16x16点阵的字模字节数。16x16点阵的排列规则是:每行16个像素,用2个字节表示,从上到下共16行。2个字节日志低字节在前,即水平方向先低字节的后8位,再高字节的前8位。
HZK24稍有不同,24x24点阵每行24个像素,用3个字节表示,总共72字节。偏移公式把32换成72即可。这种格式常见于24x24的LED显示屏和票据打印机,解析逻辑和HZK16完全一致,只是每一行的字节数从2变成3,而且字节内的位序通常会更复杂一些,需要根据具体LCD控制器的要求做转换。
3. C#源码实现:文件加载、字模解析与点阵绘制
理解了原理之后,代码本身并不复杂。我用的是WinForms,主要是因为内部工具,界面简单开发快,不需要考虑跨平台。如果你更习惯WPF或Avalonia,核心的解析代码可以直接复用,区别只在渲染层。
3.1 字库文件加载与汉字定位
先写一个基础的字模读取类,核心逻辑就是上述的偏移计算。
public class HzkParser { private byte[] _fontData; private readonly int _fontWidth; private readonly int _fontHeight; private readonly int _bytesPerLine; private readonly int _bytesPerChar; public HzkParser(string filePath, int width, int height) { _fontWidth = width; _fontHeight = height; _bytesPerLine = (int)Math.Ceiling(width / 8.0); _bytesPerChar = _bytesPerLine * height; _fontData = File.ReadAllBytes(filePath); } public byte[] GetCharBitmap(char ch, out int width, out int height) { width = _fontWidth; height = _fontHeight; // 获取GB2312编码字节 Encoding gb2312 = Encoding.GetEncoding("GB2312"); byte[] codeBytes = gb2312.GetBytes(new[] { ch }); if (codeBytes.Length < 2 || codeBytes[0] < 0xA1 || codeBytes[1] < 0xA1) return null; // 非汉字字符,不在字库中 int quCode = codeBytes[0] - 0xA0; int weiCode = codeBytes[1] - 0xA0; long offset = ((quCode - 1) * 94 + (weiCode - 1)) * _bytesPerChar; if (offset + _bytesPerChar > _fontData.Length) return null; byte[] result = new byte[_bytesPerChar]; Array.Copy(_fontData, offset, result, 0, _bytesPerChar); return result; } }这里有个细节值得注意:Encoding.GetEncoding("GB2312")在某些精简版Windows系统上可能会抛异常,因为系统默认没安装GB2312代码页。稳妥的做法是改用代码页编号936,或者直接用GBK编码替代,GBK完全兼容GB2312,而且大部分现代系统对GBK的支持更完善。
Encoding gb2312 = Encoding.GetEncoding(936);另外一个容易被忽略的是边界保护。文件长度不足、偏移越界都会导致异常,做内部工具可以不那么严谨,但如果想分享给同事用,这类防御性代码还是写上比较稳妥,不然别人拿到字库文件损坏就崩溃,这锅还是你背。
3.2 点阵绘制与可视化展示
拿到字模字节数组之后,下一步就是把它绘制到界面上。我这里用的是双倍缓冲的PictureBox,也可以直接使用自绘控件。
public Bitmap RenderBitmap(byte[] bitData, int width, int height, int scale = 8) { Bitmap bmp = new Bitmap(width * scale, height * scale); using (Graphics g = Graphics.FromImage(bmp)) { g.Clear(Color.White); Brush brush = Brushes.Black; for (int row = 0; row < height; row++) { for (int col = 0; col < width; col++) { int byteIndex = row * (width / 8) + col / 8; int bitIndex = 7 - (col % 8); bool isSet = (bitData[byteIndex] & (1 << bitIndex)) != 0; if (isSet) { g.FillRectangle(brush, col * scale, row * scale, scale, scale); } } } } return bmp; }绘制时默认每个点放大8倍显示,这样16x16的字模在界面上就是128x128像素,足够看清笔画结构。bitIndex = 7 - (col % 8)是将字节内高位对应左侧像素,这是HZK16“高位在前”的标准排列方式。如果你的屏幕驱动是低位在前,这里就改成bitIndex = col % 8。两种方式渲染出来的字会呈镜像,开发时务必核对驱动的要求。
预览区下方我放了一个多行文本框,专门显示当前汉字的二进制位流,每行一个字节,用字符串拼出来:
StringBuilder sb = new StringBuilder(); foreach (byte b in bitData) { sb.AppendLine(Convert.ToString(b, 2).PadLeft(8, '0')); }调试时对照这里的二进制文本和屏幕实际显示,能快速定位是字库问题还是驱动问题。
3.3 导出C语言数组
工具最实用的功能是生成可直接嵌入MCU工程的C数组。导出格式我做了两种:一维数组和二维数组。一维适合直接作为字库数据源,二维在按行扫描的OLED驱动中更直观。
public string ExportAsCArray(byte[] bitData, string name, bool as2D = false) { StringBuilder sb = new StringBuilder(); if (as2D) { sb.AppendLine($"const unsigned char {name}[{bitData.Length / _bytesPerLine}][{_bytesPerLine}] = {{"); for (int i = 0; i < bitData.Length; i += _bytesPerLine) { sb.Append(" { "); for (int j = 0; j < _bytesPerLine; j++) { sb.Append("0x" + bitData[i + j].ToString("X2")); if (j < _bytesPerLine - 1) sb.Append(", "); } sb.Append(" },"); sb.AppendLine(); } sb.AppendLine("};"); } else { sb.AppendLine($"const unsigned char {name}[{bitData.Length}] = {{"); sb.Append(" "); for (int i = 0; i < bitData.Length; i++) { sb.Append("0x" + bitData[i].ToString("X2")); if (i < bitData.Length - 1) sb.Append(", "); if ((i + 1) % 8 == 0) sb.AppendLine().Append(" "); } sb.AppendLine(); sb.AppendLine("};"); } return sb.ToString(); }这里有个衡量标准:导出的数组最好直接能复制进Keil/IAR工程,不需要手动改格式。我见过几个同事从网上下载的取模工具,导出的数组带了一堆注释,部分编译器还报错,非常影响效率。工具最终稿要把输出定义得干净利落。
4. 实操中的常见问题与排查技巧
开发这个工具本身不难,难的是适配各种来路不明的字库文件。我整理了一些高频坑,并给出应对策略。
4.1 高频问题与应对方案
| 现象 | 原因 | 应对方案 |
|---|---|---|
| 所有汉字预览都错位/乱码 | 偏移公式用错,或字库不是标准HZK排列 | 先确认文件大小:HZK16应为267KB,HZK24应为588KB左右,用文件大小反推验证 |
| 特定汉字显示异常 | 字体文件包含非GB2312字符集合 | 换用GBK编码计算偏移,或者过滤非常用字 |
| 显示是镜像字 | 字节内位序取反 | 检查bitIndex的计算方式,改为col % 8 |
| 预览时字是倒的 | 行扫描顺序反了 | 遍历时把row从height - 1到0 |
| 读Excel导出的字库文件失败 | 文件被Excel改写为带BOM的格式 | 用十六进制工具检查文件头,去除多余字节 |
其中“非汉字字符”这个比较典型。比如你想显示“·”或者全角标点,它们虽然也有GB2312编码,但位于符号区,HZK16字库里通常只收录汉字,没有符号数据。这种情况需要额外加载ASC16(ASCII字库)或者专门的符号字库才能正常显示。
还有一次我遇到一个特别诡异的情况:同一个HZK16文件,用别人的工具显示正常,用我的工具却乱码。排查到最后发现,别人的工具自动加了“字节内左右翻转”选项,而我的工具没有。对方拿到的屏幕是SPI接口、数据传输两位同时发送,要求字模的左右半区互换。这就提醒我们:工具默认状态一定要用标准HZK16排列,翻转、镜像、反色这类选项必须做成手动开关,千万别默认开启。
4.2 我的几个独家调试经验
第一,验证字库文件是否标准,最快的办法不是打开扫描,而是直接算“啊”字——它是GB2312中第一个汉字,区码16、位码01。用ASCII码0xB0A1搜索文件位置,如果偏移和计算结果一致说明字库没做特殊处理。这个技巧是我早期做打印机驱动时跟一个老工程师学的,排查字库加载问题非常高效。
第二,导出数组时顺手加一个“手动修改记录”的功能。很多工程师在调试过程中会手动调整字模数据,比如把某个点手动点亮、消除毛刺,但下次重新导出时修改就丢了。我在工具里加了个内存缓存,导出时对比原始数据,有差异就把修改过的字节高亮标记出来,方便回溯。这个功能看起来不起眼,但实际用起来很顺手。
第三,批量提取字模时最好生成一个独立的文本映射文件,记录哪个汉字对应哪段数据。比如LED屏要显示“温湿度监测”,就把这四个字的起始偏移和数据长度输出到一个文本里,方便联调时快速核对。我自己遇到过一次驱动读错地址,排查了半个多小时才发现是把第二个汉字的偏移算错了,如果有这个映射文件,一眼就能看出问题。
第四,原理上一定要把“字库文件大小”和“字模点数”换算关系放在脑子里。HZK16是32字符/字,HZK24是72字节/字,ASC16是16字节/字符,UCDOS字库虽然也用这些名字但个别版本加了文件头。拿到一个没见过的字库文件,先看大小再推测格式,很多时候能少走很多弯路。比如某个文件大小正好是267KB加上512字节,说明大概率带了一个512字节的信息头,解析时需要跳过。
5. 后续还可以怎么扩展
目前这个工具只解决了“看”和“导出”两个需求,实际上还可以继续发展成更完整的字库工作台。比如批量转换功能,把一组汉字一次性导出成一个大数组,自动生成索引表;比如点阵编辑器,手动点选像素来修字模;再比如反色、加粗、旋转90度这些图像处理功能,在制作OLED动画字库时会很有用。
我自己的规划是加一个“字库对比”功能,两个版本的HZK文件放一起,自动diff出哪些汉字被修改过。这在维护多语言字库、配合字体厂商做版本迭代时非常实用,不用再逐个汉字导出对比。另外一个方向是支持Unicode到GB2312的自动映射,很多新的GUI框架喜欢传Unicode编码字符串,底层又必须用点阵字库,中间这层转换放上位机处理好,能省不少单片机资源。
写代码这件事,很多时候其实是被实际需求追着跑。这个工具只花了一个周末就完成了初版,但后续零零碎碎的优化反而持续了两三周。每次在项目中遇到新的字库格式或者新的屏幕驱动,就往工具里加一点,慢慢就变成了顺手不过的工具。
如果你也在做类似的项目,建议不用等工具完全成熟,先把核心解析逻辑跑通,能显示能导出就行。剩下的功能,都是在用的时候才会发现真正需要什么。我自己在写这个工具的过程中最大的体会是:很多看似复杂的编码问题,画几张图、把字节位序彻底弄明白,代码反而是最简单的部分。希望这个工具的思路能帮你少踩几个坑。
本文还有配套的精品资源,点击获取