一、 UI基本结构
UI的基本的结构如下图所示:
最底层的是UI的绘图接口API,在不同的平台上移值时,只要实现绘图的API即可,为上层的基本绘图操作。
UI画布管理:
实现绘图块的概念,为UI相关的消息事件提供支持。
UI的绘图以“画布”为基本单元,在一块画布里通过绘图接口API,绘制不同的图形,可以在“画布”上画点,线,面,贴图等,消息事件也都是针对“画布”的操作产生,如在画布上的鼠标事件,如重绘一块“画布”,移动“画布”等。
“画布”相当于面向对像里的所有UI里对像的父类,如控件里的窗体,文本框,按钮都相当于是继承“画布”的子类。
一块“画布”处理成什么样子,响应什么消息事件,完全取决于它的回调函数里的绘图和处理的事件。画布实现了一些所有UI对像所共有的一些基本特征和功能。
UI控件:
UI的基本控件,每一个控件都是一块画布,它只是根据不同的需求绘画出不同的界面,处理它感兴趣的事件。
二、 绘图基本API接口
1. 基本接口
UI绘图的基本操作接定义在ui_adapter.h文件中,所有的上层操作都是通过下面这几个接口实现的,在不同的平台上移植,只要实现下面的几个方法即可:
ui_adapter.h中定义了如下的结构体,
typedef struct _GUI_DrawApi
{
tagUI_SetPointPixel pfSetPointPixel;
tagUI_GetPointPixel pfGetPointPixel;
tagUI_DrawHLine pfDrawHLine;
tagUI_DrawVLine pfDrawVLine;
tagUI_DrawFillRect pfDrawFillRect;
tagUI_DrawBitmap pfDrawBitmap;
tagUI_GetScreenX pfGetScreenX;
tagUI_GetScreenY pfGetScreenY;
/*后面几个接口可以不实现*/
tagUI_SetAlphaValue pfSetAlpha;
tagUI_GetAlphaValue pfGetAlpha;
tagUI_EnableSDKDraw pfEnableSDKDraw;
tagUI_DisableSDKDraw pfDisableSDKDraw;
tagUI_SetLayer pfSetLayer;
tagUI_GetLayer pfGetLayer;
}UI_DriveDrawApi;
不同的平台,只要实现如上的结构即可。
ui_adapter.h导出了以上接口的基本调用:
//获取分辨率的X,Y
int UI_GetScreenX();
int UI_GetScreenY();
//写点和获取点
void UI_SetPointPixel(int x, int y, int pixelValue);
unsigned int UI_GetPointPixel(int x, int y);
//画直线
void UI_HLine(int x0, int y0, int x1);
void UI_VLine(int x0, int y0, int y1);
//填充矩形
void UI_FillRect(int x0, int y0, int x1, int y1);
//矩形
void UI_DrawRect(int x0, int y0, int x1, int y1);
//位图
void UI_DrawBitmap( int x0, int y0,
int xsize, int ysize,
int x_offset, int y_offset,
UI_BITMAPINFOHEADER *bmpInfo,
const U8 *pData);
//设置alpha透明
void UI_SetAlphaValue(int alpha);
int UI_GetAlphaValue();
//使用硬件SDK绘图(加速绘图,如果硬件有支持)
void UI_EnableSDKDraw();
void UI_DisableSDKDraw();
//设置获取当前绘图的层(如果有分层绘图的操作,如果有分层绘图的实现)
int UI_SetLayer(int layerId);
int UI_GetLayer();
//初始化绘图的接口,在绘之前,最开始应调用这个过程来初始化
void UI_DrawApiInit(UI_DriveDrawApi* pDriveDrawApi);
2. Windows模拟器绘图接口实现
如以一个Windows程序的模拟器实现为例,在程序的一个单元中,如:UIWINDrawApi.h中定义如下的过程:
void WIN_SetAlphaValue(int alpha);
int Win_GetAlphaValue();
void WIN_SetPointPixel(int x, int y, PIXELINDEX pixelValue);
unsigned int WIN_GetPointPixel(int x, int y);
void WIN_DrawHLine(int x0, int y0, int x1);
void WIN_DrawVLine(int x0, int y0, int y1);
void WIN_DrawFillRect(int x0, int y0, int x1, int y1);
void WIN_DrawBitmap(int x0, int y0,
int xsize, int ysize,
int x_offset, int y_offset,
UI_BITMAPINFOHEADER *bmpInfo,
const U8 *pData);
void WIN_EnableSDKDraw();
void WIN_DisableSDKDraw();
int WIN_SetLayer(int layerId);
int WIN_GetLayer();
int WIN_GetScreenX();
int WIN_GetScreenY();
void WIN_StartDraw();
void WIN_EndDraw();
在程序的一个单元中按UI的绘图接口实现上面的方法。声明一个UI_DriveDrawApi实例变量:
static UI_DriveDrawApi _DriverDrawApiWin = {
WIN_SetPointPixel,
WIN_GetPointPixel,
WIN_DrawHLine,
WIN_DrawVLine,
WIN_DrawFillRect,
WIN_DrawBitmap,
WIN_GetScreenX,
WIN_GetScreenY,
WIN_SetAlphaValue,
Win_GetAlphaValue,
WIN_EnableSDKDraw,
WIN_DisableSDKDraw,
WIN_SetLayer,
WIN_GetLayer
};
在合适的地方,调用:
UI_DrawApiInit(&_DriverDrawApiWin);
这样后,UI的绘图操作就完成了。
三、 UI画布绘图管理
1. UI绘图的画布概念
如果把显示器想像成一块画布,需要实现在这个画布上绘图,那么所有的绘图操作都是在这块大的画布上实现的。
在一个UI中常见的元素有:窗体,按钮,文字等,如果这里把所有的UI里的对像抽像成一块画布单元,那么任何UI对像都是在一个范围内的画布上画点、线、面体现出来,如下面的一个窗体。
基本上大多的UI里有“容器”的这个概念,像窗体就是一个“容器”,可以在它上面放文字,按钮,图片等,而按钮上面又可以放文字,图片。
根据前面的把所有的对像都抽像成画布,这样画布是可以嵌套的,如下图:
“画布A”包含“画布B”,“画布B”包含“画布C”,表现在UI里,可能画布A是一个窗体,画布B是一个按钮,画布C是一段文字,或其它任何的对像,同时子画布里的对像是不能画出父画布的边界!
显示器抽像成了一个大的画布,那么所有其它的画布里操作,都是在这个画布容器里,这样画布的关系是一个“树”型关系:
关系如下:
在很多UI库里把上面的“显示器画布”叫做“桌面”,就像用的操作系统的UI一样,也有一个“桌面”,其它的任何窗体都在这个“桌面”里绘图展示。
同样这里也创建一个根“画布”,所有的其它画布都在这个根画布里绘图。
2. UI画布的基本数据结构
定义一个如下的结构表示画布:
typedef struct _TRect{
I32 x0;
I32 y0;
I32 x1;
I32 y1;
}TRect;
typedef struct _WM_Base{
UI_HWND hwnd; /*窗体画布句柄*/
TRect rect; /*记录窗体画布所在的位置,是以坐标原点记录的*/
struct _WM_Base* pParent; /*窗体画布的父窗体*/
struct _WM_Base* pFirstChild; /*窗体画布的第一个子窗体*/
struct _WM_Base* pNextSibling; /*下一个兄弟节点*/
struct _WM_Base* pPrevSibling;
}WM_Base;
#define Hwnd2P(hwnd) ((WM_Base*)(hwnd))
#define P2Hwnd(pWin) ((int)(pWin))
hwnd: 表示一个画布的句柄,这里只是简单的把地址指针转了一下。
rect :表示画布的绘图区域,在这块画布的所有绘布不应超出这个区域。
pParent:指向上层画布
pFirstChild:指向子画布
pNextSibling,pPrevSibling:指向同层的画布,这里表示的结构与上节说明的图型结构出现了一点不同,这里同一层的画布使用了双向链表来链接起来了,
如下面的结构关系:
“树”型是这样的:
在UI初始化时,就会创建一个最顶层的“桌面画布”,以后调用创建画布,如果没有指定父节点则会以个顶层的桌面画布为父节点。
在显示时,重绘的关系应是这样:UI先
1:绘制desktop
2:绘制A
3:绘制B
4:绘制C
最后绘制的画布,显示在最顶层。
3. UI坐标系
UI的坐标系是以显示器的左上方为起点的
创建一个画布时,他的坐标记录在WM_Base 结构里的 rect字段里。
在UI.h头文件是定义了一个如下的结构:
typedef struct _UI_SCREEN{
int ScreenX;
int ScreenY;
U32 PixelColor; //DrawApi color
UI_Font* Font;
}TUIScreen;
ScreenX,ScreenY分别用来记录当前分辨率宽,高。
这两个值是在UI初始化时WM_Init
分别调用UI_GetScreenX(),UI_GetScreenY()来初始化的,这两个接口是UI底层提供的。
4. UI初始化和画布的创建
UI画布的相关代码 在UI_WM.h和UI_WM.c中。
A. UI的初始化
WM_Init.
如下的代码
void WM_Init()
{
int w, h;
if(WM_GetBkWin())
return;
_InvalidWindowNum = 0;
memset(&_CurWMContext, 0 ,sizeof(_CurWMContext));
_CurWMContext.invalidRectList = (TRectListNode*)malloc(sizeof(TRectListNode));
_CurWMContext.invalidRectList->next = NULL;
_CurWMContext._hCurrentWinHwnd = 0;
w = UI_GetScreenX();
h = UI_GetScreenY();
Screen.ScreenX = w;
Screen.ScreenY = h;
/*创建一个桌面窗体,它是所有窗体的父窗体*/
_CurWMContext._hBackgoundWinHwnd = WM_Create(0, 0, w, h, 0, _DefaultBackgroundWinProc, WM_CS_SHOW);
}
上面的过程大概是这样:
1:WM_GetBkWin调用是获取桌面画布,如果桌面画布已经存在(大于0),那么表示系统已经初始化过了。
2: 初始化Screen里的ScreenX,ScreenY
3:调用WM_Create创建一个画布,并用
_CurWMContext._hBackgoundWinHwnd记录下来。
_CurWMContext.invalidRectList,是一个TRectListNode结构,它记录一个画布绘图时的更新区域列表,后面再说到它。
B. 创建画布
创建画布是通过调用WM_Create,
接口如下:
UI_HWND WM_Create(int x, int y, int width, int height, UI_HWND Parent ,WM_WndProc WndProc, UI_WM_STATUS status);
x, y 分别指定画布的相对于父节点的位置,注意不是UI坐标系的位置,是相对于父画布的位置。
如上图中创建B画布的时的x,y是到A的左上角位置
注:在前面说到 WM_Base 中有定义一个TRect的结构来记录画布的显示位置和界限,在WM_Create中,它会把 x, y转换为相对于UI坐标系的位置来记录。
width, height 指定画布的宽和高.
WndProc:指定画布的回调函数
status: 指定一些状态信息.
5. UI的消息事件处理
UI中的消息事件处理很简单,都是同步的,没有什么消息,事件链什么队列的概念。
1) UI中的消息结构定义:
typedef struct{
UI_HWND hwnd;
UI_WM_MSG msgId;
MSG_WPARAM wParam;
MSG_LPARAM lParam;
}WM_Message;
hwnd是画布的句柄
msgId是消息处理的ID
wParam和lParam根据不同的消息ID,有不同的内容
2) 几个基本的消息如下:
消息ID
解释
UI_WM_CREATE
在一个UI画布创建时会发送的消息,也是第一个收到的消息
UI_WM_DESTORY
画布删除时会发的消息
UI_WM_SHOW
显示画布时
UI_WM_PAINT
重画画布时,这是最重要的一个消息,收到它时,表示UI正在重绘它的区域
UI_WM_MOUSE
原始的鼠标事件
UI_WM_MOUSEOVER
鼠标在画布上移动时的消息
UI_WM_MOUSE_MOVE_OUT
鼠标从一个画布移出时会收到这个消息
UI_WM_MOUSE_MOVE_IN
鼠标移入一个画布时
UI_WM_LBUTTONUP
鼠标左键抬起
UI_WM_LBUTTONDOWN
鼠标左键按下
UI_WM_RBUTTONUP
鼠标右键抬起
UI_WM_RBUTTONDOWN
鼠标右键按下
UI_WM_GETFOCUS
画布获得焦点时
UI_WM_LOSTFOCUS
画布失去焦点时
3) 设置消息回调函数
UI的消息都是在画布的回调函数中处理的。
在创建画布时,如果没有指定回调函数,则UI会给一个基本的默认的消息处理过程给它,指定的回调函数通过 WM_Base中的 wndProc 记录。
也可以在画布创建后,调用 WM_SetCallBack来指定:
WM_WndProc WM_SetCallBack(UI_HWND hwnd, WM_WndProc WndProc)
它返回的是原来的回调函数。
如下的一个消息回调定义:
void WmCallBack(WM_Message *msg)
{
int x,y;
TRect rect;
WM_GetRect(msg->hwnd, &rect);
switch(msg->msgId)
{
case UI_WM_LBUTTONDOWN:
x = LOWORD(msg->lParam);
y = HIWORD(msg->lParam);
WM_Move(h1, x, y);
break;
case UI_WM_PAINT:
UI_SetColor(0x0);
WM_Clear();
break;
default:
WM_DefaultProc(msg);
}
}
在上面的回调中,分别处理了鼠标的按下动作,和自身的绘制,其它的动作调用了画面的默认处理函数(WM_DefaultProc), UI_SetColor会设置绘图API的颜色值,WM_Clear()会用设置的颜色值填充一个矩形,相当于清除原来画布上的东西。
6. UI画布绘图机制
UI中画布是记录在一个树型结构中的,从一个“桌面画布”的根结点开始。每一个画布的是否更新重画,是否显示,主要通过下面的几个状态来决定。
1) UI画布的几个主要状态
一个画布的状态是用WM_Base中的status来记录
下面是几个主要的状态说明
WM_CS_SHOW
画布是否显示,在重绘时,如果一个画布不显示,则不会重绘它
WM_CS_INVALID
画布是否有效,如果标志为无效了,表示需要重绘,重会的区域为WM_Base结构中的invalidRect(TRect)
WM_CS_STAYONTOP
画布是否在树型结构中同层的顶层
2) 画布的层叠关系
在UI中,不同的画布,显示时,有的画布,显示在另一些画布的上方,有的显示在另一些画布的下方,这样即存在一个层次关系。如下图:
画布B显示在画布A上面挡住了A的部分显示,同时C又显示在B的上方。
在UI的画布树型数据结构中,A,B,C是处在同一层当中,结构如下:
A,B,C用一个双向链表,链接起来了,A处在显示的最底层,它在树型同层链表中处理最前面,C在最顶端,它会画在最上层。
这样安排画布的数据结构后,是为了方便画布绘画时,决定谁先画,谁后画的关系。
3) 绘图的时机
所有的画布的重绘都是在设定的回调函数收到UI_WM_PAINT消息时,才应画图,可以调用ui_adapter.h里的几个基本API绘图。
4) UI画布重绘的过程
在UI中定义了一个 UI_Refresh(UI_Base.c中)的过程,这个过程中又会对 WM_RePaint的调用。
WM_RePaint的过程就是从“桌面画布”开始检查,根据WM_Base中的status状态,是否有画布需要重绘,如果需要重绘,则调用画布相应的回调函数。
画布的重绘的先后顺序是先父节点,画布节点本身,子画布节点,再同层链表的后继节点。
5) 同层画布的重绘区域的分解
在WM_Base 中定义了invalidRect字段,来表示一个画布的需要重绘的区域。
UI中显示在同一个层次的画布,有的会有重叠,但一个底层的画布,需要重画时,它的重绘区域,显示出来后,会把上层的画布覆盖。可以有两种方法:
一种是方法是画完底层的画布的无效区域后,检查是否有上层的画布与其重叠,如果有重叠表明上层的画布也需要重绘:
如上图的A重绘后,会把A与B交界的区域显示为A的内容,为了显示B的内容,就应把B的无效区域包括与A的交界处,当A重绘完成后,再把B重绘。
这样的方式实现比较简单,有效,但会对同一个LCD的区域反复的画点,有可能出现闪屏的现像,这种问题可以使用一个缓存来解决,当把UI中的所有无效区域绘制完成后,再把变更的区域更新至LCD。
另一种 方式是对一个画布的无效区域分解。一个画布的无效区域是一个矩形,而这个无效区域,又与另一个画布有重叠时,重叠的也是一个矩形(有可能是一个点,一条直线,这里都看成矩形对待)。
如下图:
当A的无效区域(蓝线框)需要重绘时,为了不引起B的重绘,就要把蓝线框与B的交界排除去,一个方法是对蓝给框分解。
一个矩形与另一个矩形有交叉后,要把一个矩形排除,最多只能分出4个子矩形,如下图:
当内部蓝色的矩形对角,两个坐标完全落在一另一个矩形当中时,这样按上图分解成a,b,c,d四个矩形,外层矩形在重绘时,只要重会a,b,c,d这4个小矩形即可,内部的小矩形有角 落出 外部大的矩形时,分解的矩形也不会大于4个子矩形。
当然上面只是比较了一个画布,而每一个分解完的小矩形,还要比较每一个其它的比它层次高的画布是否有重叠,如果有重叠,又需要继续分解,直至所有的上层画布都比较完成,才算分解完。
把分解完成的矩形用一个链表记录(_CurWMContext中的invalidRectList),在重绘画布里,就需要重绘这个链表中的每一个矩形。
这样会带来另一些会问题,在UI_WM_PAINT中,如果画一条直线,跨了不同的重绘矩形,也需在分开来绘直直线。