uCGUI窗口对象的创建机制详解
一、核心数据结构与属性定义
窗口对象结构
/* 窗口对象结构 共30字节 */
typedef struct {
GUI_RECT winRect; //窗口区域(x0,y0,x1,y1) 8字节
GUI_RECT dirtyRect; //失效区域(x0,y0,x1,y1) 8字节
WM_CALLBACK* pfCallback; //回调函数指针 4字节
WM_HWIN hNextSibling; //兄弟窗口链表指针 2字节
WM_HWIN hParentWin; //父窗口句柄 2字节
WM_HWIN hFirstChildWin; //第一个子窗口句柄 2字节
WM_HWIN hPrevSibling; //前一个兄弟窗口 2字节
U16 wmFlags; //状态标志 2字节
} WM_OBJECT;
窗口创建标志位定义
#define WM_CF_TRANSPARENT (1<<0) /* 透明窗口标志。不填充整个客户区的窗口需要定义此标志 */
#define WM_CF_HIDE (0<<1) /* 创建后隐藏窗口(默认) */
#define WM_CF_SHOW (1<<1) /* 创建后显示窗口 */
#define WM_CF_MEMDEV (1<<2) /* 重绘时使用内存设备 */
#define WM_CF_STAYONTOP (1<<3) /* 窗口置顶 */
#define WM_CF_DISABLED (1<<4) /* 禁用状态:不接收触摸和鼠标输入 */
/* 仅用于创建过程的标志 */
#define WM_CF_ACTIVATE (1<<5) /* 创建后自动激活窗口 */
#define WM_CF_FOREGROUND (0<<6) /* 创建后置于前台(默认) */
#define WM_CF_BACKGROUND (1<<6) /* 创建后置于后台 */
/* 锚点定位标志 */
#define WM_CF_ANCHOR_RIGHT (1<<7) /* 右锚点:父窗口调整大小时,与右边距保持不变 */
#define WM_CF_ANCHOR_BOTTOM (1<<8) /* 下锚点:父窗口调整大小时,与下边距保持不变 */
#define WM_CF_ANCHOR_LEFT (1<<9) /* 左锚点:父窗口调整大小时,与左边距保持不变 */
#define WM_CF_ANCHOR_TOP (1<<10) /* 上锚点:父窗口调整大小时,与上边距保持不变 */
#define WM_CF_FIXED_OUTLINE (1<<11) /* 固定轮廓。仅对透明窗口有意义,若透明窗口无固定轮廓,
则重绘时清除背景而非窗口本身,会增加计算开销 */
#define WM_CF_LATE_CLIP (1<<12)
#define WM_CF_MEMDEV_ON_REDRAW (1<<13)
#define WM_CF_RESERVED3 (1<<14)
#define WM_CF_RESERVED4 (1<<15)
在实际开发中,WM_CF_SHOW、WM_CF_STAYONTOP、WM_CF_HIDE、WM_CF_ACTIVATE这几个标志使用频率最高。
二、窗口创建流程深度解析
1、WM_CreateWindowAsChild 函数实现
WM_HWIN WM_CreateWindowAsChild( int x0, int y0, int width, int height
,WM_HWIN hParent, U16 Style, WM_CALLBACK* pfCallback
,int nExtraBytes) {
WM_OBJECT* pWindow;
WM_HWIN hWindow;
WM_ASSERT_NOT_IN_PAINT();
WM_LOCK();
Style |= WM__CreateFlags;
/* 默认父窗口为桌面窗口 */
if (!hParent) {
if (WM__nWindowCount) {
#if GUI_NUM_LAYERS == 1
hParent = WM__ahDesktop[0];
#else
hParent = WM__ahDesktop[GUI_Context.SelLayer];
#endif
}
}
if (hParent == WM_UNATTACHED) {
hParent = WM_HWIN_NULL;
}
if (hParent) {
WM_OBJECT* pParentObj = WM_H2P(hParent);
x0 += pParentObj->winRect.x0;
y0 += pParentObj->winRect.y0;
if (width==0) {
width = pParentObj->winRect.x1 - pParentObj->winRect.x0 + 1;
}
if (height==0) {
height = pParentObj->winRect.y1 - pParentObj->winRect.y0 + 1;
}
}
if ((hWindow = (WM_HWIN) GUI_ALLOC_AllocZero(nExtraBytes + sizeof(WM_OBJECT))) == 0) {
GUI_DEBUG_ERROROUT("WM_CreateWindow: 内存不足,无法创建窗口");
} else {
WM__nWindowCount++;
pWindow = WM_H2P(hWindow);
/* 初始化窗口参数 */
pWindow->winRect.x0 = x0;
pWindow->winRect.y0 = y0;
pWindow->winRect.x1 = x0 + width - 1;
pWindow->winRect.y1 = y0 + height - 1;
pWindow->pfCallback = pfCallback;
/* 复制可接受的标志位 */
pWindow->wmFlags |= (Style & (WM_CF_SHOW |
WM_SF_MEMDEV |
WM_CF_MEMDEV_ON_REDRAW |
WM_SF_STAYONTOP |
WM_CF_DISABLED |
WM_SF_CONST_OUTLINE |
WM_SF_HASTRANS |
WM_CF_ANCHOR_RIGHT |
WM_CF_ANCHOR_BOTTOM |
WM_CF_ANCHOR_LEFT |
WM_CF_ANCHOR_TOP |
WM_CF_LATE_CLIP));
/* 添加到链表管理 */
_AddToLinearList(hWindow);
WM__InsertToParentChain(hWindow, hParent);
/* 根据窗口样式执行相应操作 */
if (Style & WM_CF_ACTIVATE) {
WM_SelectWindow(hWindow);
}
#if WM_SUPPORT_TRANSPARENCY
if (Style & WM_SF_HASTRANS) {
WM__nTransparentCnt++;
}
#endif
if (Style & WM_CF_BACKGROUND) {
WM_BringToBottom(hWindow);
}
if (Style & WM_CF_SHOW) {
pWindow->wmFlags |= WM_SF_VISIBLE;
WM_InvalidateWindow(hWindow);
}
WM__SendMsgNoData(hWindow, WM_CREATE);
}
WM_UNLOCK();
return hWindow;
}
该函数的执行流程如下:首先根据父窗口坐标计算当前窗口的绝对坐标和尺寸。接着从动态内存池分配窗口对象所需的内存空间,并将窗口参数写入该内存区域。关键步骤包括:将新窗口加入全局窗口管理链表,以及将窗口插入父窗口的子窗口链表中。若创建时指定了WM_CF_SHOW标志,则设置窗口可见状态位并标记为无效,这样在后续GUI_Exec()或WM_Exec()调用时会触发窗口重绘。
2、_AddToLinearList 函数实现
static void _AddToLinearList(WM_HWIN hNewWin) {
WM_OBJECT* pFirst;
WM_OBJECT* pNew;
if (WM__hFirstWindow) {
pFirst = WM_H2P(WM__hFirstWindow);
pNew = WM_H2P(hNewWin);
/*
* 桌面窗口--->最近创建的窗口1--->较早创建的窗口2~~~--->0
* 插入新窗口后:
* 桌面窗口--->新窗口--->最近创建的窗口1--->较早创建的窗口2~~~--->0
*/
pNew->hNextSibling = pFirst->hNextSibling;
pFirst->hNextSibling = hNewWin;
} else {
WM__hFirstWindow = hNewWin;
}
}
此函数将新创建的窗口添加到全局线性链表中。该链表基于uCGUI的动态内存机制,通过WM_OBJECT结构体中的hNextSibling成员连接形成单向链表。链表的组织方式保证了最早创建的窗口位于链表末尾,最近创建的窗口靠近头部。
3、WM__InsertToParentChain 函数实现
/*********************************************************************
*
* WM__InsertToParentChain
*
* 功能说明:
* 将窗口插入指定父窗口的子窗口链表中
* 同一层级的窗口中,新窗口置于最顶层
*/
void WM__InsertToParentChain(WM_HWIN hWin, WM_HWIN hParent) {
int bStayOnTop;
WM_HWIN hChild;
WM_OBJECT* pWin;
WM_OBJECT* pParent;
WM_OBJECT* pSibling;
if (hParent) {
pWin = WM_H2P(hWin);
pWin->hNextSibling = 0;
pWin->hParentWin = hParent;
pParent = WM_H2P(hParent);
bStayOnTop = pWin->wmFlags & WM_CF_STAYONTOP;
hChild = pParent->hFirstChildWin;
/* 父窗口没有子窗口时,直接作为首个子窗口 */
if (hChild == 0) {
pParent->hFirstChildWin = hWin;
return;
}
/* 如果首个子窗口是置顶窗口,而新窗口不是,则插入到链表头部 */
pSibling = WM_H2P(hChild);
if (!bStayOnTop) {
if (pSibling->wmFlags & WM_SF_STAYONTOP) {
pWin->hNextSibling = hChild;
pParent->hFirstChildWin = hWin;
return;
}
}
/* 遍历链表,找到合适的位置插入 */
do {
WM_OBJECT* pNext;
WM_HWIN hNext;
if ((hNext = pSibling->hNextSibling) == 0) {
pSibling->hNextSibling = hWin;
break;
}
pNext = WM_H2P(hNext);
if (!bStayOnTop) {
if (pNext->wmFlags & WM_SF_STAYONTOP) {
pSibling->hNextSibling = hWin;
pWin->hNextSibling = hNext;
break;
}
}
pSibling = pNext;
} while (1);
#if WM_SUPPORT_NOTIFY_VIS_CHANGED
WM__NotifyVisChanged(hWin, &pWin->winRect);
#endif
}
}
此函数负责将新窗口添加到父窗口的子窗口链表中。链表基于动态内存,通过hFirstChildWin和hNextSibling成员连接。
插入规则如下:
1、若父窗口尚无子窗口,直接将其作为首个子窗口。
插入前: 父窗口--->0 ====> 插入后: 父窗口--->当前窗口--->0
2、若父窗口已有子窗口,且首个子窗口具有置顶标志,而新窗口没有,则将新窗口插入链表头部。
插入前:父窗口--->长子--->~~~--->0 => 插入后:父窗口--->当前窗口--->长子--->~~~--->0
3、若父窗口已有子窗口,且首个子窗口无置顶标志,新窗口也无此标志,则插入到所有无置顶标志窗口之后。
插入前:
父窗口--->长子(普通)--->次子(普通)--->三子(置顶)--->~~~--->0 => 插入后:
父窗口--->长子(普通)--->次子(普通)--->当前窗口--->三子(置顶)--->~~~--->0
4、若父窗口已有子窗口,且首个子窗口无置顶标志,但新窗口有置顶标志,则插入到链表末尾。
插入前:
父窗口--->长子(普通)--->次子(普通)--->三子(置顶)--->~~~--->0 => 插入后:
父窗口--->长子(普通)--->次子(普通)--->三子(置顶)--->~~~--->当前窗口--->0
三、窗口层级与绘制顺序
1、按照窗口层级组织结构,链表头部的窗口显示在底层,链表尾部的窗口显示在顶层。窗口重绘时,从链表头部依次向尾部遍历执行。
2、子窗口始终显示在父窗口的上层位置。
四、系统桌面窗口的创建
桌面窗口在GUI_Init函数执行过程中创建。当系统启用窗口管理功能时,GUI_Init会调用WM_Init函数进行窗口管理器初始化,此时会自动创建桌面窗口。相关实现代码如下:
/*********************************************************************
*
* WM_Init
*/
void WM_Init(void) {
......
WM__ahDesktop[0] = WM_CreateWindow(0, 0, GUI_XMAX, GUI_YMAX, WM_CF_SHOW, cbDesktop, 0);
......
WM_InvalidateWindow(WM__ahDesktop[i]);
......
WM_SelectWindow(WM__ahDesktop[0]);
}
桌面窗口的默认回调处理函数
static void cbDesktop( WM_MESSAGE* pMsg) {
const WM_KEY_INFO* pKeyInfo;
switch (pMsg->MsgId) {
case WM_KEY:
pKeyInfo = (const WM_KEY_INFO*)pMsg->Data.p;
if (pKeyInfo->PressedCnt == 1) {
GUI_StoreKey(pKeyInfo->Key);
}
break;
case WM_PAINT:
{
int nLayer;
#if GUI_NUM_LAYERS > 1
nLayer = _DesktopHandle2Index(pMsg->hWin);
#else
nLayer = 0;
#endif
if (WM__aBkColor[nLayer] != GUI_INVALID_COLOR) {
GUI_SetBkColor(WM__aBkColor[nLayer]);
GUI_Clear();
}
}
default:
WM_DefaultProc(pMsg);
}
}
需要特别说明的是:桌面窗口的句柄默认为1,即使用动态内存块数组的第二个槽位。动态内存分配时从数组的第二个元素开始使用。