Qt/C++点阵取字模工具:从QFont栅格化到51/STM32字模生成

发布时间:2026/9/16 13:42:20
Qt/C++点阵取字模工具:从QFont栅格化到51/STM32字模生成 简介这套基于Qt/C开发的单片机点阵取字模工具面向嵌入式、物联网及计算机相关专业学生和开发者解决LED点阵屏字符与图形编码生成效率低的问题也适合作为课程设计、毕业设计或Qt上位机开发练手项目。压缩包共78个文件约294KB主体为62个SVG图标资源、Qt样式表QSS、界面UI文件、C源文件及工程配置.pro/.qrc并附带详细README文档代码结构清晰便于快速理解模块划分。当前已有84人学习下载说明其实用性受到一定认可。除了可直接运行的完整源码外还包含界面资源与说明文档读者既能直接用于点阵字模提取也可在此基础上扩展其他单片机上位机功能或借此熟悉Qt/C项目组织与GUI开发流程。1. 点阵取字模为什么值得单独做一个桌面工具做过 LED 点阵屏的人都有这种体验在 51 单片机或 STM32 上要显示一个汉字先得去网上找个取模软件选字体、选字号、设置取模方向点一下生成一堆十六进制字节然后手动复制到 Keil 的数组里。如果型号不对、方向错了显示出来就是一团乱码回头还要对着数据表一行行改。更麻烦的是很多现成取模工具界面陈旧、参数不透明换了高分屏之后字体渲染还不正常生成的字模边缘有毛刺。这个基于 Qt/C 的点阵取字模项目解决的就是这个问题。它把「输入字符 → 选择字体字号 → 像素栅格化 → 生成字节数组」整条链路放在一个跨平台桌面应用里界面上直接预览点阵效果也可以把所有字模数据导出成头文件给固件工程师用。对于硬件开发从业者和做单片机课程设计的学生来说这个项目的价值不只是「能用」而是你可以在源码层面看到字模到底是怎么从字形变成二进制比特位的。2. 工程结构与字模数据模型从 dotmatrix_font.pro 到 base.hpp拿到压缩包后先看根目录下的dotmatrix_font.pro和src目录。作为 Qt 工程.pro文件里定义了模块依赖、源文件和 Qt 版本兼容规则。工程里没有用 QML全部是mainwindow.cppmainwindow.uibase.hpp组成的经典 Widgets 架构所以编译门槛很低Qt 5.12 以上任意版本的桌面套件都能直接打开运行。2.1 源码仓库的文件角色划分base.hpp是这个项目的数据模型层它承载了所有字模相关的结构体定义和常量枚举。mainwindow.h/cpp是界面与控制逻辑层负责把用户在界面上录入的字符、字体、大小这些参数交给base.hpp里的数据模型去处理。mainwindow.ui是 Qt Designer 设计的界面布局文件里面包含了字符输入框、字体选择按钮、点阵预览区域和导出选项面板。这里有一个值得关注的细节base.hpp单独拆出来说明设计者有意把取模逻辑与界面解耦。这样做的直接好处是如果你不想用这个界面完全可以在控制台程序里#include base.hpp调用它的核心接口来批量生成字模这才是对嵌入式工程师友好的设计。2.2 字模数据结构的设计思路在base.hpp里核心数据结构大致承担这样的职责记录每个字模的宽度、高度、扫描方向和生成的字节数据。我在拆解这个结构时发现最关键的字段是两个标志位——一个表示「横向取模还是纵向取模」另一个表示「高位在前还是低位在前」。// base.hpp 字模数据封装结构说明非原始代码逐行拷贝 typedef struct { unsigned int width; // 点阵宽度如 16、24、32 unsigned int height; // 点阵高度 int mode; // 0横向取模1纵向取模 int bitOrder; // 0高位在前1低位在前 QVectorunsigned char data; // 字模字节数组 } FontDotMatrix; class DotMatrixGenerator { public: // 传入QFont字体对象和字符返回字模数据 static FontDotMatrix generate(const QFont font, const QString text, int size, int mode, int bitOrder); };这个结构体设计的聪明之处是把「取模方式」直接编码进数据对象而不只是函数参数。因为同一批字模在 51 单片机的 8×8 点阵屏和 STM32 的 16×16 点阵屏上扫描方式完全不同数据和模式绑定后导出时就能在文件头部自动写入「本文件的取模参数」避免烧进单片机后找不到对应代码的问题。2.3 界面参数与逻辑的衔接方式mainwindow.cpp中的核心逻辑是从QLineEdit拿到用户输入的字符串用QFontComboBox或系统字体对话框选择字体再通过QSpinBox设置字号和宽度。信号槽的连接方式是connect(ui-sizeSpinBox, QSpinBox::valueChanged, this, MainWindow::refreshPreview)即任何参数变化都会触发重新生成预览。提示在 Linux 下如果发现字体列表为空不是代码问题是系统缺少 fontconfig 库实例化出来的字体族需要安装中文字体包。3. QFont 栅格化与像素遍历取模算法的完整实现取模是软件的核心它的本质是把矢量字体的轮廓描述转换成点阵位图再逐像素读取出黑白状态压制成字节。如果你读懂了这一章的思路不仅能用这个工具还能自己改造出空心字、反白字、渐变字模等高级变体。3.1 从 QFont 到 QImage 的渲染链路Qt 内置的字体引擎支持直接从 QFont 绘制到 QImage。这一步非常关键因为 51 单片机上没有字体文件必须先主机端完成「栅格化」。常见做法是创建一张尺寸略大于字号的单色位图然后把字符绘制到中央。// 核心取模逻辑用QPainter将文本渲染到单色位图 QImage image(size padding * 2, size padding * 2, QImage::Format_Mono); image.fill(Qt::white); // 白底 QPainter painter(image); painter.setFont(font); painter.setPen(Qt::black); painter.drawText(image.rect(), Qt::AlignCenter, text); painter.end();代码逻辑说明用Format_Mono而不是Format_RGB32是因为单色位图每个像素只占 1 个 bit遍历时的计算量小一个数量级而且取模本身就是二值化的不需要灰度中间态。fill(Qt::white)保证背景为 1点亮drawText绘制的黑色前景为 0这样后面遍历像素时判断逻辑更直观。3.2 横向取模与纵向取模的字节拼装差异横向取模是嵌入式开发中最常用的方式每 8 个连续像素组成一个字节。比如一个 16×16 的汉字横向取模后每一行有 2 个字节共 32 个字节。纵向取模则按列扫描适合 8 列一组的硬件。// 横向取模核心遍历mode 0 for (int y 0; y height; y) { for (int x 0; x width; x) { int index y * width x; QRgb pixel image.pixel(x, y); bool isBlack qGray(pixel) 128; if (isBlack) { int byteIndex index / 8; // 所在字节 int bitIndex x % 8; // 字节内位置 data.data()[byteIndex] | (0x80 bitIndex); // 高位在前 } } }参数说明0x80 bitIndex表示字节高位对应左边第一个点这与绝大多数点阵屏逐行发送时先送高位的时序匹配。如果硬件要求低位在前只需要改成0x01 bitIndex。byteIndex index / 8在横向取模下等价于y * (width/8) x/8两者结果不同使用统一的线性索引可以避免行边界错位。纵向取模的差异在于遍历顺序——外层循环遍历列内层循环遍历行。代码结构类似// 纵向取模mode 1 for (int x 0; x width; x) { for (int y 0; y height; y) { QRgb pixel image.pixel(x, y); if (qGray(pixel) 128) { int index x * height y; data.data()[index / 8] | (0x80 (y % 8)); } } }3.3 字节序、掩码与对齐边界问题很多初学时生成的数组烧进单片机后显示乱码原因不是取模方向错了而是字节序和掩码没对齐。上面代码中0x80 bitIndex与0x01 bitIndex的结果刚好互为镜像在行宽不是 8 的整数倍时比如 11×11 的字符一定要做边界掩码处理。// 行尾多余bit清零避免脏数据 int rowBytes (width 7) / 8; int validBits width % 8; if (validBits ! 0) { unsigned char lastMask (0xFF (8 - validBits)) 0xFF; data.data()[(y 1) * rowBytes - 1] lastMask; }这段代码的作用是当宽度是 11 时每行最后一个字节只有低 3 位有效不处理的话高位 5 个 bit 是随机值点阵屏上就会出现垂直于扫描方向的噪声点。这个细节在项目中虽然只是几行但对输出质量影响非常直接。4. 51/STM32 点阵屏实战生成数据烧进去亮起来字模数据最终要进入单片机工程生效这块我用一个 51 单片机 8×8 点阵屏显示的实例来演示完整链路。生成好的字模数据是一个字节数组在 Keil 或 STM32CubeIDE 里创建源文件存放这些数组然后写扫描函数点亮对应的 LED。4.1 将 Qt 导出的字模数据接入 Keil 工程软件侧导出头文件时代码格式应该长这样// font_data.h 由Qt工具导出的字模数组 #ifndef __FONT_DATA_H #define __FONT_DATA_H // 字符 A8x8 横向取模高位在前 const unsigned char code FONT_A[8] { 0x00, 0x3C, 0x66, 0x66, 0x7E, 0x66, 0x66, 0x66 }; // 汉字 你16x16 横向取模 const unsigned char code FONT_NI[32] { 0x00, 0xFC, 0x04, 0x24, 0xFC, 0x04, 0xFC, 0x00, // ... 此处省略其余字节 }; #endifcode关键字是 51 编译器的特殊存储限定它把数组放入程序存储区而不是内部 RAM。在 STM32 上则不需要这个关键字直接用const unsigned char即可因为 CM 内核的 Flash 空间通常足够存放字模。4.2 8×8 点阵屏的动态扫描显示代码8×8 点阵屏只有 16 个引脚必须逐行或逐列快速扫描利用视觉暂留效应形成完整图像。下面是常见做法P0 口接行线输出字模数据P2 口接列线输出列选信号。// 51单片机动态扫描显示一个8x8字模 void display_font(const unsigned char *font_data) { unsigned char col 0x01; // 从第0列开始扫描 for (int i 0; i 8; i) { P0 font_data[i]; // 该行8个bit的数据 P2 ~col; // 选中第i列低电平有效 delay_us(200); // 保持点亮200us P0 0x00; // 消隐防止拖影 col 1; } }这段代码里的P0 font_data[i]把第 i 行数据直接送给行驱动~col是因为这类点阵模块的列选是低电平导通。delay_us(200)不能省也不宜超过 1ms否则光闪烁会很明显。如果想要字模数据按列存储把font_data[i]换成按列组织的数组即可这种灵活切换正是前面数据结构保留 mode 字段的原因。4.3 16×16 汉字点阵扩展16×16 点阵屏由 4 个 8×8 模块拼接扫描代码在 8×8 基础上顺序扩展即可。每条行线控制 2 个字节用两个 P 口或 74HC595 级联扩展。// 16x16 分上下半屏扫描上半屏 void display_han_upper(const unsigned char *data) { unsigned char row_sel 0xFE; for (int i 0; i 8; i) { P0 data[i * 2]; // 左上模块行数据 P1 data[i * 2 1]; // 右上模块行数据 P3 row_sel; // 选择第i行 delay_us(500); P0 0x00; P1 0x00; row_sel (row_sel 1) | 0x01; } }这里i * 2和i * 2 1的索引方式说明 16×16 取模时同一行的左右两字节是连续存储的这也符合第 3 章中横向取模的生成顺序。上下半屏的衔接关键在于扫描行号循环 8 次后复位数据指针偏移 16 字节继续下一半。5. 字模边界问题高 DPI、字节序与反色处理的几个细节这块是使用中容易踩坑但文档里不会写的地方属于进阶内容。无论是用这个 Qt 工具还是自己改写代码都需要处理以下三类边界条件。5.1 高 DPI 下字体渲染偏移的处理在 2K/4K 分辨率的显示器上Qt 默认启用缩放因子image.pixel(x, y)取到的像素可能跟QSpinBox里设置的 16 号字对不上。常见做法是显式禁用界面的缩放而只让预览区缩放// main.cpp 中强制按物理像素渲染 QApplication::setAttribute(Qt::AA_Use96Dpi); QApplication::setAttribute(Qt::AA_DisableHighDpiScaling);注意在 Qt 6 中 API 有所调整需要改用QGuiApplication::setHighDpiScaleFactorRoundingPolicy或者用环境变量QT_ENABLE_HIGHDPI_SCALING0启动。判定标准是绘制出来的点阵宽度与输入字号在数值上一致而不是看起来大小舒服。5.2 大端机与小端机的字节序差异字模数组是纯字节流本身不受大小端影响但移植到 STM32 时如果通过指针强制转换访问或按 16 位半字组织数据就会出现高低字节互换的故障。// 错误的做法将两个字节转为16位整数 uint16_t dot *(uint16_t *)font_data[i * 2]; // 大小端导致错位 // 正确的做法严格按字节运算 uint16_t dot (font_data[i * 2] 8) | font_data[i * 2 1];在 51 单片机上不存在这个问题因为 8051 是纯大端架构而 STM32 默认小端。如果你的固件代码里对字模做了位运算、按位取反或移位必须确认数据在目标平台上的字节序并在 Qt 导出时提供可选的「字节交换」开关。5.3 反色、空心和缩放三个实用技巧字模工具生成的默认字体是实心黑体如果点阵屏是共阳极接法需要把数据整体按位取反。利用QImage::invertPixels一行代码即可// 反色处理共阳极点阵屏适用 image.invertPixels(QImage::InvertRgb);空心字模的做法是在取模遍历时过滤掉内部像素。检测方法是判断当前像素的四个正交邻居是否全部为黑如果是则说明当前像素位于内部将其置为白这样描边就保留下来bool isInside isBlackNeighbor(image, x - 1, y) isBlackNeighbor(image, x 1, y) isBlackNeighbor(image, x, y - 1) isBlackNeighbor(image, x, y 1); if (isInside) continue; // 跳过内部像素形成空心轮廓缩放则是遍历目标位图的每个像素在源图中找到最近邻取样点适合将 16×16 放大到 32×32 而不引入灰度插值。Qt 的QPainter::drawImage配合Qt::FastTransformation能保证硬边效果不会出现抗锯齿灰阶边缘污染字模数据。本文还有配套的精品资源点击获取