用Qt C++实现点阵取字模工具:从原理到单片机显示验证
发布时间:2026/9/16 12:54:22来源:尧图网络
简介基于Qt/C开发的单片机点阵取字模软件面向嵌入式、自动化、电子信息等专业的学生与开发者旨在解决LED点阵屏字符与图形取模繁琐的问题。软件提供可视化点阵编辑界面可将汉字、字符或图形快速转换为单片机可用的字模数据并支持格式调整与导出适合课程设计、毕业设计及项目二次开发。资源共78个文件以SVG矢量图标、QSS样式表、CPP源文件、UI界面文件、QRC资源文件为主包体仅294KB目录结构简洁清晰便于定位源码、界面与资源配置。项目代码已经过测试运行功能稳定曾获导师认可答辩评审分95分附详细文档适合需要完整参考实现的人群。目前已有84人学习可放心下载研究。1. 点阵取字模为什么值得用Qt C重写一遍单片机显示驱动开发里取字模是流程中特别容易被卡住的一环。51单片机、STC这类平台的RAM通常只有几百字节到几KBOLED或LCD上要显示中文时系统内存带不动字库可行方案就是把每个文字提前拆成点阵字节写进Flash。市面上的取模工具版本普遍老旧扫描方式经常和具体屏的驱动芯片对不上默认输出的字节序也要手工去改改错一个位屏上显示的就是镜像字或乱码。用Qt C实现一个点阵取字模工具本质是把字体渲染、逐像素扫描、字节拼接和代码导出固化成一条可配置的流水线能针对自家屏的参数随时调整也方便扩展批量取模、预览回画这类功能。这篇文章从取模原理讲到Qt C落地实现再给出写入单片机前的验证思路适合正在写显示驱动、被字模坑过的工程师。2. 字模结构与取模方向先弄清字节里每一位的含义2.1 16×16点阵与32字节数组的对应关系单片机取模最常见的场景有两类ASCCII字符用8×8或8×16汉字用16×16。16×16点阵包含256个像素点8个点合并成一个字节所以总字节数是32。驱动代码里定义字库时最常见的写法是这样const unsigned char code font16x16[][32] { // 每个汉字占32字节 {0x00, 0x04, /* ... */ }, };code关键字是51单片机的常量段修饰符STM32平台上则用const uint8_t。这里无论用哪个关键字[32]这个容量都来自16×16点阵的结构不能随意改动。如果某天发现数组长度变成33或34多半是字号设置出了问题而不是编译器多给了空间。点阵字节在数组里的排列取决于取模工具如何把“行”切分。横向取模逐行式的逻辑是每一行16个点拆成左右两个字节左边字节表示第07列右边字节表示第815列再从第0行开始自上而下扫完16行。于是有了下面这张固定的映射表字节索引屏幕行覆盖的列范围含义0第0行第07列左上角字节1第0行第815列右上角字节2第1行第07列第二行左半3第1行第815列第二行右半…………30第15行第07列最后一行左半31第15行第815列最后一行右半如果驱动芯片是SSD1306这类OLED屏幕按8像素一页的方式组织数据显示汉字时通常改用纵向逐列式取模以一列为单位从上到下8个点合成一个字节再从左到右推进。同一套字库横向和纵向取出来的字节序列完全不同写入驱动后的显示结果自然也不同所以第一步就得确认屏幕控制器的数据组织方式。2.2 横向、纵向与逆向取模方向决定字节顺序取模方向直接决定字节在数组里的先后顺序这也是网上现成工具最容易出错的地方。常见的扫描方式需要从三个维度确认按行还是按列、从左还是从右、从上还是从下。我一般把选项按下面的表拆开取模方向字节生成顺序适用面逐行正向从左到右自上而下并口LCD、TFT、带字库模块逐行逆向从右到左自上而下数据线接法反向的屏逐列正向从上到下自左而右SSD1306、ST7567等OLED逐列逆向从下到上自右而左面板旋转或镜像显示一个常见的低级错误是把取模方向和汉字编码顺序混为一谈。取模方向描述的是“字节内位和字节间顺序”汉字编码顺序描述的是“字库索引方式”两者互不影响。比如用GB2312索引“中”和“国”对应各自的32字节与取模方向无关同一个字在不同取模方向下只是32字节的内容和排列不一样字与字的相对编号不会变。2.3 字节内的位序选择MSB在前还是LSB在前字节排列方向之外还有一个更隐蔽的位序问题。逐行取模时一行拆成两组8像素点最左边那个点对应的是bit7还是bit0由驱动芯片读取数据时的位序决定。多数屏幕驱动是高位在前即最左点对应0x80。用Qt做取模时从QImage里读到的像素坐标是绝对的(x, y)需要自己把坐标换算进字节里的位// pixel为0表示黑点笔画col为列坐标范围0~7 if (pixel 0) { byte | (0x80 col); // 高位在前最左点落在bit7 }0x80 col就是高位在前的写法如果目标屏要求低位在前把它换成1 col即可。位序这个参数最好在界面里做成选项因为在实物屏上错了表现就是左右镜像而且不容易从数组内容上看出来。2.4 阴码与阳码背景像素要不要输出字节里每一位的含义也有两种约定。笔画所在的点置1、背景置0叫阳码反过来笔画置0、背景置1叫阴码。OLED和TFT常见反色显示需求某些驱动代码里也会直接做~buf[i]取反这时如果取模工具输出的已经是阴码叠加到驱动里就会整屏反转。取模工具里需要提供阴码/阳码切换实现上其实就是输出前对每个字节做一次按位取反。这个操作可以放到字节拼接完成后统一处理不要把阴码阳码的逻辑散落在扫描循环里否则后面加新功能时很容易漏改一处。选项名可以明确写成“输出反色数据”避免用户不知道阴码阳码分别是什么意思。3. 用Qt C把汉字画成点阵再转成字节数组3.1 用QFont和QImage得到无抗锯齿的点阵位图取字模的起点是获得一张“字形的黑白位图”。自己解析TrueType轮廓不划算Qt的字体引擎已经做完了这个工作QFont负责选字形QPainter负责绘制QImage负责承载像素。真正需要操心的不是如何描述汉字而是如何让字体引擎不要“美化”输出关闭抗锯齿、关闭亚像素渲染、用像素尺寸而不是磅值。我把渲染函数封装成下面这种形式界面和批量取模都复用它#include QImage #include QPainter #include QFont QImage renderGlyph(const QString ch, int width, int height) { QImage image(width, height, QImage::Format_Mono); // 1bit/像素非黑即白 image.fill(1); // 背景填充为白 QPainter painter(image); painter.setRenderHint(QPainter::Antialiasing, false); painter.setRenderHint(QPainter::TextAntialiasing, false); QFont font(QStringLiteral(SimSun)); // 取模用中文字体 font.setPixelSize(height); // 像素尺寸严格等于点阵高 font.setStyleStrategy(QFont::NoAntialias); painter.setFont(font); painter.drawText(image.rect(), Qt::AlignCenter, ch); painter.end(); return image; }这里有两个关键点。QImage::Format_Mono把每个像素压缩成1位后续读像素时不会出现灰度过渡setPixelSize按像素设置字号保证字形尺寸和点阵宽高严格一致。如果改用font.setPointSize最终位图高度会按屏幕DPI换算16号的字在某些高分屏上可能渲染成18或20像素数组长度随之变化。image.fill(1)的含义也值得说一句Format_Mono下1表示白色背景0表示黑色笔画。后续取模时判断像素为0的位置才是有效点。如果程序跑在不同的显示后端上建议先打印image.pixel(0,0)确认黑白定义有的平台对单色位图做了颜色反转。3.2 按扫描方向逐位读取并拼装字节拿到黑白位图后原理就很直接了逐行或逐列扫描像素按上一章确定的规则把位组装成字节。逐行扫描的完整实现如下QByteArray makeRowMajor(const QImage img, int width, int height) { QByteArray data; data.reserve((width / 8) * height); for (int y 0; y height; y) { for (int x 0; x width; x 8) { quint8 byte 0; for (int bit 0; bit 8; bit) { QRgb c img.pixel(x bit, y); if (qRed(c) 0) { // 黑点为笔画 byte | (0x80 bit); // MSB在前 } } data.append(static_castchar(byte)); } } return data; }这个函数对16×16输入会返回32字节。循环的逻辑是外层按行内层先按8像素分组第三层再逐位判断。0x80 bit对应高位在前最左像素落在bit7最右像素落在bit0。如果屏幕驱动要求低位在前把这句改成byte | (1 bit)。3.3.1 大尺寸点阵下的像素读取性能img.pixel()每次调用都会做坐标到内存地址的换算16×16时影响不大但点阵放大到32×32或48×48后批量取模的速度会明显下降。这时建议改用img.bits()直接读位图内存bool isBlackFast(const QImage img, int x, int y) { const uchar *bits img.bits(); int byteIndex y * img.bytesPerLine() x / 8; return (bits[byteIndex] (0x80 (x % 8))) 0; }注意这里要用bytesPerLine()计算行偏移不要自己按width / 8去算。QImage内部为了内存对齐每行字节数可能比实际数据多出几个填充字节写死步长会在图片宽度不是8的倍数时读到错位。3.3 把字节数组格式化成可粘贴的C源码取模出来的字节最后要进单片机工程格式化成C数组是工具的收尾环节。输出格式需要支持51单片机的code关键字和ARM平台的const我一般把数组定义写成模板函数QString formatArray(const QByteArray data, const QString typeName) { QString out; out QString(const unsigned char %1[] {\n).arg(typeName); for (int i 0; i data.size(); i) { out QString(0x%1) .arg(static_castquint8(data[i]), 2, 16, QLatin1Char(0)); if (i ! data.size() - 1) out , ; if ((i 1) % 8 0) out \n; } out \n};\n; return out; }参数2, 16, QLatin1Char(0)的含义是最小宽度2位、十六进制、左边补0。有个容易踩的坑QString::arg接收int如果直接传data[i]有符号char会被转成负数输出变成0xffffff80这种8位十六进制整个数组就废了所以必须先static_castquint8回无符号类型。3.4 字体选择与覆盖范围对取模结果的影响取模工具的实用性很大程度上由字体决定。系统字体列表可以通过QFontDatabase::families()枚举我建议在界面里放一个“仅显示中文字体”的过滤条件默认选中宋体或黑体。等宽字体对ASCII和数字的取模更整齐显示英文时可以切换。中文字体的覆盖范围也有坑部分精简字体的汉字数量只有几千个生僻字渲染出来是空框取模成功但屏幕显示空白排查起来很费劲。取模前最好把目标字集和字体覆盖范围做一次比对或者直接提醒用户换成全字库字体这样至少不会出现“取模能过、屏幕不显示”的诡异问题。4. 界面与参数面板让取模工具适配不同屏幕驱动4.1 控件布局参数区、预览区、代码区的分工取模工具的界面不宜复杂核心区域就是三块左侧参数区、中间预览区、下方代码区。参数区用一组QComboBox和QSpinBox纵向排放从上到下依次是字体、字号、取模方向、位序、阴码阳码。字号用QSpinBox并允许单步1像素调整范围设置在8到48比较合理。取模是像素级操作字号跨度太大会让预览区频繁重绘拖慢交互。取模方向用QComboBox枚举“逐行、逐列、逆向”几个选项切换后立即重算当前字符的字模并刷新界面。4.2 信号槽联动与实时刷新界面的价值在于即时反馈。不要在用户点“取模”按钮后才刷新应该在QLineEdit的textChanged信号里触发更新。刷新做两件事把当前字符重新渲染成位图显示在预览区同时更新下方代码区的内容。void MainWindow::onTextChanged(const QString text) { if (text.isEmpty()) { return; } const QChar ch text.right(1).at(0); const int sizeVal ui_ -sizeSpinBox-value(); QImage img renderGlyph(QString(ch), sizeVal, sizeVal); ui_ -previewLabel-setPixmap(scaleForPreview(img)); updateCodeArea(img); }scaleForPreview不能直接调用QPixmap::fromImage后拉伸那样会产生平滑过渡的假象看不出真实点阵边界。更好的做法是按像素放大比如每个原始点画成8×8的小方格再叠加网格线。这样预览区看到的就是和最终显示一致的像素块比抗锯齿渲染直观得多。4.3 批量取模、去重与进度反馈批量取模的目标是把一段文本一次性转成多组32字节。这里要处理两个实际问题字符去重和进度反馈。去重是因为文本里重复的字没必要反复取模按字符顺序去重后直接生成数组能节省不少Flash空间。进度反馈用QProgressDialog处理长文本时保持UI响应QProgressDialog progress(tr(正在取模...), tr(取消), 0, chars.size(), this); progress.setWindowModality(Qt::WindowModal); for (int i 0; i chars.size(); i) { if (progress.wasCanceled()) { break; } QImage img renderGlyph(chars[i], sizeVal, sizeVal); glyphMap.insert(chars[i], makeRowMajor(img, sizeVal, sizeVal)); progress.setValue(i 1); QCoreApplication::processEvents(); }processEvents()手动处理事件队列里待处理的事件让进度条能及时刷新不然批量取模时窗口会像卡死一样。批量输出除了字模数组本身还要同时生成“字符索引映射”单片机侧才能知道第几个字模对应哪个字符。常见做法是维护下面这个结构体struct GlyphItem { QString ch; // 字符本身 QByteArray data; // 取出的字模字节 };导出后驱动侧遍历这个数组比对字符编码找到匹配项后直接拷贝32字节到显存缓冲区。4.4 按目标单片机平台切换导出模板不同编译器和单片机的常量关键字不一样。51单片机用unsigned char code font[][32]STM32常用const uint8_t font[][32]。界面里用一个QComboBox选择目标平台导出时替换数组声明和头文件包含。声明好格式模板后后续新增平台只改模板字符串不碰取模逻辑这是把工具沉淀下来的关键一步。5. 字模进单片机前先在PC端回画验证一遍5.1 把32字节数组反向渲染回点阵图像字模烧进单片机后出问题定位成本比PC端高得多。取模工具输出前完全可以在PC端做一次“回画”把生成的32字节按同样的扫描方式还原成像素点刷新到预览区。如果回画结果和原始字符一致说明取模链路没有坏。回画是取模的逆运算核心代码QImage drawBack(const QByteArray data, int width, int height) { QImage img(width, height, QImage::Format_Mono); img.fill(1); int idx 0; for (int y 0; y height; y) { for (int x 0; x width; x 8) { quint8 byte static_castquint8(data[idx]); for (int bit 0; bit 8; bit) { if (byte (0x80 bit)) { img.setPixel(x bit, y, 0); } } } } return img; }把回画图和渲染原图做像素级对比能立刻发现取模方向设置和位序设置的问题。逐列取模模式下的回画逻辑要把外层循环换成列内层改成行参数不一致时回画会直接花屏比人眼查数组高效得多。这个验证步骤应该做成界面上的一个“校验”按钮一键把两个QImage逐像素比较返回差异点坐标。5.2 三个高频坑与排查位置实际项目中字模出错最常见的位置就三个数组元素数量不匹配。16×16必须32字节字号设置成17后数组就会变成33字节以上驱动按32字节拷贝就会整体错位。检查时先数输出数组长度再对照点阵尺寸。编码不一致。取模工具输出时用UTF-8保存单片机编译器按GB2312索引字符串首字符对应关系就乱了。导出选项里必须明确编码格式取模端和编译端保持一致。显示方向反转。竖屏和横屏切换场景下不要在驱动里逐像素翻转那会浪费运行期资源。直接在取模工具里选逆向取模把反转交给离线流程处理。5.3 把工具参数固化成配置文件减少团队返工如果项目里有多块不同参数的屏幕这个Qt工具最终会自然演进成一个小库字体列表、取模方向、导出模板都存进一个配置文件不同项目组共享同一个可执行文件避免每个人手里都留着一份不同参数的工具。源码和文档一起交付后新同事改参数时只看配置不看代码也不容易把取模方向改错。这个配置化的思路比在代码里堆一堆硬编码常量要省心得多。本文还有配套的精品资源点击获取
网站建设高端定制企业官网