简介:这套基于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.cpp+mainwindow.ui+base.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低位在前 QVector<unsigned 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 // 字符 'A',8x8 横向取模,高位在前 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_SCALING=0启动。判定标准是绘制出来的点阵宽度与输入字号在数值上一致,而不是看起来大小舒服。
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能保证硬边效果,不会出现抗锯齿灰阶边缘污染字模数据。
本文还有配套的精品资源,点击获取