嵌入式应用实例→电子产品量产工具→UI界面的绘制和测试
前言
之前已经在博文https://blog.csdn.net/wenhao_ir/article/details/144747714中实现了用Freetype在LCD屏上绘制字符,本篇博文我们利用Freetype实现UI界面的绘制。
头文件include\ui.h的分析
头文件内的代码
#ifndef _UI_H
#define _UI_H#include <common.h>
#include <disp_manager.h>
#include <input_manager.h>#define BUTTON_DEFAULT_COLOR 0xff0000
#define BUTTON_PRESSED_COLOR 0x00ff00
#define BUTTON_TEXT_COLOR 0x000000struct Button;typedef int (*ONDRAW_FUNC)(struct Button *ptButton, PDispBuff ptDispBuff);
typedef int (*ONPRESSED_FUNC)(struct Button *ptButton, PDispBuff ptDispBuff, PInputEvent ptInputEvent);typedef struct Button {char *name;int status;Region tRegion;ONDRAW_FUNC OnDraw;ONPRESSED_FUNC OnPressed;
}Button, *PButton;void InitButton(PButton ptButton, char *name, PRegion ptRegion, ONDRAW_FUNC OnDraw, ONPRESSED_FUNC OnPressed);#endif
代码struct Button;
在 C 语言中,struct Button; 是一种 前向声明,其作用是告诉编译器“存在一个名为 struct Button 的结构体,但我现在不打算定义它的具体内容”。具体用途如下:
为什么使用 struct Button;?
-
为指针定义类型
前向声明允许你在结构体定义之前声明指向该结构体的指针类型。这在需要定义互相引用的结构体或函数时很有用。
例如:struct Button; // 前向声明 typedef struct Button *PButton; // 定义指向该结构体的指针类型 -
避免完整定义的开销
如果某些地方只需要使用struct Button的指针而不需要了解其完整内容,前向声明可以减少编译依赖,从而加快编译速度。
为什么这里需要 struct Button;?
在后成的代码中:
typedef int (*ONDRAW_FUNC)(struct Button *ptButton, PDispBuff ptDispBuff);
typedef int (*ONPRESSED_FUNC)(struct Button *ptButton, PDispBuff ptDispBuff, PInputEvent ptInputEvent);
用到了结构体 struct Button 。
代码typedef int (*ONDRAW_FUNC)(struct Button *ptButton, PDispBuff ptDispBuff);
这段代码是 C 语言中 函数指针类型 的定义。我们逐步分析:
定义拆解
typedef int (*ONDRAW_FUNC)(struct Button *ptButton, PDispBuff ptDispBuff);
-
typedeftypedef用于为一个已存在的类型定义一个新的类型别名。- 在这里,它为一个特定类型的函数指针定义了别名
ONDRAW_FUNC。
-
函数指针
(*ONDRAW_FUNC)定义了一个函数指针,表示ONDRAW_FUNC是指向某种函数的指针。- 这个函数的原型是:
也就是说,它接收两个参数,返回一个int Function(struct Button *ptButton, PDispBuff ptDispBuff);int类型的值。
-
参数类型
struct Button *ptButton
指向一个struct Button类型的指针。通过它,函数可以操作一个Button对象,Button对象的定义在后面。PDispBuff ptDispBuff
假设PDispBuff是一个类型别名(可能定义为typedef DispBuff *PDispBuff),表示DispBuff类型的指针。这是FrameBuffer的显示缓冲区的指针。
-
返回类型
int
表示函数执行的结果是一个整数,通常用来表示状态码(如 0 表示成功,非 0 表示失败)。
使用方式
-
声明函数指针变量
ONDRAW_FUNC myDrawFunc;这里
myDrawFunc是一个指向函数的指针,它的函数原型符合ONDRAW_FUNC定义。 -
定义符合原型的函数
int MyButtonDraw(struct Button *ptButton, PDispBuff ptDispBuff) {// 绘制按钮的逻辑return 0; // 成功 } -
赋值并调用
myDrawFunc = MyButtonDraw; // 将函数指针指向具体实现 myDrawFunc(ptButton, ptDispBuff); // 通过指针调用函数
具体用途
ONDRAW_FUNC 的设计通常用于 回调机制,允许将函数指针作为参数传递,或在结构体中保存,提供灵活的扩展能力。在你的代码中,ONDRAW_FUNC 是一个绘制按钮的回调函数,它的用途包括:
- 在按钮需要绘制时,调用该函数实现具体的绘制逻辑。
- 提供不同的绘制方法(比如改变样式或颜色),而无需修改其他代码。
代码typedef int (*ONPRESSED_FUNC)(struct Button *ptButton, PDispBuff ptDispBuff, PInputEvent ptInputEvent);
这里面涉及到的将函数指针定义为一个类型的语法知识这里不再赘述,只说下几个参数的意义。
ptButton:这是一个Button结构体类似的指针,Button的定义见后面
ptDispBuff:这是FrameBuffer的显示缓冲区的指针。
ptInputEvent:这里面存储着来自触摸屏的输入数据:
typedef struct InputEvent {struct timeval tTime;int iType;int iX;int iY;int iPressure;char str[1024];
}InputEvent, *PInputEvent;
结构体Button
typedef struct Button {char *name;int status;Region tRegion;ONDRAW_FUNC OnDraw;ONPRESSED_FUNC OnPressed;
}Button, *PButton;
这个结构体就代表屏幕上的一个一个按钮(下图中,一个框就是一个按钮):

name代表一个按钮的名字;
status代表按钮的状态;
tRegion代表按钮的显示区域;
函数指针OnDraw用于区域的绘制;
函数指针OnPressed用于对点击按钮的处理。
文件ui\button.c的分析
按钮初始化函数InitButton()
void InitButton(PButton ptButton, char *name, PRegion ptRegion, ONDRAW_FUNC OnDraw, ONPRESSED_FUNC OnPressed)
{ptButton->status = 0;ptButton->name = name;ptButton->tRegion = *ptRegion;ptButton->OnDraw = OnDraw ? OnDraw : DefaultOnDraw;ptButton->OnPressed = OnPressed ? OnPressed : DefaultOnPressed;
}
这个没啥好说的,就是对结构体PButton的实例ptButton进行实始化赋值处理。
绘制按钮和文字的函数DefaultOnDraw()
static int DefaultOnDraw(struct Button *ptButton, PDispBuff ptDispBuff)
{/* 绘制底色 */DrawRegion(&ptButton->tRegion, BUTTON_DEFAULT_COLOR);/* 居中写文字 */DrawTextInRegionCentral(ptButton->name, &ptButton->tRegion, BUTTON_TEXT_COLOR);/* flush to lcd/web */FlushDisplayRegion(&ptButton->tRegion, ptDispBuff);return 0;
}
这个没啥好讲的,注释已经写得很清楚了,需要注意的就是函数FlushDisplayRegion()对于咱们这进而的LCD屏其实是没必要的,因为咱们这里的LCD屏,其存储区的值变了,对应的屏幕上的颜色也就变了。
点击处理函数DefaultOnPressed()
static int DefaultOnPressed(struct Button *ptButton, PDispBuff ptDispBuff, PInputEvent ptInputEvent)
{unsigned int dwColor = BUTTON_DEFAULT_COLOR;ptButton->status = !ptButton->status;if (ptButton->status)dwColor = BUTTON_PRESSED_COLOR;/* 绘制底色 */DrawRegion(&ptButton->tRegion, dwColor);/* 居中写文字 */DrawTextInRegionCentral(ptButton->name, &ptButton->tRegion, BUTTON_TEXT_COLOR);/* flush to lcd/web */FlushDisplayRegion(&ptButton->tRegion, ptDispBuff);return 0;
}
文件display\disp_manager.c分析
以某种颜色填充区域的函数DrawRegion()
void DrawRegion(PRegion ptRegion, unsigned int dwColor)
{int x = ptRegion->iLeftUpX;int y = ptRegion->iLeftUpY;int width = ptRegion->iWidth;int heigh = ptRegion->iHeigh;int i,j;for (j = y; j < y + heigh; j++){for (i = x; i < x + width; i++)PutPixel(i, j, dwColor);}
}
这个函数用来把某个矩形区域绘制成一种颜色,实际上就是以某种颜色填充某块矩形区域。
在区域中居中书写文字的函数DrawTextInRegionCentral()
void DrawTextInRegionCentral(char *name, PRegion ptRegion, unsigned int dwColor)
{int n = strlen(name);int iFontSize = ptRegion->iWidth / n / 2;FontBitMap tFontBitMap;int iOriginX, iOriginY;int i = 0;int error;if (iFontSize > ptRegion->iHeigh)iFontSize = ptRegion->iHeigh;iOriginX = (ptRegion->iWidth - n * iFontSize)/2 + ptRegion->iLeftUpX;iOriginY = (ptRegion->iHeigh - iFontSize)/2 + iFontSize + ptRegion->iLeftUpY;SetFontSize(iFontSize);while (name[i]){/* get bitmap */tFontBitMap.iCurOriginX = iOriginX;tFontBitMap.iCurOriginY = iOriginY;error = GetFontBitMap(name[i], &tFontBitMap);if (error){printf("SelectAndInitFont err\n");return;}/* draw on buffer */ DrawFontBitMap(&tFontBitMap, dwColor); iOriginX = tFontBitMap.iNextOriginX;iOriginY = tFontBitMap.iNextOriginY; i++;}}
这个没啥好说的,关键是确定字符串的起始位置,不过这个的算法也不难。
测试单元main函数分析
int main(int argc, char **argv)
{PDispBuff ptBuffer;int error;Button tButton;Region tRegion;if (argc != 2){printf("Usage: %s <font_file>\n", argv[0]);return -1;}DisplayInit();SelectDefaultDisplay("fb");InitDefaultDisplay();ptBuffer = GetDisplayBuffer();FontsRegister();error = SelectAndInitFont("freetype", argv[1]);if (error){printf("SelectAndInitFont err\n");return -1;}tRegion.iLeftUpX = 200;tRegion.iLeftUpY = 200;tRegion.iWidth = 300;tRegion.iHeigh = 100;InitButton(&tButton, "UI_test", &tRegion, NULL, NULL);tButton.OnDraw(&tButton, ptBuffer);while (1){tButton.OnPressed(&tButton, ptBuffer, NULL);sleep(2);}return 0;
}
这个流程就很简单了,首先对FramBuffer设备(LCD设备)进行初始化,然后对Freetype库进行初始化,接着就可以在LCD屏上进行UI界面的绘制了。由于我们只是测试UI界面的绘制,还没有进行UI界面的交互功能的开发,所以这里就没有进行tslib库的设置和初始化。
交叉编译
首先把freetype的文件复制到工程的include文件中,然后记着把Makefile文件修改好。
接着代码复制到Ubuntu中。

make
把生成的可执行文件test重命名为:UI_test,并在NFS文件中准备好下面三个文件备用:

板上测试
mount -t nfs -o nolock,vers=3 192.168.5.11:/home/book/nfs_rootfs /mnt
cd /mnt/UI_test
先把屏幕刷黑:
./draw_lcd_black
然后运行咱们这里的测试程序(由于程序是处于while循环中,所以这里让它后台运行):
./UI_test ./simsun.ttc &
就得到了我们想要的结果(每隔2秒重新绘制一次,绿色和红色交替进行):


附完整源代码
https://pan.baidu.com/s/1-HGuKQj4lpFn6xPagaBSEA?pwd=n19t
相关文章:
嵌入式应用实例→电子产品量产工具→UI界面的绘制和测试
前言 之前已经在博文https://blog.csdn.net/wenhao_ir/article/details/144747714中实现了用Freetype在LCD屏上绘制字符,本篇博文我们利用Freetype实现UI界面的绘制。 头文件include\ui.h的分析 头文件内的代码 #ifndef _UI_H #define _UI_H#include <common…...
如何删除 Docker 中的悬虚镜像?
在 Docker 中,悬虚镜像(Dangling Images)是指那些没有 标签 且没有被任何容器使用的镜像。这些镜像通常是由于构建过程中生成的中间层镜像或未正确清理的镜像残留。删除悬虚镜像可以释放磁盘空间并保持 Docker 环境的整洁。 1. 列出悬虚镜像…...
el-table树形懒加载展开改为点击行展开
思路:获取el-table中小箭头,然后调它的click事件! <el-tablerow-click"getOpenDetail":row-class-name"tableRowClassName">// 点击当前行展开节点getOpenDetail(row, column, event) {// 如果是叶子节点或点击的是…...
【Ubuntu】Ubuntu server 18.04 搭建Slurm并行计算环境(包含NFS)
Ubuntu server 18.04 搭建Slurm并行计算环境(包含NFS) 一、Munge 认证模块 1.1、安装 munge 主节点和子节点都安装munge #安装 sudo apt update && sudo apt install munge libmunge-dev#设置开机启动 sudo systemctl enable munge sudo syste…...
高并发场景下的秒杀系统架构设计与实现
引言 秒杀系统是一种高并发场景的典型应用,广泛存在于电商平台、抢票系统和促销活动中。秒杀活动的特点是短时间内吸引大量用户同时访问并尝试抢购商品,这对系统的高并发处理能力、稳定性和用户体验提出了极高的要求。 在秒杀系统中,常见的…...
搭建开源版Ceph分布式存储
系统:Rocky8.6 三台2H4G 三块10G的硬盘的虚拟机 node1 192.168.2.101 node2 192.168.2.102 node3 192.168.2.103 三台虚拟机环境准备 1、配置主机名和IP的映射关系 2、关闭selinux和firewalld防火墙 3、配置时间同步且所有节点chronyd服务开机自启 1、配置主机名和…...
QT----------多媒体
实现思路 多媒体模块功能概述: QT 的多媒体模块提供了丰富的功能,包括音频播放、录制、视频播放和摄像头操作等。 播放音频: 使用 QMediaPlayer 播放完整的音频文件。使用 QSoundEffect 播放简短的音效文件。 录制音频: 使用 QMe…...
选择器(结构伪类选择器,伪元素选择器),PxCook软件,盒子模型
结构为类选择器 伪元素选择器 PxCook 盒子模型 (内外边距,边框) 内外边距合并,塌陷问题 元素溢出 圆角 阴影: 模糊半径:越大越模糊,也就是越柔和 案例一:产品卡片 <!DOCTYPE html> <html lang&q…...
Vue2/Vue3 响应式原理对比指南
Vue2/Vue3 响应式原理对比指南 1. 基本实现原理 1.1 Vue2 响应式实现 (Object.defineProperty) // Vue2 响应式核心实现 function defineReactive(obj, key, val) {// 递归处理嵌套对象observe(val);const dep new Dep();Object.defineProperty(obj, key, {get() {// 依赖收…...
FastExcel:超越EasyExcel的新一代Excel处理工具
简介 FastExcel是由原EasyExcel作者在阿里巴巴宣布停止维护EasyExcel之后推出的升级版框架。它继承了EasyExcel的所有优点,并且在性能和功能上进行了显著的提升和创新。 FastExcel的特点 高性能读写:FastExcel专注于性能优化,能够高效处理…...
大模型系列17-RAGFlow搭建本地知识库
大模型系列17-RAGFlow搭建本地知识库 安装ollama安装open-wehui安装并运行ragflowRAG(检索、增强、生成)RAG是什么RAG三过程RAG问答系统构建步骤向量库构建检索模块生成模块 RAG解决LLM的痛点 使用ragflow访问ragflow配置ollama模型添加Embedding模型添加…...
常用的mac软件下载地址
目录 iRightMouse Pro(超级右键) xmind(思维导图) Parallels Desktop(虚拟机工具) Paste(跨平台复制粘贴) AutoSwitchInput Pro(自动切换输入法) Snipa…...
基于51单片机和16X16LED点阵屏(74HC138和74HC595驱动)的小游戏《贪吃蛇》
目录 系列文章目录前言一、效果展示二、原理分析三、各模块代码1、定时器02、自制八位独立按键3、点阵屏模块 四、主函数总结 系列文章目录 前言 《贪吃蛇》,一款经典的、怀旧的小游戏,单片机入门必写程序。 以《贪吃蛇》为载体,熟悉各种屏…...
python中常用的内置函数介绍
python中常用的内置函数介绍 1. print()2. len()3. type()4. str(), int(), float()5. list(), tuple(), set(), dict()6. range()7. sum()8. max(), min()9. sorted()10. zip()11. enumerate()12. map()13. filter()14. any(), all()15. abs()16. pow()17. round()18. ord(), …...
【微服务】Spring Cloud Config解决的问题和案例
文章目录 强烈推荐引言解决问题1. 配置管理的集中化2. 配置的版本控制3. 环境特定配置4. 配置的动态刷新5. 安全管理敏感数据6. 配置的一致性 组件1. **配置服务器(Config Server)**2. **配置客户端(Config Client)** 配置示例配置…...
华为OD机试E卷 --最小的调整次数--24年OD统一考试(Java JS Python C C++)
文章目录 题目描述输入描述输出描述用例题目解析JS算法源码Java算法源码python算法源码c算法源码c++算法源码题目描述 有一个特异性的双端队列一,该队列可以从头部或尾部添加数据,但是只能从头部移出数据。 小A依次执行2n个指令往队列中添加数据和移出数据。其中n个指令是添…...
Oracle Dataguard(主库为 Oracle 11g 单节点)配置详解(2):配置主数据库
Oracle Dataguard(主库为 Oracle 11g 单节点)配置详解(2):配置主数据库 目录 Oracle Dataguard(主库为 Oracle 11g 单节点)配置详解(2):配置主数据库一、配置…...
慧集通iPaaS集成平台低代码训练-实践篇
练习使用帐号信息: 1.致远A8平台(请自行准备测试环境) 慧集通连接器配置相关信息 访问地址: rest账号:rest rest密码: OA账号: 2.云星空(请自行准备测试环境) 连接…...
TDengine 如何进行高效数据建模
1.背景 数据建模对于数据库建立后整体高效运行非常关键,不同建模方式,可能会产生相差几倍的性能差别 2. 建库 建模在建库阶段应考虑几下几点: 建多少库 根据业务情况确定建库个数,TDengine 不支持跨库查询,如果业…...
HarmonyOS NEXT应用开发实战:一分钟写一个网络接口,JsonFormat插件推荐
在开发鸿蒙操作系统应用时,网络接口的实现往往是一个繁琐且重复的过程。为了提高开发效率,坚果派(nutpi.net)特别推出了一个非常实用的插件——JsonFormat。这款插件的主要功能是将JSON格式的数据直接转换为arkts的结构定义,让我们在编写接口…...
在软件开发中正确使用MySQL日期时间类型的深度解析
在日常软件开发场景中,时间信息的存储是底层且核心的需求。从金融交易的精确记账时间、用户操作的行为日志,到供应链系统的物流节点时间戳,时间数据的准确性直接决定业务逻辑的可靠性。MySQL作为主流关系型数据库,其日期时间类型的…...
QMC5883L的驱动
简介 本篇文章的代码已经上传到了github上面,开源代码 作为一个电子罗盘模块,我们可以通过I2C从中获取偏航角yaw,相对于六轴陀螺仪的yaw,qmc5883l几乎不会零飘并且成本较低。 参考资料 QMC5883L磁场传感器驱动 QMC5883L磁力计…...
什么是库存周转?如何用进销存系统提高库存周转率?
你可能听说过这样一句话: “利润不是赚出来的,是管出来的。” 尤其是在制造业、批发零售、电商这类“货堆成山”的行业,很多企业看着销售不错,账上却没钱、利润也不见了,一翻库存才发现: 一堆卖不动的旧货…...
汇编常见指令
汇编常见指令 一、数据传送指令 指令功能示例说明MOV数据传送MOV EAX, 10将立即数 10 送入 EAXMOV [EBX], EAX将 EAX 值存入 EBX 指向的内存LEA加载有效地址LEA EAX, [EBX4]将 EBX4 的地址存入 EAX(不访问内存)XCHG交换数据XCHG EAX, EBX交换 EAX 和 EB…...
RNN避坑指南:从数学推导到LSTM/GRU工业级部署实战流程
本文较长,建议点赞收藏,以免遗失。更多AI大模型应用开发学习视频及资料,尽在聚客AI学院。 本文全面剖析RNN核心原理,深入讲解梯度消失/爆炸问题,并通过LSTM/GRU结构实现解决方案,提供时间序列预测和文本生成…...
Spring是如何解决Bean的循环依赖:三级缓存机制
1、什么是 Bean 的循环依赖 在 Spring框架中,Bean 的循环依赖是指多个 Bean 之间互相持有对方引用,形成闭环依赖关系的现象。 多个 Bean 的依赖关系构成环形链路,例如: 双向依赖:Bean A 依赖 Bean B,同时 Bean B 也依赖 Bean A(A↔B)。链条循环: Bean A → Bean…...
省略号和可变参数模板
本文主要介绍如何展开可变参数的参数包 1.C语言的va_list展开可变参数 #include <iostream> #include <cstdarg>void printNumbers(int count, ...) {// 声明va_list类型的变量va_list args;// 使用va_start将可变参数写入变量argsva_start(args, count);for (in…...
python爬虫——气象数据爬取
一、导入库与全局配置 python 运行 import json import datetime import time import requests from sqlalchemy import create_engine import csv import pandas as pd作用: 引入数据解析、网络请求、时间处理、数据库操作等所需库。requests:发送 …...
LOOI机器人的技术实现解析:从手势识别到边缘检测
LOOI机器人作为一款创新的AI硬件产品,通过将智能手机转变为具有情感交互能力的桌面机器人,展示了前沿AI技术与传统硬件设计的完美结合。作为AI与玩具领域的专家,我将全面解析LOOI的技术实现架构,特别是其手势识别、物体识别和环境…...
医疗AI模型可解释性编程研究:基于SHAP、LIME与Anchor
1 医疗树模型与可解释人工智能基础 医疗领域的人工智能应用正迅速从理论研究转向临床实践,在这一过程中,模型可解释性已成为确保AI系统被医疗专业人员接受和信任的关键因素。基于树模型的集成算法(如RandomForest、XGBoost、LightGBM)因其卓越的预测性能和相对良好的解释性…...
