自已实现一个UI库-UI核心绘图层管理

一、 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中,如果画一条直线,跨了不同的重绘矩形,也需在分开来绘直直线。

四、 位图操作

五、 鼠标

六、 基本控件

七、 文字字体

猜你喜欢

转载自blog.csdn.net/jhting/article/details/46913159