数码相册——main_page主界面的显存管理、页面规划、输入控制
- 硬件平台:韦东山嵌入式Linxu开发板(S3C2440.v3)
- 软件平台:运行于VMware Workstation 12 Player下UbuntuLTS16.04_x64 系统
- 参考资料:《嵌入式Linux应用开发手册》、《嵌入式Linux应用开发手册第2版》
- 开发环境:Linux 3.4.2内核、arm-linux-gcc 4.3.2工具链
- 源码仓库:https://gitee.com/d_1254436976/Embedded-Linux-Phase-3
目录
一、前言
在【1.9 数码相册—在LCD上显示BMP图片】中,实现了在LCD中显示bmp图片,对bmp图片的二进制数据有了一定的了解,在这篇博文中,实现page_manager
页面管理者的相关函数以及main_page
主页面的测试,实现如下目的:
- 有效的管理内存,实现页面的快速刷新;
- 合理的规划页面空间,达到较好的显示效果;
- 有效的输入区域,实现触控的效果。
二、main_page页面的实现
由于需要程序改动的地方过多,所以这篇博文会比较侧重于描述思想与方法,其思想与方法同样适用于其他页面,这里只是用main_page
来介绍。
1、关键的结构体
/* 描述图片信息 */
typedef struct PixelDatas{
int bpp; //图片的bpp
int width; //图片的宽度
int height; //图片的高度
int linebytes; //图片每行占据的字节数
int TotalBytes; //图片总大小
unsigned char *PixelDatas; //存储图片具体像素数据的首地址
}T_PixelDatas, *PT_PixelDatas;
/* 页面内存数据是否被使用 */
typedef enum {
VMS_FREE = 0, //空闲
VMS_USED_FOR_PREPARE, //子线程占用
VMS_USED_FOR_CURMAIN, //当前主线程占用
}E_VideoMemState;
/* 页面内存数据是否已准备好 */
typedef enum {
PS_BLANK = 0, //数据空白
PS_GENERATING, //数据生成中
PS_GENERATED, //数据完毕
}E_PicState;
/* 描述页面内存块 */
typedef struct VideoMem {
int id; //页面内存的id
int isDevFB; //当前描述的内存是否为Framebuffer:1-是,2-否
E_VideoMemState eVideoMemState; //描述该页面内存数据的使用状态
E_PicState ePicState; //描述该页面内存数据是否已准备好
T_PixelDatas tPixelDatas; //描述图片信息
struct VideoMem *ptNext; //指向下一结点的指针
}T_VideoMem, *PT_VideoMem;
2、显存管理
2.1 思想
对于页面的显示,LCD与Framebuffer之间通过LCD控制器来进行像素信息的传递,当我们需要显示图片时,只需要在Framebuffer
中写入颜色信息就可以在LCD上显示出来。
当应用程序过大,会导致显示的很慢,此时需要优化措施:
- 在内存中开辟一个与
Framebuffer
大小相同的内存空间; - 提前把需要显示的内容写到新开辟的内存空间中;(3、页面规划与显示中介绍)
- 需要显示时,直接把新开辟的内存空间
memcpy
到Framebuffer
中。(3、页面规划与显示中介绍)
2.2 具体的实现
- 步骤描述:
/*---------------------------显存管理----------------------------------------*/
页面管理内存 = 页面描述信息 + 显存
int AllocVideoMem(int num)函数:
1. 获得LCD显示设备的分辨率以及bpp,计算得到需要开辟内存空间的大小(单位:byte)以及行宽(单位:byte)
2. 先把设备本身的Framebuffer放入链表
2.1 开辟一个大小为sizeof(T_VideoMem)内存,用来存放页面描述信息,地址放在ptNew结构体指针中
2.2 根据1中获取的信息设置该内存的描述信息,以及把Framebuffer的地址放入结构体变量中
2.3 强制设置其eVideoMemState状态位为占用状态
2.4 采用头插法插入链表
3. 后根据num分配用于页面管理的内存并设置结构体
3.1 开辟一个大小为sizeof(T_VideoMem) + VMSize内存,用来存放页面描述信息与显存,地址放在ptNew结构体指针中
3.2 根据1获得的信息,设置该内存的描述信息与显存的地址
3.3 采用头插法插入链表
- 代码实现:
int AllocVideoMem(int num)
{
int i;
int bpp;
int linebytes;
int xres, yres;
int VMSize;
PT_VideoMem ptNew;
PT_VideoMem ptTmp;
bpp = yres = xres = 0;
GetDispResolution(&xres, &yres, &bpp);
VMSize = xres * yres * bpp / 8;
linebytes = xres * bpp / 8;
/* 1、先把设备本身的Framebuffer放入链表 */
ptNew = (PT_VideoMem)malloc(sizeof(T_VideoMem));
if (ptNew == NULL) {
DebugPrint(APP_ERR"Framebuffer set fail!\n");
goto fail;
}
/* 设置该内存所描述页面的信息 */
ptNew->id = 0;
ptNew->isDevFB = 1;
ptNew->eVideoMemState = VMS_FREE;
ptNew->ePicState = PS_BLANK;
ptNew->tPixelDatas.bpp = bpp;
ptNew->tPixelDatas.height = yres;
ptNew->tPixelDatas.width = xres;
ptNew->tPixelDatas.linebytes = linebytes;
ptNew->tPixelDatas.PixelDatas = s_ptDefaultDispOpr->pDispMem;
ptNew->tPixelDatas.TotalBytes = VMSize;
/* 强制设置设备本身的Framebuffer状态为被占用 */
if (num != 0)
ptNew->eVideoMemState = VMS_USED_FOR_CURMAIN;
/* 头插法插入链表 */
ptNew->ptNext = s_ptVieoMemHead;
s_ptVieoMemHead = ptNew;
/* 2、后分配用于页面管理的内存并设置结构体,组成:页面描述 + 显存 */
for (i = 0; i < num; i++) {
ptNew = (PT_VideoMem)malloc(sizeof(T_VideoMem) + VMSize);
if (ptNew == NULL) {
DebugPrint(APP_ERR"ptNew malloc fail,already malloc num: %d!\n", i);
goto fail;
}
/* 设置该内存所描述页面的信息 */
ptNew->id = 0;
ptNew->isDevFB = 0;
ptNew->eVideoMemState = VMS_FREE;
ptNew->ePicState = PS_BLANK;
ptNew->tPixelDatas.bpp = bpp;
ptNew->tPixelDatas.height = yres;
ptNew->tPixelDatas.width = xres;
ptNew->tPixelDatas.linebytes = linebytes;
ptNew->tPixelDatas.PixelDatas = (unsigned char *)(ptNew + 1);
ptNew->tPixelDatas.TotalBytes = VMSize;
/* 头插法插入链表 */
ptNew->ptNext = s_ptVieoMemHead;
s_ptVieoMemHead = ptNew;
}
return 0;
fail:
while (s_ptVieoMemHead) {
ptTmp = s_ptVieoMemHead->ptNext;
free(s_ptVieoMemHead);
s_ptVieoMemHead = ptTmp;
}
return -1;
}
3、页面规划与显示
3.1 思想
- 页面规划:
为了适应多种尺寸的LCD设备以及良好的视觉感受,所以图标的尺寸计算如下:(不通用于其他页面,每个页面都需要制定不同的方案)
- 显示:
对于多个页面的显示与切换,需要大致做到以下步骤:
1、获得该页面的显存;
2、描画该页面内存的显示数据;
3、刷新到显示设备的Framebuffer
中。
3.2 具体的实现
- 部分使用到的函数介绍:
/*---------------------GetPixelDatasForIcon()函数-获取图标的像素数据---------------------*/
// 函数的实现步骤:
int GetPixelDatasForIcon(char *strFileName, int DevBpp, PT_PixelDatas ptPixelDatas)函数:
1. 根据strFileName变量以及宏定义的图标存放目录,组织出图标的具体位置
2. 调用MapFile()函数,获取图标文件信息并进行内存映射
3. 判断该文件是否符合bpp编码
不符合则返回,不进行下面的处理
符合则进行图标文件像素信息的获取
4. 调用GetPixelDatasFrmBMP(),获取图片的bmp像素信息
// 函数的代码实现:
int GetPixelDatasForIcon(char *strFileName, int DevBpp, PT_PixelDatas ptPixelDatas)
{
int error;
T_FileMap tFileMap;
/* 存入文件的目录名字与文件名 */
snprintf(tFileMap.strFileName, 128, "%s/%s", ICON_PATH, strFileName);
tFileMap.strFileName[127] = '\0';
/* 获取文件信息并进行映射 */
error = MapFile(&tFileMap);
if (error != 0) {
DebugPrint(APP_ERR"MapFile err! File:%s Line:%d \n", __FILE__, __LINE__);
return -1;
}
/* 判断文件是否支持 */
error = s_tBMPParser.isSupport(tFileMap.pFileMem);
if (error != 1) {
DebugPrint(APP_ERR"Can not support %s! File:%s Line:%d \n",
tFileMap.strFileName , __FILE__, __LINE__);
return -1;
}
ptPixelDatas->bpp = DevBpp;
/* 获取图片像素信息 */
error = s_tBMPParser.GetPixelDatas(tFileMap.pFileMem, ptPixelDatas);
if (error == -1) {
DebugPrint(APP_ERR"File:%s Function:%s Line:%d err!\n", __FILE__, __FUNCTION__, __LINE__);
return -1;
}
return 0;
}
/*-------------MapFile()函数-获取文件信息,并对文件进行映射----------------*/
// 函数的实现步骤:
MapFile(PT_FileMap ptFileMap)函数:
1. open()打开图标文件,获得文件描述符
2. fstat()获得文件大小信息
3. 根据以上得到的数据,进行内存的映射
4. 设置该映射文件的ptFileMap结构体的信息:文件描述符与文件大小
// 函数的代码实现:
int MapFile(PT_FileMap ptFileMap)
{
int fd;
struct stat tStat;
/* 打开bmp文件 */
fd = open(ptFileMap->strFileName, O_RDWR);
if (fd == -1) {
DebugPrint(APP_ERR"can not open %s! File:%s Line:%d \n\n",
ptFileMap->strFileName, __FILE__, __LINE__);
return -1;
}
/* 获得文件的大小 */
fstat(fd, &tStat);
/* 直接映射到内存 */
ptFileMap->pFileMem = (unsigned char *)mmap(NULL, tStat.st_size,
PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (ptFileMap->pFileMem == (unsigned char *)-1) {
DebugPrint(APP_ERR"ptFileMap->pFileMem mmap err! \
File:%s Line:%d \n", __FILE__, __LINE__);
return -1;
}
/* 设置结构体信息 */
ptFileMap->FileFd = fd;
ptFileMap->FileSize = tStat.st_size;
return 0;
}
/*-----------------GetPixelDatasFrmBMP()函数---------------------------------------------
*-----------从bmp文件中获取像素数据,并分配缓冲区存放像素信息,供外部文件调用---------------*/
// 函数的实现步骤:
int GetPixelDatasFrmBMP(unsigned char *pFileHead, PT_PixelDatas ptPixelDatas)
1. 根据bmp的编码规范,利用结构体表示bmp二进制数据中每几个字节所代表的含义
2. 根据上述结构体的信息,获得bmp文件的高度、宽度、行宽(单位:byte),存储在ptPixelDatas结构体对应的变量中
3. 根据上述高度、宽度、行宽计算得到整个bmp文件的大小,分配对应内存,首地址存储在ptPixelDatas结构体对应的变量中
4. 由于bmp编码的特殊性(开始存储的是左下角图片的像素信息),通过数学计算找到bmp源文件左上角的像素信息,
存储到源文件的像素信息到3中开辟的内存中
// 函数的代码实现:
int GetPixelDatasFrmBMP(unsigned char *pFileHead, PT_PixelDatas ptPixelDatas)
{
int y;
int width;
int height;
int BMPbpp;
int LineWidthReal;
int LineWidthAlign;
unsigned char *pSrc;
unsigned char *pDest;
BITMAPFILEHEADER *ptBITMAPFILEHEADER;
BITMAPINFOHEADER *ptBITMAPINFOHEADER;
ptBITMAPFILEHEADER = (BITMAPFILEHEADER *)pFileHead;
ptBITMAPINFOHEADER = (BITMAPINFOHEADER *)(pFileHead + sizeof(BITMAPFILEHEADER));
/* 获取bmp文件信息 */
width = ptBITMAPINFOHEADER->biWidth;
height = ptBITMAPINFOHEADER->biHeight;
BMPbpp = ptBITMAPINFOHEADER->biBitCount;
if (BMPbpp != 24) {
DebugPrint(APP_ERR"can not support %d bpp\n", BMPbpp);
return -1;
}
/* 设置结构体 */
ptPixelDatas->height = height;
ptPixelDatas->width = width;
ptPixelDatas->linebytes = width * ptPixelDatas->bpp / 8;
/* 分配缓冲区 */
ptPixelDatas->PixelDatas = (unsigned char *)malloc(width * height * ptPixelDatas->bpp / 8);
if (ptPixelDatas->PixelDatas == NULL) {
DebugPrint(APP_ERR"malloc ptPixelDatas->PixelDatas err\n");
return -1;
}
/* 获取bmp像素信息:存储像素的起始是图片左下角 */
LineWidthReal = width * BMPbpp / 8; //每行像素所占的字节
LineWidthAlign = (LineWidthReal + 3) & ~0x3; //向4取整后的每行像素所占的字节
pSrc = pFileHead + ptBITMAPFILEHEADER->bfOffBits; //像素信息的源地址
pSrc = pSrc + LineWidthAlign * (height- 1); //移动到存储图片左上角的数据地址
pDest = ptPixelDatas->PixelDatas; //数据最终的去向
for (y = 0; y < height; y++) {
//memcpy(pDest, pSrc, LineWidthReal);
CoverOneLine(width, BMPbpp, ptPixelDatas->bpp, pSrc, pDest);
pSrc -= LineWidthAlign; //bmp二进制数据
pDest += ptPixelDatas->linebytes; //LCD显示数据
}
return 0;
}
- 步骤描述:
/*---------------------------页面规划----------------------------------------*/
// 步骤1:获得显存
ptVideoMem = GetVideoMem(ID("main"), 1);
遍历寻找对应页面内存:
1.1 优先策略:取出空闲并页面ID相同的videomem页面内存,设置其eVideoMemState状态位
注意:一开始链表中所有节点页面ID都为0
1.2 择优策略:1.1条件不成立,取出任意一个空闲的videomem页面内存,设置其id、eVideoMemState状态位、ePicState页面数据状态位
// 步骤2:描画数据
需要判断ePicState页面数据状态位是否处于准备完毕状态:
若是则表示数据已经准备好,则直接进行步骤3
否则进行如下措施:
2.1 获得LCD显示设备的分辨率以及bpp
2.2 根据事先约定的LCD上的页面规划,进行图标的尺寸计算
2.3 根据上述数据,计算并设置该页面内存的描述信息:height高度、width宽度、bpp、linebytes行宽(单位:byte)、TotalBytes显示页面的内存大小(单位:byte)
2.4 根据TotalBytes的数据来malloc分配存储LCD显示的图片数据的内存,把内存的首地址存储到页面内存的PixelDatas
2.5 描绘该页面的需要显示的图标源bmp信息
2.5.1 调用GetPixelDatasForIcon(),获取每个图标的bmp像素信息
2.5.2 调用PicZoom(),根据2.2中获得的LCD显示的图标信息与2.5.1获得的源bmp文件信息,采用近邻取样插值,对LCD显示的图标的像素信息进设置
2.5.3 调用PicMerge(),根据2.5.2获得的LCD显示的图标信息,把数据整合到2.4中的页面内存的PixelDatas所指向的内存中
2.5.4 调用FreePixelDatasForIcon(),释放2.5.1中所开辟的内存
移动到下一个图标的在LCD中显示的位置
2.6 释放2.4中所开辟的内存,并更新该页面的ePicState页面数据状态位为数据完毕位
// 步骤3:刷新设备
FlushVideoMemToDev(ptVideoMem);
根据ptVideoMem->isDevFB位判断是否直接操作Framebuffer显存
是则不进行下步骤
不是则:
3.1 获得显示设备的结构体
3.2 调用该结构体的ShowPage(),进行memcpy内存块的拷贝
- 函数的实现:
static void ShowMainPage(PT_Layout ptLayout)
{
int xres;
int yres;
int bpp;
int error;
int IconX;
int IconY;
int IconWidth;
int IconHeight;
PT_VideoMem ptVideoMem;
T_PixelDatas tOriginIconPixelDatas;
T_PixelDatas tZoomIconPixelDatas;
/* 获得显存 */
ptVideoMem = GetVideoMem(ID("main"), 1);
if (ptVideoMem == NULL) {
DebugPrint(APP_ERR"Can not get video mem for main_page!\n");
return ;
}
/* 描画数据 */
if (ptVideoMem->ePicState != PS_GENERATED) {
GetDispResolution(&xres, &yres, &bpp);
/* 计算图标尺寸信息 */
IconHeight = yres * 1 / 5;
IconWidth = yres * 2 / 5;
IconX = (xres - IconWidth) / 2;
IconY = yres / 10;
/* 设置LCD显示的图片的信息 */
tZoomIconPixelDatas.height = IconHeight;
tZoomIconPixelDatas.width = IconWidth;
tZoomIconPixelDatas.bpp = bpp;
tZoomIconPixelDatas.linebytes = IconWidth * bpp / 8;
tZoomIconPixelDatas.TotalBytes = tZoomIconPixelDatas.linebytes * IconHeight;
/* 分配存储LCD显示的图片数据的内存 */
tZoomIconPixelDatas.PixelDatas = (unsigned char *)malloc(tZoomIconPixelDatas.TotalBytes);
if (tZoomIconPixelDatas.PixelDatas == NULL) {
free(tZoomIconPixelDatas.PixelDatas); //释放内存
DebugPrint(APP_ERR"Malloc err! File:%s Line:d\n", __FILE__, __LINE__);
return ;
}
/* 描绘每个图片的坐标信息 */
while (ptLayout->strIconName) {
/* 计算图标坐标信息 */
ptLayout->TopLeftX = IconX;
ptLayout->TopLeftY = IconY;
ptLayout->BotRightX = IconX + IconWidth - 1;
ptLayout->BotRightY = IconY + IconHeight - 1;
/* 获取图片像素信息 */
error = GetPixelDatasForIcon(ptLayout->strIconName, bpp, &tOriginIconPixelDatas);
if (error == -1) {
DebugPrint(APP_ERR"GetPixelDatasForIcon err! File:%s Line:%d\n", __FILE__, __LINE__);
return ;
}
/* 对图片的缩放参数进行设置 */
PicZoom(&tOriginIconPixelDatas, &tZoomIconPixelDatas);
/* 把缩放后的图片信息整合到ptVideoMem.tPixelDatas中 */
PicMerge(IconX, IconY, &tZoomIconPixelDatas, &ptVideoMem->tPixelDatas);
FreePixelDatasForIcon(&tOriginIconPixelDatas);
IconY += yres * 3 / 10;
ptLayout++;
}
free(tZoomIconPixelDatas.PixelDatas); //释放内存
ptVideoMem->ePicState = PS_GENERATED; //更新状态
}
/* 刷新/加载到设备 */
FlushVideoMemToDev(ptVideoMem);
/* 设置页面内存为空闲状态 */
PutVideoMem(ptVideoMem);
}
4、触摸输入的设置
4.1 思想
对于页面中需要显示的图标,每个图标都有对应的有效触摸区域。当用户触摸到对应的区域时,进入不同的页面(未实现,下篇博客实现)且该图标的颜色变化。
由于这里使用到输入系统,即【1.7 数码相册—电子书(5)—多线程支持多输入】中介绍的,会使用到触摸屏子线程与tslib库。
4.2 具体实现
- 部分使用的函数介绍:
/*---------------MainPageGetInputEvent()-main页面输入事件函数------------------------*/
// 函数的实现步骤:
static int MainPageGetInputEvent(PT_Layout ptLayout, PT_InputEvent ptInputEvent)函数:
1. 调用之前写的GetInputEvent(),进入输入事件子线程,获得原始的输入事件数据
2. 限制条件:只允许输入事件的类型为INPUT_TYPE_TOUCHSCREEN触摸屏输入事件
3. 根据调用tslib库所获得触点的坐标,比较与"步骤2:描画数据"的2.2获得的每个图标的LCD页面规划坐标
定位触点所在的图标,返回该图标的索引
4. 寻找不到则返回-1
// 函数的代码实现:
static int MainPageGetInputEvent(PT_Layout ptLayout, PT_InputEvent ptInputEvent)
{
int i;
int ret;
T_InputEvent tInputEvent;
/* 调用input_manager.c的函数,获得原始的输入数据 */
ret = GetInputEvent(&tInputEvent);
if (ret != 0) {
DebugPrint(APP_ERR"GetInputEvent err! File:%s Line:%d\n", __FILE__, __LINE__);
return -1;
}
*ptInputEvent = tInputEvent;
/* 限制条件 */
if (tInputEvent.type != INPUT_TYPE_TOUCHSCREEN)
return -1;
/* 处理数据 */
/* 确定触点位于哪个图标上 */
i = 0;
while (ptLayout[i].strIconName) {
if ((tInputEvent.x >= ptLayout[i].TopLeftX) && (tInputEvent.x <= ptLayout[i].BotRightX) \
&& (tInputEvent.y >= ptLayout[i].TopLeftY) && (tInputEvent.y <= ptLayout[i].BotRightY))
return i;
else
i++;
}
/* 找不到对应图标 */
return -1;
}
- 步骤描述:
/*---------------------------------输入触摸----------------------------------*/
// 步骤1:获取输入事件与图标索引
index = MainPageGetInputEvent(s_tMainPageLayout, &tInputEvent);
1. 调用MainPageGetInputEvent(),获得该页面的输入事件与对应图标的索引,
由于调用到之前的输入事件子线程,所以在这里程序会处于休眠,有输入事件才唤醒
index = -1;
indexPressured = -1;
pressured = 0;
//步骤2:根据反馈的情况进行不同的处理
为了支持按下和松开的位置不一的情况,进行如下处理:
pressured: 1-曾经按下 0-一直为按下
indexPressured: 按下图标的索引
2.1 若输入事件得到的压力值为0(松开) 且 pressured==1曾经按下,则调用ReleaseButton(),根据indexPressured,把图标颜色取反
2.2 若输入事件得到的压力值为1(按下) 且 已经获得了按键的index索引 且 pressured==0未按下,则
pressured= 1;
indexPressured = index;
PressButton(&s_tMainPageLayout[indexPressured]);把图标颜色取反
- 代码实现:
static int MainPageRun(void)
{
int index;
int pressured;
int indexPressured;
T_InputEvent tInputEvent;
/* 显示页面 */
ShowMainPage(s_tMainPageLayout);
index = -1;
indexPressured = -1;
pressured = 0;
/* 调用GetInputEvent(), 获得输入事件,进而处理 */
while (1) {
index = MainPageGetInputEvent(s_tMainPageLayout, &tInputEvent);
/* 松开和按下不在同一个图标范围内 */
if (tInputEvent.pressure == 0) {
/* 曾经有按键按下 */
if (pressured) {
ReleaseButton(&s_tMainPageLayout[indexPressured]);
pressured = 0;
indexPressured = -1;
}
} else {
/* 松开和按下都在同一个图标范围内 */
/* 按下 */
if (index != -1) {
/* 之前未按下 */
if (!pressured) {
pressured = 1;
indexPressured = index;
PressButton(&s_tMainPageLayout[indexPressured]);
}
}
}
}
return 0;
}
三、编译与运行
1、编译
执行make
生成可执行文件digitpic
文件
2、运行
- 可执行文件
digitpic
放入到根文件系统 - icon目录下的图标文件放入到根文件系统的
/etc/digitpic/icons
(程序打开图标文件时需要用到) - 执行可执行文件,得到以下的效果:
程序一开始进入main_page
主界面,按下与松开3个图标的位置,图标的颜色会反转。