【Qt移植LVGL】QWidget手搓LVGL软件仿真模拟器(非直接运行图形库)
打包开源地址:
Qt函数库gitee地址
更新以gitee为准
移植后的demo工程:
gitee
有些没实现的 后续我会继续优化
文章目录
- 别碰瓷看清楚:是移植,不是直接运行LVGL
- Qt的C/C++混编
- Qt的模拟显示触摸屏
- LVGL的API函数编写
- LVGL文件移植
- LVGL工程配置
- LVGL显示配置
- LVGL触摸配置
- 主线程初始化和定时器轮询
- 测试
- 附录:C语言到C++的入门知识点(主要适用于C语言精通到Qt的C++开发入门)
- C语言与C++的不同
- C++中写C语言代码
- C语言到C++的知识点
- Qt开发中需要了解的C++基础知识
- namespace
- 输入输出
- 字符串类型
- class类
- 构造函数和析构函数(解析函数)
- 类的继承
别碰瓷看清楚:是移植,不是直接运行LVGL
LVGL是一种轻量级嵌入式图像界面库 且不需要多线程就能跑通
这里的在Qt上面移植LVGL并不是用Qt的IDE去建立一个没有Qt应用的工程 然后编译运行带SDL2等图形库的程序 从而实现在PC上运行LVGL
而是将Qt的窗口作为一个完整的嵌入式设备进行移植 使LVGL调用Qt窗口来进行显示和输入
说到FreeRTOS和LVGL这两种模拟多线程代表 我的个人理解就是:
FreeRTOS是硬件上的系统移植 其根据不同架构调用了硬件底层中断 systick等外设 以实现任务调度 属于用外设硬件模拟的多线程
LVGL的多线程是用两个定时器中断 不断刷新页面 进行操作 其触摸延迟至少得5ms 属于软件层面的多线程运行 实际上就相当于一个while 1里面不断刷新页面罢了
很早之前用51单片机+LCD1602写界面也是用一个while+状态判断来写的
Qt的C/C++混编
C++是兼容C的
在Qt的工程里面 可以直接添加C文件
但是调用C文件时 却不能直接拿来用
除了导入C文件对应的头文件函数声明外
还需要加上extern "C"
来表示下面代码按C语言文件的方式编译
譬如
#ifdef __cplusplus
extern "C" {
#endif...//C文件的函数声明#ifdef __cplusplus
}
#endif
这样才能正常调用不报错
另外 在C文件中 当然就用不了C++的函数
不过又想实现交互 有两种解决方式:
分别导入<csignal>
和<signal.h>
库
自己给自己的进程发送信号
或者 直接将要调用C++函数的C文件改成cpp文件
显然 后者更方便
但在混编时 只需要写一个总的接口函数文件用于调用即可 该接口文件可以调用C++的函数
另外也可以调用子类的C语言函数
譬如A.cpp操作B.c 直接调用即可
但是B.c如果想给A.cpp的槽函数信号 再去判断做什么事(调用emit函数) 则可以定义一个C.cpp 在C.cpp中发送emit来做判断 然后再去操作B.c
Qt的模拟显示触摸屏
通过QWidget可以模拟出一个显示屏
【Qt开发】QWidget的虚拟触摸显示屏配置 QPainter、QPixmap以及resizeEvent、paintEvent、mouseEvent鼠标输入事件
同时采样信号队列的形式
去捕获按键输入等等
connect(this, SIGNAL(goto_setStyle(int)),this, SLOT(setStyle(int)),Qt::QueuedConnection);connect(this, SIGNAL(goto_setWidth(int)),this, SLOT(setWidth(int)),Qt::QueuedConnection);connect(this, SIGNAL(goto_setColor(QColor)),this, SLOT(setColor(QColor)),Qt::QueuedConnection);connect(this, SIGNAL(goto_setBack(QColor)),this, SLOT(setBack(QColor)),Qt::QueuedConnection);connect(this, SIGNAL(goto_clear()),this, SLOT(clear()),Qt::QueuedConnection);connect(this, SIGNAL(goto_drawPoint(QPoint)),this, SLOT(drawPoint(QPoint)),Qt::QueuedConnection);connect(this, SIGNAL(goto_drawPoints(QPoint*,int)),this, SLOT(drawPoints(QPoint*,int)),Qt::QueuedConnection);connect(this, SIGNAL(goto_drawLine(QPoint,QPoint)),this, SLOT(drawLine(QPoint,QPoint)),Qt::QueuedConnection);connect(this, SIGNAL(goto_setTextStyle(int)),this, SLOT(setTextStyle(int)),Qt::QueuedConnection);connect(this, SIGNAL(goto_setTextFont(QString,int)),this, SLOT(setTextFont(QString,int)),Qt::QueuedConnection);connect(this, SIGNAL(goto_drawText(QRectF,QString)),this, SLOT(drawText(QRectF,QString)),Qt::QueuedConnection);connect(this, SIGNAL(goto_fillRect(QPoint,QPoint)),this, SLOT(fillRect(QPoint,QPoint)),Qt::QueuedConnection);
signals:void goto_setStyle(int s);void goto_setWidth(int w);void goto_setColor(QColor c);void goto_clear(void);void goto_setBack(QColor c);void goto_setTextStyle(int text_style);void goto_setTextFont(QString strfont,int size);void goto_drawText(QRectF rectangle,QString text);void goto_drawPoint(QPoint p);void goto_drawPoints(QPoint *p,int pointCount);void goto_drawLine(QPoint p1,QPoint p2);void mouse_signal(int mode,QPoint p);void goto_fillRect(QPoint p1,QPoint p2);
模拟的触摸屏只需要触摸功能即可
LVGL的API函数编写
在触摸显示屏上移植LVGL 涉及到的API只有画点、面,触摸按下、松开,触摸坐标函数
以上面的模拟显示屏为接口 编写LVGL的API函数如下:
#include "LVGL_API.h"LVGL_API_Class *LVGL_API;void drawPoint(int x,int y,unsigned short color)
{emit LVGL_API->display->goto_setStyle(1);QPoint p(x,y);int r=0;int g=0;int b=0;r=(color>>11)*8;g=((color>>5)&0x003F)*4;b=(color&0x001F)*8;QColor c(r,g,b);emit LVGL_API->display->goto_setColor(c);emit LVGL_API->display->goto_drawPoint(p);emit LVGL_API->display->goto_setStyle(0);
}void drawLine(int x1,int y1,int x2,int y2,unsigned short color)
{emit LVGL_API->display->goto_setStyle(1);QPoint p1(x1,y1);QPoint p2(x2,y2);int r=0;int g=0;int b=0;r=(color>>11)*8;g=((color>>5)&0x003F)*4;b=(color&0x001F)*8;QColor c(r,g,b);emit LVGL_API->display->goto_setColor(c);emit LVGL_API->display->goto_drawLine(p1,p2);emit LVGL_API->display->goto_setStyle(0);
}void fillRect_drawLine(int x1,int y1,int x2,int y2)
{QPoint p1(x1,y1);QPoint p2(x2,y2);emit LVGL_API->display->goto_drawLine(p1,p2);
}void fillRect(int x1,int y1,int x2,int y2,unsigned short color)
{
#if 0emit LVGL_API->display->goto_setStyle(1);QPoint p1(x1,y1);QPoint p2(x2,y2);QPoint p3(x1,y2);QPoint p4(x2,y1);int r=0;int g=0;int b=0;r=(color>>11)*8;g=((color>>5)&0x003F)*4;b=(color&0x001F)*8;QColor c(r,g,b);emit LVGL_API->display->goto_setColor(c);emit LVGL_API->display->goto_drawLine(p1,p3);emit LVGL_API->display->goto_drawLine(p1,p4);emit LVGL_API->display->goto_drawLine(p2,p3);emit LVGL_API->display->goto_drawLine(p2,p4);emit LVGL_API->display->goto_fillRect(p1,p2);emit LVGL_API->display->goto_setStyle(0);
#elseint i=0;emit LVGL_API->display->goto_setStyle(1);int r=0;int g=0;int b=0;r=(color>>11)*8;g=((color>>5)&0x003F)*4;b=(color&0x001F)*8;QColor c(r,g,b);emit LVGL_API->display->goto_setColor(c);for(i=0;i<=y2-y1;i++){fillRect_drawLine(x1,y1+i,x2,y1+i);}emit LVGL_API->display->goto_setStyle(0);
#endif
}void Init_LVGL_API(MY_Display *dis)
{LVGL_API = new LVGL_API_Class(dis);
}
#ifndef LVGL_API_H
#define LVGL_API_H
#include "MY_QT_DEF.h"void drawPoint(int x,int y,unsigned short color);
void fillRect(int x1,int y1,int x2,int y2,unsigned short color);
void drawLine(int x1,int y1,int x2,int y2,unsigned short color);class LVGL_API_Class;
class LVGL_API_Class: public QObject
{Q_OBJECTpublic:int Touch_x;int Touch_y;int Touch;MY_Display *display;LVGL_API_Class(MY_Display *dis=nullptr){Touch=0;if(dis!=nullptr){display=dis;connect(display, SIGNAL(mouse_signal(int,QPoint)),this, SLOT(TouchEvent(int,QPoint)),Qt::QueuedConnection);emit display->goto_setStyle(0);}}~LVGL_API_Class(void){}
public slots:void TouchEvent(int mode,QPoint p){if(mode==2){Touch=0;}else{Touch=1;}Touch_x=p.x();Touch_y=p.y();}
};extern LVGL_API_Class *LVGL_API;void Init_LVGL_API(MY_Display *dis);#endif // LVGL_API_H
其中 画面提供了两种方式 一个是for循环画线遍历 一个是直接填充然后再画边框
但这两个在Qt上实现对LVGL的兼容性不太好(Qt会将比较小的像素点给优化掉)
所以在LVGL显示方面 还是选择画点函数吧
譬如画点是正常的:
画面就糊掉了
LVGL文件移植
移植与在嵌入式设备上移植相似
直接按照STM32的移植方式进行即可
这里用的LVGL库版本是8.3
下载后 实际需要移植的就下面三个文件夹和两个文件
将这些文件复制到一个单独的文件夹 比如LVGL
并且删除掉_template后缀
在examples中只保留porting 文件夹
同样删除_template后缀
然后这个LVGL文件夹里面的内容就是我们要移植的文件 适用于所有工程
包括STM32、Qt、C工程等等
LVGL工程配置
将上面的文件拷贝到Qt工程中
不要将LVGL文件夹整个拷贝 而是将里面的内容拷贝过来
用命令表示的区别就是 一个是cp ./
一个是 cp ./*
这里是后者
这是因为Qt的搜素头文件路径默认就是根目录
而LVGL里面的文件导入则是以相对路径来的 譬如src下的文件:
这样一看就明白了吧
当然 你直接拷个文件夹过来也不是不行 那你就得弄好相对路径
在源文件和头文件中添加
直接点击Add Existing Directory 即可筛选
要添加的文件如下:
首先是两个主要头文件:
lv_conf.h
lvgl.h
porting目录 下的四个文件
lv_port_disp.c 、lv_port_disp.h、 lv_port_indev.c、lv_port_indev.h
src 下的所有C文件
除此之外 其他的一律不要添加
可以按如下进行添加:
另外 如果你用的不是Qt Creator 那么就需要添加头文件搜素路径
LVGL显示配置
打开 lv_conf.h 修改文件
表示启用
打开 lv_port_disp.h
同样启用 并且头文件路径改一下
打开 lv_port_disp.c
启用 并且删除后缀
在 lv_port_disp.c中添加我们的LVGL_API.h
注意 LVGL_API.cpp是一个C++文件 所以为了能使用 需要将 lv_port_disp.c改成 lv_port_disp.cpp
同理 触摸的文件也要改
然后定义屏幕可用的大小
LVGL提供了三种缓存方式 选择一种 其他的注释
第一种最简单 而且不需要添加什么东西
先配置 配好了以后有时间自己再研究就好了
然后关联画点函数即可
这里有一种更为优化 刷新率更高的方式 就是直接移植填充一块区域的函数
但是就如上面所说的 Qt这块给优化掉了 导致细小边界看不清 所以就只能用画点函数
LVGL触摸配置
LVGL可以设置触摸、按键、鼠标事件
这里我们只用触摸
虽然Qt的鼠标事件也可以捕获 但是我们把所有的鼠标事件都定义为触摸就行了
配置如显示类似
一样导入文件定义区域
在93行以后 只保留触摸 注释掉鼠标和按键 一直到170行(除非你要使用)
添加检测触摸函数接口和获取坐标接口
即可
如果想看看触摸能不能生效 那么就添加一个画点的函数在触摸获取xy坐标的后面即可
帧率和触摸率还有待优化:
有些没实现的 后续我都会继续优化
主线程初始化和定时器轮询
在main中导入库:
#include "lvgl.h" // 它为整个LVGL提供了更完整的头文件引用
#include "examples/porting/lv_port_disp.h" // LVGL的显示支持
#include "examples/porting/lv_port_indev.h" // LVGL的触屏支持
在窗口的构造函数中初始化模拟触摸屏后 进行LVGL初始化
lv_init(); // LVGL 初始化lv_port_disp_init(); // 注册LVGL的显示任务lv_port_indev_init(); // 注册LVGL的触屏检测任务
建立两个定时器线程 一个1ms 一个5ms 分别调用lv_tick_inc(1);
和lv_timer_handler();
void timer0_callback(void * pCBParam,uint32_t Event,void * pArg)
{lv_tick_inc(1);
}void timer1_callback(void * pCBParam,uint32_t Event,void * pArg)
{lv_timer_handler();
}Timer0 = new MY_Timer(timer0_callback,1,true);Timer1 = new MY_Timer(timer1_callback,5,true);Timer0->Start_Timer();Timer1->Start_Timer();
这就是LVGL的心跳
测试
建立几个控件测试一下:
void button_evnet(lv_event_t * event)
{qDebug()<<event->code;lv_obj_t *btn = lv_event_get_target(event); // 获得调用这个回调函数的对象if (event->code == LV_EVENT_CLICKED){static uint8_t cnt = 0;cnt++;lv_obj_t *label = lv_obj_get_child(btn, NULL); // 获取第1个子对象(我们在设计时,已安排了它的第1个子对象是一个label对象)lv_label_set_text_fmt(label, "Button: %d", cnt); // 设置标签的文本,写法类似printf}
}//按钮lv_obj_t *myBtn = lv_btn_create(lv_scr_act()); // 创建按钮; 父对象:当前活动屏幕lv_obj_set_pos(myBtn, 10, 10); // 设置坐标lv_obj_set_size(myBtn, 120, 50); // 设置大小lv_obj_add_event_cb(myBtn, button_evnet, LV_EVENT_CLICKED, NULL); //添加事件// 按钮上的文本lv_obj_t *label_btn = lv_label_create(myBtn); // 创建文本标签,父对象:上面的btn按钮lv_obj_align(label_btn, LV_ALIGN_CENTER, 0, 0); // 对齐于:父对象lv_label_set_text(label_btn, "Test"); // 设置标签的文本// 独立的标签lv_obj_t *myLabel = lv_label_create(lv_scr_act()); // 创建文本标签; 父对象:当前活动屏幕lv_label_set_text(myLabel, "Hello world!"); // 设置标签的文本lv_obj_align(myLabel, LV_ALIGN_CENTER, 0, 0); // 对齐于:父对象lv_obj_align_to(myBtn, myLabel, LV_ALIGN_OUT_TOP_MID, 0, -20); // 对齐于:某对象
运行后效果:
帧率信息显示在:
lv_conf.h中第282行,找到:LV_USE_PERF_MONITOR,原值:0, 修改为:1
内存显示则在:
lv_conf.h中第289行,找到:LV_USE_MEM_MONITOR,原值:0, 修改为:1
附录:C语言到C++的入门知识点(主要适用于C语言精通到Qt的C++开发入门)
C语言与C++的不同
C语言是一门主要是面向工程的语言
C++则是面向对象
C语言中 某些功能实现起来较为繁琐
比如结构体定义:
一般写作:
typedef struct stu_A
{
}A;
也可以写作:
typedef struct
{
}A;
但 大括号后面的名称是不可省去的
不过 C++的写法就比较简单
除了支持上述写法外
也支持直接声明
typedef struct A
{
}
另外 C++是完全支持C语言库和语法的
不过C++里面的库也有些很方便的高级功能用法 只不过实现起来可能不如C的速度快
再者 C语言与C++的编译流程不一样
C语言没有函数重载 所以给编译器传参就是直接传函数名称
但是C++除了传函数名称外 还会穿函数的参数、类型等等 以实现函数重载
C++中写C语言代码
上文提到 C++可以完全兼容C的写法
但是编译流程也还是不一样
所以如果在编译层面进行C语言代码编译 则通常用以下方法:
extern "C"
{
...
}
表面大括号内的内容用C的方法进行编译
另外 如果还是用C++的编译器 但要实现C语言函数 则需要用到C语言的库
在C语言中 我们一般用如下方法导入库
#include <stdio.h>
此方法同样适用于C++ 但是C++可以更方便的写成去掉.h的方式
比如:
#include <iostream>
在C++中 为了调用C语言的库 可以采用在原库名称前加一个"c"的方式导入
如:
#include <cstdio>
这样就可以使用printf等函数了 甚至比C++的std方法更快
C语言到C++的知识点
Qt开发中需要了解的C++基础知识
namespace
C++面向对象的特性下诞生的一个名称
表示某个函数、变量在某个集合下 用作namespace
比如 <iostream>
库中的关键字cin在std下 则写作std::cin
std就是namespace
::表示某空间下的某某
前面是空间名称 后面是变量、函数名称
用using namespace
可以告诉编译器以下都用xx名称空间
比如:
using namespace std;
cout<<"a";
如果没有告诉编译器所使用的空间名称 则要写成:
std::cout<<"a";
同样 可以自定义某一段代码属于哪个空间:
namespace xx
{
...
}
输入输出
在C++中 用iostream作为输入输出流的库
#include <iostream>
用cin和cout关键字进行输入和输出
如:
using namespace std;
int a=0;
cin>>a; //输入到acout<<a; //输出a
类比scanf和printf
同样 还有一个关键字endl表示换行
cout和cin的传参是不固定的
由编译器自行裁定
字符串类型
在C语言中 常用char *表示字符串
但是在C++中 可以直接用string类型
比如:
char * s="456";
string str="123";
由于cout的特性 这两种字符串都可以直接打印
但如果使用C语言中printf的打印方式时 采用%s方式打印字符串 则不能传入string类型
class类
C++的核心就是class
同Python等支持面向对象的语言一样
可以理解成一个支持函数、继承、自动初始化、销毁的结构体
在class类中 有private
私有、public
公有变量
前者只能内部访问 后者可以外部调用使用
如:
class A
{
public:
int a;
private:
int b;
}
a可以用A.a的方式方位 b则外部无法访问
构造函数和析构函数(解析函数)
构造函数可以理解成对类的初始化 反之析构函数则是退出时进行销毁前的函数
两者需要与类的名称相同 析构函数则在前面加一个~表示非
如:
class A
{
public:
int a;
A();
~A();
private:
int b;
}A::A()
{
...
}A::~A()
{
...
}
构造函数可以定义传参 析构函数则不行
类的继承
如果有两个类A和B 想让A里面包含B 则可以写作继承的写法
继承后 A类的变量可以直接调用B下面的成员
如:
class B
{
int b;
}
class A: public B
{
int a;
}
在定义A后 可以访问到B的成员b 当然 继承也可以私有