数码相框实现二、显示设置页面、时间间隔设置页面
一、写在前面
突然发现,如何先把思路讲清,然后再贴出一堆代码。这种分离式的讲解不仅难以将代码与原理联系在一起,
更重要的是后面贴出的一段代码实在枯涩难懂(一没注释,二代码太长)。因此,个人看法,应该一文按实现顺序拆分为几部分,
每一部分将思路与代码实现结合起来,且第一部分应提出本文目标,结尾部分再捋一下全篇思路,做出总结。
此外,如有参考资料或辅助资料放在最后面,帮助理解和知识拓展。
行,就这么干!
二、本文内容概述
本文在前篇显示主界面基础上,完成显示设置页面、时间间隔页面及实现加减时间间隔。
三、显示设置页面
思路:
1.显示主页面后,进入while循环等待触摸屏输入,没输入则休眠,否则子线程捕捉到输入事件后会唤醒主线程,
主线程再调用捕捉事件函数并返回事件(注: 事件用结构体封装,记录了触摸屏点击位置(X,Y)、事件类型(触摸屏或按键)、
点击时间、按键值(按键还没实现))。然后根据事件做出相应处理。
2. 因此,要想显示设置页面,首先捕捉到触摸屏点击输入事件后,判断点击位置落在“设置”图标按钮上,然后,
类似主页面显示一样,调用设置页面的Run函数,显示“设置页面”后进入while()循环,不断地捕捉触摸屏输入事件,
判断事件并作出相应处理。
瞧,设置页面的显示和功能跟主页面完全一致。
因此,编程就简单多了!直接复制主页面代码,做些修改就可以啦。
在前面分析基础上,实现分两步。
1)修改主页面while循环里的代码,判断点击在“设置”按钮后调用设置页面的Run()函数。代码如下
/* 这段代码在主页面的Run()函数里面 */
/* 获得输入事件(暂且只有触摸屏作为输入) */
int iIndex;
int bPressed = 0;
int iIndexPressed = -1;
while(1)
{
iIndex = GetMainPageInputEvent(g_atMainPageLayout, &tInputEvent);
if(tInputEvent.iPressure == 0)
{
if(bPressed) // 曾经按下
{
/* 曾经有按钮被按下, 则释放按钮 */
ReleaseButton(&g_atMainPageLayout[iIndexPressed]);
bPressed = 0;
if (iIndexPressed == iIndex) /* 按下和松开都是同一个按钮 */
{
switch (iIndexPressed)
{
case 2: /* 设置按钮 */
{
/* 调用设置页面的Run函数 */
GetPageOpr("setting")->Run();
/* 从设置页面返回后显示当首的主页面 */
ShowMainPage(g_atMainPageLayout);
break;
}
default:
{
break;
}
}
}
iIndexPressed = -1;
}
}
else
{
if(iIndex != -1)
{
if(!bPressed)
{
/* 未曾按下按钮 */
bPressed = 1;
iIndexPressed = iIndex;
PressButton(&g_atMainPageLayout[iIndexPressed]);
}
}
}
}
2)实现页面设置文件 setting_page.c
这部分代码就不贴出来了,因为实现逻辑和主页面的完全一样。这里回顾下实现思路,并注意两点。
实现思路:看下图
注意两点
1)设置页面的 return.bmp 大小不同于前两个,需特殊处理(即缩放为 54x54, 前面两个bmp缩放为 108 x 54)
if(atLayout->pcIconName && strcmp(atLayout->pcIconName, "return.bmp") == 0)
{
iIconWidth = iIconWidth / 2;
iIconX = (iXres - iIconWidth) / 2;
tScaledIconPixelDatas.iBpp = iBpp;
tScaledIconPixelDatas.iWidth = iIconWidth;
tScaledIconPixelDatas.iHeight = iIconHeight;
tScaledIconPixelDatas.iLineBytes = iIconWidth * iBpp / 8;
tScaledIconPixelDatas.iTotalBytes = tScaledIconPixelDatas.iLineBytes * iIconHeight;
}
2) 调用分配缓冲块函数中 注意,ptNew->tPixelDatas.aucPixelDatas = (unsigned char*)(ptNew + 1);
即(ptNew+1)先加括号,避免先强转为unsigned char*,然后 + 1 byte。这会导致调用ClearVideoMem
将iTotalBytes设置为0. 即点击设置后并不会显示出“设置页面”。(这段话没听懂没关系,不影响。如果遇到
点击设置页面后并没有显示,可考虑是否这里出了问题,我就掉坑在这里 :)
四、显示时间间隔设置页面
思路:
时间间隔设置页面的显示和功能与主页面、设置页面的逻辑完全相同,这里不再赘述。
区别在于,时间间隔的布局要比前面两个要复杂。需提前计算各图标的位置信息。
计算图标在LCD显示坐标并不难,也不是重点,因此这里忽略不讲。
重点在下面的第5点,如何实现时间间隔的加减。
五、实现加减时间间隔
要改变时间间隔,思路如下
如:要显示 07 ---> 转为字符串--->逐个字符处理,写入LCD的显存
前面两步很简单,用snprintf函数就可解决。难点在最后一步。处理过程如下:
确定单个字符的编码值,根据编码值找到字体(用freetype库),再把这个字体写到LCD显存对应位置。
(备注:所谓字体写到LCD显存,即把位图数据复制到LCD显存而已)
问题来了,如何确定要显示的字符在LCD的坐标值?
1)事先计算字符串的最大宽度和高度,然后计算第一个字符的原点坐标
2)确定显示位置后,根据字符编码确定字体位图,然后写入LCD的显存指定位置,如此不断循环处理
(注意:这里先写入缓冲区,再刷到LCD的显存FrameBuffer)
下面看一下代码实现
/*--------------------下面代码在interval_page.c文件的Run函数中----------------*/
while (1)
{
iIndex = IntervalPageGetInputEvent(&g_tIntervalPageLayout, &tInputEvent);
if (tInputEvent.iPressure == 0)
{
/* 如果是松开 */
if (bPressed)
{
/* 曾经有按钮被按下 */
ReleaseButton(&g_atIntervalPageIconsLayout[iIndexPressed]);
bPressed = 0;
if (iIndexPressed == iIndex) /* 按下和松开都是同一个按钮 */
{
switch (iIndexPressed)
{
case 0: /* inc按钮 */
{
iNumber++;
if (iNumber == 60)
{
iNumber = 0;
}
GenerateIntervalPageSpecialIcon(iNumber, ptDevVideoMem);
break;
}
case 2: /* dec按钮 */
{
iNumber--;
if (iNumber == -1)
{
iNumber = 59;
}
GenerateIntervalPageSpecialIcon(iNumber, ptDevVideoMem);
break;
}
case 3: /* ok按钮 */
{
return;
break;
}
case 4: /* cancel按钮 */
{
return;
break;
}
default:
{
break;
}
}
}
iIndexPressed = -1;
}
}
else
{
/* 按下状态 */
if (iIndex != -1)
{
if (!bPressed && (iIndex != 1))
{
/* 未曾按下按钮 */
bPressed = 1;
iIndexPressed = iIndex;
PressButton(&g_atIntervalPageIconsLayout[iIndexPressed]);
}
}
}
}
/* 绘制图标中的数字 */
static int GenerateIntervalPageSpecialIcon(int dwNumber, PT_VideoMem ptVideoMem)
{
unsigned int dwFontSize;
char strNumber[3];
int iError;
dwFontSize = g_tIntervalNumberLayout.iBotRightY - g_tIntervalNumberLayout.iTopLeftY;
SetFontSize(dwFontSize);
/* 显示两位数字: 00~59 */
if (dwNumber > 59)
{
return -1;
}
snprintf(strNumber, 3, "%02d", dwNumber);
//DBG_PRINTF("strNumber = %s, len = %d\n", strNumber, strlen(strNumber));
iError = MergerStringToCenterOfRectangleInVideoMem(g_tIntervalNumberLayout.iTopLeftX, g_tIntervalNumberLayout.iTopLeftY, g_tIntervalNumberLayout.iBotRightX, g_tIntervalNumberLayout.iBotRightY, (unsigned char *)strNumber, ptVideoMem);
return iError;
}
/* -----------------------以下代码在render.c文件------------------------------------*/
/*
* 在videomem的指定矩形中间显示字符串
*/
int MergerStringToCenterOfRectangleInVideoMem(int iTopLeftX, int iTopLeftY, int iBotRightX, int iBotRightY, unsigned char *pucTextString, PT_VideoMem ptVideoMem)
{
int iLen;
int iError;
unsigned char *pucBufStart;
unsigned char *pucBufEnd;
unsigned int dwCode;
T_FontBitMap tFontBitMap;
int bHasGetCode = 0;
int iMinX = 32000, iMaxX = -1;
int iMinY = 32000, iMaxY = -1;
int iFirstFontTopLeftX = 32000, iFirstFontTopLeftY = 32000;
int iNewFirstFontTopLeftX , iNewFirstFontTopLeftY;
int iWidth, iHeight;
tFontBitMap.iCurOriginX = 0;
tFontBitMap.iCurOriginY = 0;
pucBufStart = pucTextString;
pucBufEnd = pucTextString + strlen((char *)pucTextString);
/* 0. 清除这个区域 */
ClearRectangleInVideoMem(iTopLeftX, iTopLeftY, iBotRightX, iBotRightY, ptVideoMem, COLOR_BACKGROUND);
/* 1.先计算字符串显示的总体宽度、高度 */
while (1)
{
iLen = GetCodeFrmBuf(pucBufStart, pucBufEnd, &dwCode);
if (0 == iLen)
{
/* 字符串结束 */
if (!bHasGetCode)
{
//DBG_PRINTF("%s %s %d\n", __FILE__, __FUNCTION__, __LINE__);
return -1;
}
else
{
break;
}
}
bHasGetCode = 1;
pucBufStart += iLen;
iError = GetFontBitmap(dwCode, &tFontBitMap);
if (0 == iError)
{
if (32000 == iFirstFontTopLeftX)
{
iFirstFontTopLeftX = tFontBitMap.iXLeft;
iFirstFontTopLeftY = tFontBitMap.iYTop;
}
if (iMinX > tFontBitMap.iXLeft)
{
iMinX = tFontBitMap.iXLeft;
}
if (iMaxX < tFontBitMap.iXMax)
{
iMaxX = tFontBitMap.iXMax;
}
if (iMinY > tFontBitMap.iYTop)
{
iMinY = tFontBitMap.iYTop;
}
if (iMaxY < tFontBitMap.iXMax)
{
iMaxY = tFontBitMap.iYMax;
}
tFontBitMap.iCurOriginX = tFontBitMap.iNextOriginX;
tFontBitMap.iCurOriginY = tFontBitMap.iNextOriginY;
}
else
{
DBG_PRINTF("GetFontBitmap for calc width/height error!\n");
}
}
iWidth = iMaxX - iMinX;
iHeight = iMaxY - iMinY;
if (iWidth > iBotRightX - iTopLeftX)
{
return -1;
}
if (iHeight > iBotRightY - iTopLeftY)
{
return -1;
}
/* 2.确定第1个字符的原点
* 2.1 先计算左上角坐标
*/
iNewFirstFontTopLeftX = iTopLeftX + (iBotRightX - iTopLeftX - iWidth) / 2;
iNewFirstFontTopLeftY = iTopLeftY + (iBotRightY - iTopLeftY - iHeight) / 2;
/*
* 2.2 再计算第1个字符原点坐标
*/
tFontBitMap.iCurOriginX = iNewFirstFontTopLeftX - iFirstFontTopLeftX;
tFontBitMap.iCurOriginY = iNewFirstFontTopLeftY - iFirstFontTopLeftY;
pucBufStart = pucTextString;
bHasGetCode = 0;
while (1)
{
iLen = GetCodeFrmBuf(pucBufStart, pucBufEnd, &dwCode);
if (0 == iLen)
{
/* 字符串结束 */
if (!bHasGetCode)
{
DBG_PRINTF("%s %s %d\n", __FILE__, __FUNCTION__, __LINE__);
return -1;
}
else
{
break;
}
}
bHasGetCode = 1;
pucBufStart += iLen;
iError = GetFontBitmap(dwCode, &tFontBitMap);
if (0 == iError)
{
/* 显示一个字符 */
if (MergeOneFontToVideoMem(&tFontBitMap, ptVideoMem))
{
DBG_PRINTF("MergeOneFontToVideoMem error for code 0x%x\n", dwCode);
return -1;
}
tFontBitMap.iCurOriginX = tFontBitMap.iNextOriginX;
tFontBitMap.iCurOriginY = tFontBitMap.iNextOriginY;
}
else
{
DBG_PRINTF("GetFontBitmap for drawing error!\n");
}
}
return 0;
}
static void ClearRectangleInVideoMem(int iTopLeftX, int iTopLeftY, int iBotRightX, int iBotRightY, PT_VideoMem ptVideoMem, unsigned int dwColor)
{
int x, y;
for (y = iTopLeftY; y <= iBotRightY; y++)
for (x = iTopLeftX; x <= iBotRightX; x++)
SetColorForPixelInVideoMem(x, y, ptVideoMem, dwColor);
}
/* 返回值: 设置了VideoMem中多少字节 */
static int SetColorForPixelInVideoMem(int iX, int iY, PT_VideoMem ptVideoMem, unsigned int dwColor)
{
unsigned char *pucVideoMem;
unsigned short *pwVideoMem16bpp;
unsigned int *pdwVideoMem32bpp;
unsigned short wColor16bpp; /* 565 */
int iRed;
int iGreen;
int iBlue;
pucVideoMem = ptVideoMem->tPixelDatas.aucPixelDatas;
pucVideoMem += iY * ptVideoMem->tPixelDatas.iLineBytes + iX * ptVideoMem->tPixelDatas.iBpp / 8;
pwVideoMem16bpp = (unsigned short *)pucVideoMem;
pdwVideoMem32bpp = (unsigned int *)pucVideoMem;
//DBG_PRINTF("%s %s %d\n", __FILE__, __FUNCTION__, __LINE__);
//DBG_PRINTF("x = %d, y = %d\n", iX, iY);
switch (ptVideoMem->tPixelDatas.iBpp)
{
case 8:
{
*pucVideoMem = (unsigned char)dwColor;
return 1;
break;
}
case 16:
{
iRed = (dwColor >> (16+3)) & 0x1f;
iGreen = (dwColor >> (8+2)) & 0x3f;
iBlue = (dwColor >> 3) & 0x1f;
wColor16bpp = (iRed << 11) | (iGreen << 5) | iBlue;
*pwVideoMem16bpp = wColor16bpp;
return 2;
break;
}
case 32:
{
*pdwVideoMem32bpp = dwColor;
return 4;
break;
}
default :
{
return -1;
}
}
return -1;
}
static int MergeOneFontToVideoMem(PT_FontBitMap ptFontBitMap, PT_VideoMem ptVideoMem)
{
int i;
int x, y;
int bit;
int iNum;
unsigned char ucByte;
if (ptFontBitMap->iBpp == 1)
{
for (y = ptFontBitMap->iYTop; y < ptFontBitMap->iYMax; y++)
{
i = (y - ptFontBitMap->iYTop) * ptFontBitMap->iPitch;
for (x = ptFontBitMap->iXLeft, bit = 7; x < ptFontBitMap->iXMax; x++)
{
if (bit == 7)
{
ucByte = ptFontBitMap->pucBuffer[i++];
}
if (ucByte & (1<<bit))
{
iNum = SetColorForPixelInVideoMem(x, y, ptVideoMem, COLOR_FOREGROUND);
}
else
{
/* 使用背景色 */
// g_ptDispOpr->ShowPixel(x, y, 0); /* 黑 */
iNum = SetColorForPixelInVideoMem(x, y, ptVideoMem, COLOR_BACKGROUND);
}
if (iNum == -1)
{
return -1;
}
bit--;
if (bit == -1)
{
bit = 7;
}
}
}
}
else if (ptFontBitMap->iBpp == 8)
{
for (y = ptFontBitMap->iYTop; y < ptFontBitMap->iYMax; y++)
for (x = ptFontBitMap->iXLeft; x < ptFontBitMap->iXMax; x++)
{
//g_ptDispOpr->ShowPixel(x, y, ptFontBitMap->pucBuffer[i++]);
if (ptFontBitMap->pucBuffer[i++])
{
iNum = SetColorForPixelInVideoMem(x, y, ptVideoMem, COLOR_FOREGROUND);
}
else
{
iNum = SetColorForPixelInVideoMem(x, y, ptVideoMem, COLOR_BACKGROUND);
}
if (iNum == -1)
{
return -1;
}
}
}
else
{
DBG_PRINTF("ShowOneFont error, can't support %d bpp\n", ptFontBitMap->iBpp);
return -1;
}
return 0;
}
上面的是单击实现时间间隔的加减,那长按如何实现呢?
思路是这样的:第一次点击时,记录点击的时间,然后显示下沉效果,如果点着不放,
则比对此次时间与上次点击时间间隔是否>2s, 如果是,则设置一个标志位 bFast=1 以开启长按累加/减模式,
如果继续不放,后面就会进入到快速累加/减模式,每隔50ms加/减一次。如果释放了,
则清零bFast 以 取消长按模式,可以单击累加/减。
注意:不清零会导致长按<2s也会出现连续的加/减。因此,编程注意这点。
下面看看代码实现。
/* 2. 获得输入事件(暂且只有触摸屏作为输入) */
while(1)
{
iIndex = GetIntervalPageInputEvent(&g_tIntervalPageLayout, &tInputEvent);
if(tInputEvent.iPressure == 0)
{
bFast = 0; // 记得清零 !
if(bPressed) // 曾经按下
{
/* 曾经有按钮被按下 */
ReleaseButton(&g_atIntervalPageLayout[iIndexPressed]);
bPressed = 0;
if(iIndex == iIndexPressed)
{
switch (iIndexPressed)
{
case 0: /* inc按钮 */
{
iIntervalSecond++;
if (iIntervalSecond == 60)
{
iIntervalSecond = 0;
}
GenerateIntervalPageSpecialIcon(iIntervalSecond, ptDevVideoMem);
break;
}
case 2: /* dec按钮 */
{
iIntervalSecond--;
if (iIntervalSecond == -1)
{
iIntervalSecond = 59;
}
GenerateIntervalPageSpecialIcon(iIntervalSecond, ptDevVideoMem);
break;
}
case 3: /* ok按钮 */
{
return;
break;
}
case 4: /* cancel按钮 */
{
return;
break;
}
default:
{
break;
}
}
}
iIndexPressed = -1;
}
}
else
{
if(iIndex != -1)
{
if(!bPressed && (iIndex != 1))
{
/* 未曾按下按钮 */
bPressed = 1;
iIndexPressed = iIndex;
tInputEventPrePress = tInputEvent; /* 记录下来 */
PressButton(&g_atIntervalPageLayout[iIndexPressed]);
}
}
/* 如果按下的是"inc.bmp"或"dec.bmp"
* 连按2秒后, 飞快的递增或减小: 每50ms变化一次
*/
if ((iIndexPressed == 0) || (iIndexPressed == 2))
{
if (bFast && (TimeMSBetween(tInputEventPrePress.time, tInputEvent.time) > 50))
{
iIntervalSecond = iIndexPressed ? (iIntervalSecond - 1) : (iIntervalSecond + 1);
if (iIntervalSecond == 60)
{
iIntervalSecond = 0;
}
else if (iIntervalSecond == -1)
{
iIntervalSecond = 59;
}
GenerateIntervalPageSpecialIcon(iIntervalSecond, ptDevVideoMem);
tInputEventPrePress = tInputEvent;
}
if (TimeMSBetween(tInputEventPrePress.time, tInputEvent.time) > 2000)
{
bFast = 1;
tInputEventPrePress = tInputEvent;
}
}
}
}
六、全文总结
前文显示主界面基础上,增加了两个页面——设置页面和时间设置页面,此外实现时间间隔的加减(点击或长按)。
对于页面的显示,套路与显示主页面完全一样(代码可直接复制,再修改即可)。
三个页面的区别在于图标位置计算越来越复杂。这个并不难理解的,图纸上先画一下确定坐标位置再编程实现即可。
本文的难点在于加减时间间隔,涉及一系列的处理。但总体思路还是很清晰的。
第一步:计算时间间隔字符串在LCD的坐标位置
第二步:根据字符串每个字符编码值确定字体位图数据,再写入缓冲块“相应位置”,最后刷到LCD的显存FB显示。