基于Qt/C++的嵌入式点阵取字模软件设计与实现 📅 发布时间:2026/9/16 13:42:18 👁 浏览次数: 简介这款基于Qt/C开发的单片机点阵取字模软件面向嵌入式、电子信息和计算机相关专业的在校学生、教师及企业开发者。它解决了在LED点阵屏、汉字显示项目中最繁琐的取模问题将汉字或图形自动转换为单片机可用的字模数据适用于课程设计、毕业设计及项目初期演示。压缩包共78个文件核心代码包括mainwindow.cpp、base.hpp等C源文件配套mainwindow.ui界面定义、resources.qrc资源管理和QSS样式表62个SVG文件提供了清晰的界面图标与流程图整体仅294KB内容精简且完整。目前已有84人学习下载项目经过完整测试具备较强的参考价值。文档和源码共同展示了从界面布局、事件处理到字模算法生成的整体实现思路读者既可直接使用打包好的软件也能在此基础上二次开发实现自定义字库或扩展更多显示功能。1. 基于QtC的点阵取字模软件到底解决什么痛点一次32位MCU项目里需要给一块12864 OLED显示中文菜单手边的取模工具只支持宋体16x16而产品UI要的是12号微软雅黑另一套工具导出的是逐行式阴码驱动库却只认逐列式高位在前。临时改脚本最后还因为字节序没对上折腾了一晚上。网上能下到的许多“点阵取字模软件资料源码”压缩包往往只给固定字号和固定格式拿来适配新屏时又绕回原点。这类问题的根源在于字符显示效果由字体和字号决定单片机侧点阵数据必须和驱动IC的扫描顺序严格匹配。基于Qt和C写一个点阵取字模软件就是把“字体渲染”和“数据包装”分开Qt负责把任意字符按指定字体、字号画到内存位图上C负责按你选定的取模模式把像素打包成C语言数组。换字体、改字号、换目标屏都只是改参数不用为每种屏重写一套工具。下面按这个思路展开先讲点阵方向与字节位序的核心概念再给可直接编译运行的Qt/C转换代码补上界面参数和导出设计最后说几个对接单片机工程时最常踩的坑。适合正在调LCD/OLED屏显、需要批量生成字模数据又不想被现成软件绑定的嵌入式或桌面开发者。2. 取模核心原理点阵方向、字节位序与取模模式2.1 从字体轮廓到点阵QPainter渲染与像素提取单片机显示屏的汉字显示本质是把“字形”网格化成0/1矩阵。传统做法直接读HZK16点阵字库一个16x16汉字固定占32字节但字库里的字形由国标统一设计字体单一、字号固定。产品界面需要微软雅黑、思源黑体或12x12窄体字时HZK16覆盖不了。在Qt里更直接用QPainter把字符画到QImage上。QImage是跨平台的内存像素缓冲区画完后用pixelColor()逐像素取出再按阈值切成二值。相比解析字库文件这样能直接用系统已安装字体QFont还能配置字号、加粗、斜体。典型代码如下QImage createGlyphImage(const QFont font, QChar ch, const QSize size) { QImage image(size, QImage::Format_ARGB32_Premultiplied); image.fill(Qt::white); QPainter painter(image); painter.setRenderHint(QPainter::TextAntialiasing, false); painter.setPen(Qt::black); painter.setFont(font); painter.drawText(QRect(QPoint(0, 0), size), Qt::AlignCenter, QString(ch)); painter.end(); return image; }因为先关掉了TextAntialiasing边缘像素是纯黑或纯白后端二值化不容易出“毛边”。如果产品想要更平滑显示也可以保留抗锯齿在二值化时用灰度阈值决定0/1第5章会专门讲。2.2 阴码/阳码、逐行式/逐列式先统一这四组概念取模软件最麻烦的不是画字而是“数据怎么排”。同一个“中”字不同模式生成的数组完全不一样写进驱动后可能镜像、颠倒、散乱。这张表是每换一块新显示屏驱动时要先确认的配置参数含义常见使用场景阳码像素点亮为1黑白屏SSD1306常规模式阴码像素点亮为0部分反白显示、段码LCD逐行式从上到下、从左到右扫描每8个像素凑1字节ST7789等RGB屏内建字模逐列式按列扫描先取垂直方向8个点SSD1306/SSD1315 OLED页寻址字节内高位在前bit7对应第一个像素常见单片机取模软件默认字节内低位在前bit0对应第一个像素部分LCD控制器硬件扫描顺序以16x16汉字为例逐行式生成2行×16列共32字节每行16像素切两个字节逐列式则在前8列各取8像素组成8字节后8列再组成8字节。驱动IC是页寻址时数据要按列喂给显存因此必须选逐列式拿逐行式直接发过去显示结果往往是字形上下裂开。16x16汉字的逐列式字节编号如下0~15 第0~15列的第0~7行上半部分 16~31 第0~15列的第8~15行下半部分SSD1306类驱动发送时一般先发上半部分16字节再发下半部分16字节。如果你的驱动库按其他顺序发送就必须在取模软件里提供对应的“数据重排”选项而不是到单片机端再去倒腾。2.3 位序打乱时的表现能提前判断的单片机故障特征字节内位序错常见表现是左右镜像或笔画错乱。视觉上字符依然横平竖直但左右位置换掉或笔画呈规律性错位此时优先怀疑字节位序。方向错了则更明显逐行数据被逐列方式解释时16x16汉字会裂成上下两半因为纵向8像素被当成了横向8像素。所以软件里最好把“方向”“极性”“位序”做成三个独立下拉框而不是塞进一个预设列表。预设列表方便新手入门但独立参数能让老手调驱动时逐个排除变量。下面第3章的转换函数就按这种独立参数设计这也是调试时最省力的结构。3. 用 Qt 和 C 实现的最小取字模转换核心3.1 把 QImage 像素二值化为按模式排列的字节数组画好位图后核心任务是把二维像素转成一维字节数组。先定义一个参数结构体把取模相关的三个变量独立出来struct ModeParams { bool rowMajor; // true逐行式, false逐列式 bool inverse; // true阴码, false阳码 bool msbFirst; // true高位在前, false低位在前 };这三个字段如果取错在屏幕上的表现完全不同字段取值含义选错时的现象rowMajortrue按行方向连续采样8像素逐列驱动下字符裂成上下两半rowMajorfalse按列方向连续采样8像素逐行驱动下字形错位、偏斜inversetrue点亮像素对应0白字变黑字黑字变白字msbFirsttrue8像素里第一个进入bit7左右镜像或像素水平位移转换函数如下QByteArray imageToByteArray(const QImage img, bool rowMajor, bool inverse, bool msbFirst) { QByteArray result; int w img.width(); int h img.height(); auto pixelAt [](int x, int y) - bool { QColor c img.pixelColor(x, y); int gray qGray(c.rgb()); return inverse ? (gray 128) : (gray 128); }; if (rowMajor) { for (int y 0; y h; y) { for (int x 0; x w; x 8) { uchar byte 0; for (int b 0; b 8; b) { bool bit (x b w) pixelAt(x b, y); int shift msbFirst ? (7 - b) : b; if (bit) byte | (1 shift); } result.append(byte); } } } else { for (int x 0; x w; x) { for (int y 0; y h; y 8) { uchar byte 0; for (int b 0; b 8; b) { bool bit (y b h) pixelAt(x, y b); int shift msbFirst ? (7 - b) : b; if (bit) byte | (1 shift); } result.append(byte); } } } return result; }逻辑说明pixelAt先做灰度转换再比较阈值。阳码时黑像素为1所以gray 128返回true阴码时反过来。移位方向由msbFirst控制高位在前时第一个像素进bit7低位在前时进bit0。这样无论驱动要求哪种位序都不用改数据只切参数。3.2 把整段文字按固定字模区域拼接取模软件通常不只是取一个字而是批量生成“温度 湿度 风速”这类菜单字符串。每个字符独立画一遍再连续追加到同一个QByteArray里QByteArray batchTextToBytes(const QString text, const QFont font, int areaWidth, int areaHeight, const ModeParams params) { QByteArray all; for (QChar ch : text) { if (ch \n) continue; QImage img createGlyphImage(font, ch, QSize(areaWidth, areaHeight)); QByteArray bytes imageToByteArray(img, params.rowMajor, params.inverse, params.msbFirst); all.append(bytes); } return all; }这里每个字符都使用固定areaWidth和areaHeight比如16x16或24x24。为什么不用字符的实际像素宽度因为单片机端取字模时通常按固定地址偏移计算索引每个字符数据长度必须一致。字体本身不是等宽的但固定取模区域后显示位置统一代价是窄字符会留点空白可接受。3.3 生成可直接编译的 C 语言数组文本数据生成之后还要格式化成C源码。常见输出格式如下const unsigned char code font_16x16[] { 0x00, 0x10, 0x7F, 0x10, 0x10, 0x10, 0x20, 0xFF, 0x20, 0x20, 0x20, 0x40, 0x40, 0x40, 0x00, 0x00, // ... };对应的C格式化函数QString bytesToCArray(const QByteArray bytes, int bytesPerLine) { QString out; out const unsigned char code font_array[] {\n; for (int i 0; i bytes.size(); i) { out QString(0x%1).arg((uchar)bytes[i], 2, 16, QLatin1Char(0)).toUpper(); if (i bytes.size() - 1) out , ; if ((i 1) % bytesPerLine 0) out \n; } out \n};\n; return out; }参数说明bytesPerLine通常设为8或16方便在Keil、IAR里检查对齐。Keil C51需要“code”关键字STM32平台不需要所以最好在界面上加一个“目标平台”复选框。QString::arg用两位十六进制不足位补0保证字节对齐。4. 软件界面与导出设计参数联动、预览和单片机驱动对接4.1 界面分区字体选择区、取模参数区、点阵预览区一个能实际用的取字模工具界面大致分三块。常见做法是用QSplitter左右分栏左边放参数右边放实时预览。参数区从上到下依次是字体、字号、取模方向、字节序再加“生成数组”按钮和QPlainTextEdit输出框。字体选择用QFontComboBox字号用QSpinBox。这里有个容易忽略的联动点选“逐列式”时高度要限制为8的倍数选“逐行式”时宽度要限制为8的倍数。否则生成的数据末尾缺位单片机端数组长度对不上。spinWidth-setRange(8, 128); spinHeight-setRange(8, 128); spinWidth-setSingleStep(8); connect(spinWidth, QSpinBox::valueChanged, this, [](int v) { spinWidth-setValue(v / 8 * 8); });这里把步进设为8再在值改变时向下取整用户选到17会跳回16从源头避免非8倍数尺寸。实际项目里更细的规则是逐列式需要宽度为1的倍数、高度为8的倍数逐行式反之。上面的简化写法只保证通用性。一把常用参数控件可以做成如下映射控件取值说明字体系统已安装字体支持中文、西文、符号字号8~128步进1对应字模画布高度字模宽度8~128步进8必须是8的倍数字模高度8~128步进8必须是8的倍数取模方向逐行式/逐列式与驱动扫描方向一致位序高位在前/低位在前决定左右镜像字体选择区保留多字体覆盖能力后续如果要支持俄语、泰语或自定义符号只需要在QFontComboBox里切换并重新生成即可不需要改任何点位算法。4.2 点阵预览按网格放大显示避免拉伸模糊预览的核心是用自定义绘制把点阵放大显示而不是直接拉伸QImage。常见的做法是继承QWidget重写paintEvent每个bit画一个方格class DotPreviewWidget : public QWidget { protected: void paintEvent(QPaintEvent*) override { QPainter p(this); int cellW width() / m_cols; int cellH height() / m_rows; for (int y 0; y m_rows; y) { for (int x 0; x m_cols; x) { bool on m_bytes[(y / 8) * m_cols x] (1 (7 - (y % 8))); p.fillRect(x * cellW, y * cellH, cellW, cellH, on ? Qt::black : Qt::white); } } } };这里m_cols按字模宽度计算逐列式预览时要按相同的列扫描方式重排字节否则预览和实际显示不一致。预览区的行列数必须与实际点阵尺寸一致而不是和控件像素尺寸一致这样你能在软件里先看到单片机屏幕上的效果。4.3 将生成的字模数组嵌入单片机驱动示例拿到字模数组后单片机端大致是这样用的。下面以SSD1306 OLED 12864为例显示一个16x16汉字void OLED_ShowChinese(uint8_t x, uint8_t y, const uint8_t *data) { uint8_t i; for (i 0; i 16; i) { // 上半部分第y页16列 OLED_Set_Pos(x i, y); OLED_Write_Data(data[i]); // 页寻址下 data[i] 是纵向8点 } for (i 0; i 16; i) { // 下半部分第y1页 OLED_Set_Pos(x i, y 1); OLED_Write_Data(data[i 16]); } }这段代码的前提是字模逐列式生成且上半部分16字节在前半段、下半部分16字节在后半段。如果取模软件把上下两部分交错输出这里就要改成先发偶数列再发奇数列。因此“打包顺序”必须和“驱动发送顺序”一致这是对接时第一个要确认的点。另一个常见坑是I2C通信时每页写入会受缓冲区限制如果一次写入超过64字节需要在驱动里分帧发送不然数据会丢。5. 四个实用验证技巧判断取模方向、处理抗锯齿和工程复用5.1 用绘图工具反向验证取模方向最直接的做法是写一个极小的Qt预览函数把生成的字模数组重新填回QImage按你理解的顺序画出来看它显示的是不是原字符。如果能对上说明软件里的参数组合和数组写入方向一致。void bytesToPreviewImage(const QByteArray bytes, int width, int height, QImage img) { img.fill(Qt::white); for (int i 0; i bytes.size(); i) { for (int b 0; b 8; b) { int bit (bytes[i] (7 - b)) 1; int row (i * 8 b) / width; int col (i * 8 b) % width; if (row height col width bit) img.setPixelColor(col, row, Qt::black); } } }如果这样填充显示正确但烧进屏幕方向不对重点检查驱动IC的扫描方向设置和Set_Pos函数别急着改取模软件。注意预览与屏幕方向相反时先查显存地址自增方向和页面寻址参数这是SSD1306上最容易漏的配置。5.2 抗锯齿字体的二值化阈值处理很多人直接用像素是否等于黑色来判断但在macOS或Linux上字体渲染的灰度像素很多。我一般把阈值设在128同时在真实屏幕上验证如果笔画边缘出现0x3f这类中间灰度值就提高阈值如果是细体字被截断就降一点。快速定位用灰度直方图QVectorint hist(256, 0); for (int y 0; y img.height(); y) for (int x 0; x img.width(); x) hist[qGray(img.pixelColor(x, y).rgb())];若hist在32~223区间有明显计数说明抗锯齿没完全关掉。先检查setRenderHint(Antialiasing, false)再看是否用了Alpha通道混合。低分辨率下即使关闭抗锯齿字体渲染引擎仍可能留下灰点这时只能靠阈值调整来平衡。5.3 字节长度自检防止数组越界生成完数组最好在软件里做一次长度自检不匹配就直接弹窗提示。自检逻辑可以直接复用3.1节的循环来计算期望长度而不是靠手写公式。因为同一个24x24汉字逐行式是3×2472字节逐列式是24×372字节看起来一样但一旦宽度或高度不是8的倍数两种模式长度就会不同。把期望长度放在生成函数内部每次导出时断言能省去大量手工数括号的麻烦。5.4 把配置序列化为JSON方便不同项目复用一个产品里可能有多套字模配置16x16中文、8x16数字、24x24特殊符号。把这些参数存成语义化配置下次启动直接加载比手动调可靠得多。用QJsonDocument保存{ fontFamily: Microsoft YaHei, fontSize: 16, width: 16, height: 16, rowMajor: false, inverse: false, msbFirst: true }加载之后把参数填回界面预览区会立刻更新。注意把“导出配置”和“导出C数组”分开避免用户拷贝数组时带进一堆JSON注释。配置里还可以保存最近一次使用的串口波特率、上板验证用的模拟数据但核心始终是字体、尺寸和三个取模方向参数。这套验证组合拳适合在调一块新屏时快速排查先做一次字节回读绘图确认软件内部一致再烧进单片机用屏幕显示效果和预览对比如果屏幕与预览不一致优先怀疑发送地址函数和显存寻址模式而不是取模软件。按这个顺序查整个联调过程通常几分钟内就能定位到是上位机数据排列问题还是驱动发送问题。本文还有配套的精品资源点击获取