一、概述
µC/GUI的窗口重绘是学习者理解窗口工作原理和应用窗口操作的重点。µC/GUI的窗口重绘引入了回调机制,回调机制可以实现图形系统调用用户的代码,由于图形系统使用了剪切算法,使得屏幕重绘的效率和重绘的操作都大大提高。本文主要分析µC/GUI重绘窗口的过程,使学习者理解窗口的回调机制,为进一步的应用窗口操作打下一个好的基础。
回调机制后面的哲学
µC/GUI 为窗口和窗口对象(控件)提供的回调机制实质是一个事件驱动系统。正如在大多数视窗系统中一样,原则是控制流程不只是从用户程序到图形系统(用户程序调用图形系统函数来更新窗口),而且可以从用户程序到图形系统,同时也从图形系统回到用户程序,意思是图形系统也可以调用用户程序提供的回调函数来达到更新窗口的目的。这种机制——常常表现好莱坞法则的特点(“不要打电话给我们,我们会打电话给你们!”)——主要是视窗管理器为了启动窗口重绘的需要。与传统程序比较有差异,但它使对视窗管理器的无效逻辑开发成为可能。
二、与窗口有关的结构体和变量
1、WM_Obj
/* 窗体管理结构体 共30个字节 */ struct WM_Obj { GUI_RECT Rect; //窗体尺寸(x0,y0,x1,y1) 8个字节 GUI_RECT InvalidRect; //无效区域(x0,y0,x1,y1) 8个字节 WM_CALLBACK* cb; //回调函数 4个字节 WM_HWIN hNextLin; //指向链表中的下一个窗体 2个字节 WM_HWIN hParent; //当前窗体的父窗体 2个字节 WM_HWIN hFirstChild; //当前窗体的第一个子窗体 2个字节 WM_HWIN hNext; //下一个兄弟窗体 2个字节 U16 Status; //标志位 2个字节 };
2、WM_MESSAGE
struct WM_MESSAGE { int MsgId; //信息的类型 WM_HWIN hWin; //信息的接收窗口 WM_HWIN hWinSrc; //发送信息的源窗口 union { const void* p; int v; GUI_COLOR Color; } Data; };
Messages Ids
/********************************************************************* * * Messages Ids The following is the list of windows messages. */ #define WM_CREATE 0x0001 /* The first message received, right after client has actually been created */ #define WM_MOVE 0x0003 /* window has been moved (Same as WIN32) */ #define WM_SIZE 0x0005 /* Is sent to a window after its size has changed (Same as WIN32, do not change !) */ #define WM_DELETE 11 /* Delete (Destroy) command: This tells the client to free its data strutures since the window it is associates with no longer exists.*/ #define WM_TOUCH 12 /* Touch screen message */ #define WM_TOUCH_CHILD 13 /* Touch screen message to ancestors */ #define WM_KEY 14 /* Key has been pressed */ #define WM_PAINT 0x000F /* Repaint window (because content is (partially) invalid */ #if GUI_SUPPORT_MOUSE #define WM_MOUSEOVER 16 /* Mouse has moved, no key pressed */ #define WM_MOUSEOVER_END 18 /* Mouse has moved, no key pressed */ #endif #define WM_PID_STATE_CHANGED 17 /* Pointer input device state has changed */ #define WM_GET_INSIDE_RECT 20 /* get inside rectangle: client rectangle minus pixels lost to effect */ #define WM_GET_ID 21 /* Get id of widget */ #define WM_SET_ID 22 /* Set id of widget */ #define WM_GET_CLIENT_WINDOW 23 /* Get window handle of client window. Default is the same as window */ #define WM_CAPTURE_RELEASED 24 /* Let window know that mouse capture is over */ #define WM_INIT_DIALOG 29 /* Inform dialog that it is ready for init */ #define WM_SET_FOCUS 30 /* Inform window that it has gotten or lost the focus */ #define WM_GET_ACCEPT_FOCUS 31 /* Find out if window can accept the focus */ #define WM_NOTIFY_CHILD_HAS_FOCUS 32 /* Sent to parent when child receives / loses focus */ #define WM_NOTIFY_OWNER_KEY 33 /* Some widgets (e.g. listbox) notify owner when receiving key messages */ #define WM_GET_BKCOLOR 34 /* Return back ground color (only frame window and similar) */ #define WM_GET_SCROLL_STATE 35 /* Query state of scroll bar */ #define WM_SET_SCROLL_STATE 36 /* Set scroll info ... only effective for scrollbars */ #define WM_NOTIFY_CLIENTCHANGE 37 /* Client area may have changed */ #define WM_NOTIFY_PARENT 38 /* Notify parent. Information is detailed as notification code */ #define WM_NOTIFY_PARENT_REFLECTION 39 /* Notify parent reflection. Sometimes send back as a result of the WM_NOTIFY_PARENT message to let child react on behalf of its parent. Information is detailed as notification code */ #define WM_NOTIFY_ENABLE 40 /* Enable or disable widget */ #define WM_NOTIFY_VIS_CHANGED 41 /* Visibility of a window has or may have changed */ #define WM_HANDLE_DIALOG_STATUS 42 /* Set or get dialog status */ #define WM_GET_RADIOGROUP 43 /* Send to all siblings and children of a radio control when selection changed */ #define WM_MENU 44 /* Send to owner window of menu widget */ #define WM_SCREENSIZE_CHANGED 45 /* Send to all windows when size of screen has changed */ #define WM_TIMER 0x0113 /* Timer has expired (Keep the same as WIN32) */ #define WM_WIDGET 0x0300 /* 256 messages reserved for Widget messages */ #define WM_USER 0x0400 /* Reserved for user messages ... (Keep the same as WIN32) */ /********************************************************************* * * Notification codes * * The following is the list of notification codes send * with the WM_NOTIFY_PARENT message */ #define WM_NOTIFICATION_CLICKED 1 #define WM_NOTIFICATION_RELEASED 2 #define WM_NOTIFICATION_MOVED_OUT 3 #define WM_NOTIFICATION_SEL_CHANGED 4 #define WM_NOTIFICATION_VALUE_CHANGED 5 #define WM_NOTIFICATION_SCROLLBAR_ADDED 6 /* Scroller added */ #define WM_NOTIFICATION_CHILD_DELETED 7 /* Inform window that child is about to be deleted */ #define WM_NOTIFICATION_GOT_FOCUS 8 #define WM_NOTIFICATION_LOST_FOCUS 9 #define WM_NOTIFICATION_SCROLL_CHANGED 10 #define WM_NOTIFICATION_WIDGET 11 /* Space for widget defined notifications */ #define WM_NOTIFICATION_USER 16 /* Space for application (user) defined notifications */
3、调试时观察的全局变量
WM__NumWindows、WM__NumInvalidWindows、WM__FirstWin、NextDrawWin。
三、窗口重绘的过程
在分析窗口重绘的过程之前,我们先看一下怎么令一个窗口进行重绘呢?
要使一个窗口重绘,需要为这个窗口指定回调函数,可以在创建的时候指定或者利用函数WM_SetCallback进行指定。然后,令这个窗口无效,窗口无效可能只是窗口的一部分区域或者全部区域。最后,调用函数GUI_Exec()或者WM_Exec()来进行重绘,重绘的时候会调用我们之前定义的回调函数。
可见,我们设置了回调函数,包括使窗口无效都不能使屏幕发生改变,只有当执行了GUI_Exec()函数后,才能将之前的需要改变展现在屏幕上。
1、过程概览
GUI_Exec()-->
GUI_Exec1()-->
WM_Exec()-->
WM_Exec1()-->
_DrawNext-->
WM__Paint-->
WM_PaintWinAndOvlays-->
__Paint1-->
WM_SendMessage-->
WmCallback(回调函数)
2、GUI_Exec()
int GUI_Exec(void) { int r = 0; while (GUI_Exec1()) { r = 1; /* We have done something */ } return r; }
循环执行多次,直到把需要派发的消息和需要更新的窗口全部完成,才结束此函数。从GUI_Exec()到WM_Exec1(),可以看到有三个函数都有类似“int r = 0; xxx(xxx_Exec1()) {r = 1; }”这样的代码。对于GUI_Exec()中的while循环,只有当GUI_Exec1()返回0的时候,才能结束这个函数。而GUI_Exec1()返回0的时候,对应的是(*GUI_pfTimerExec)()和WM_Exec()都返回0,这就意味着这两个函数里边包含的工作已经完全完成。
3、 GUI_Exec1()
int GUI_Exec1(void) { int r = 0; /* Execute background jobs */ if (GUI_pfTimerExec) { if ((*GUI_pfTimerExec)()) { r = 1; /* We have done something */ } } #if GUI_WINSUPPORT /* If 0, WM will not generate any code */ if (WM_Exec()) r = 1; #endif return r; }
返回1,标志着目前GUI还有未处理的消息。返回0,标志着GUI所有的消息都已经处理完毕。
4、WM_Exec()
int WM_Exec(void) { int r = 0; while (WM_Exec1()) { r = 1; /* We have done something */ } return r; }
5、WM_Exec1()
返回1,标志着目前还有未处理的窗口。返回0,标志着所有的窗口都已经处理完毕。
int WM_Exec1(void) { /* Poll PID if necessary */ if (WM_pfPollPID) { WM_pfPollPID(); } if (WM_pfHandlePID) { if (WM_pfHandlePID()) return 1; /* We have done something ... */ } if (WM_IsActive) { if (GUI_PollKeyMsg()) { return 1; /* We have done something ... */ } } #ifdef WIN32 if (WM_PollSimMsg()) { return 1; /* We have done something ... */ } #endif /* * 此部分代码是重绘的代码,每一次只能重绘一个窗口,由于此函数被上一层的while循环调用 * 所以会一直重绘,直到没有再需要重绘的窗口,也就是WM__NumInvalidWindows=0。 * 通过开辟在动态内存中的窗口管理链表,不断地搜寻每一个无效窗口进行重绘,直到没有需要重绘的窗口, * 可以说WM__NumInvalidWindows控制了循环的次数。 */ if (WM_IsActive && WM__NumInvalidWindows) { WM_LOCK(); _DrawNext(); WM_UNLOCK(); return 1; //如果重绘还没有结束,返回1,证明还有需要干的事,就不退出外边的循环 } return 0; //如果所有的操作都完成了,返回0,就退出外边的循环 }
返回1,标志着目前还有未处理的窗口。返回0,标志着所有的窗口都已经处理完毕。
6、_DrawNext
static void _DrawNext(void) { int UpdateRem = 1; /* 如果下一个重绘的窗口为空,那么就将iwin设置为第一个窗口的句柄 */ WM_HWIN iWin = (NextDrawWin == WM_HWIN_NULL) ? WM__FirstWin : NextDrawWin; GUI_CONTEXT ContextOld; GUI_SaveContext(&ContextOld); //重绘一个窗口的时候,会修改GUI_Context,我们希望在重绘结束的时候 //恢复到原来的样子,所以要保存这个全局变量 /* Make sure the next window to redraw is valid */ //这个for循环的目的就是确定一定要重绘一个窗口,如果iWin刚开始为有效窗口,那么就循环找到 //第一个无效窗口,并进行重绘。它并不会把多个无效窗口进行重绘。 for (; iWin && UpdateRem; ) { //确保iwin是有效的 WM_Obj* pWin = WM_H2P(iWin); //获得当前需要操作的窗口在动态内存中的管理节点地址 if (WM__Paint(iWin, pWin)) { //重绘(注意只有是无效的窗口才会进行重绘,而有效的窗口是不会重绘的) UpdateRem--; /* Only the given number of windows at a time ... */ } iWin = pWin->hNextLin; //iwin指向下一个窗体的句柄 } NextDrawWin = iWin; //下一个需要重绘的窗口�WM_Exec()-->WM_Exec1()-->_DrawNext //每次只能重绘一个窗口,所以有多个需要重绘的窗口,我们希望它能继续接着上一次重绘窗口的后边 //继续重绘,而非从头再来 GUI_RestoreContext(&ContextOld); //重绘结束的时候,恢复GUI_Context }
_DrawNext会遍历整个窗口链表,从最底层的桌面窗口开始到最顶层的窗口,根据需要(是否设置为无效、是否设置为可视等)进行重绘操作。
7、WM__Paint()
返回值,1代表有窗口被重绘了,0代表没有窗口被重绘。
/********************************************************************* * * WM__Paint Returns: 1: a window has been redrawn 有窗口被重绘了 0: No window has been drawn 没有窗口被重绘了 */ int WM__Paint(WM_HWIN hWin, WM_Obj* pWin) { int Ret = 0; if (pWin->Status & WM_SF_INVALID) { //首先判断窗口是无效的,才会进行重绘 if (pWin->cb) { //如果窗口有回调函数会调用它的回调函数,如果没有则不处理 if (WM__ClipAtParentBorders(&pWin->InvalidRect, hWin)) { WM_PAINTINFO Info; Info.hWin = hWin; WM_SelectWindow(hWin); //因为要对窗口进行重绘,所以要先选择窗口作为活动窗口 #if GUI_SUPPORT_MEMDEV Info.pWin = NULL; /* 'Invalidate' the window pointer, because it can become invalid through the creation of a memory device */ if (pWin->Status & WM_SF_MEMDEV) { int Flags; GUI_RECT r = pWin->InvalidRect; Flags = (pWin->Status & WM_SF_HASTRANS) ? GUI_MEMDEV_HASTRANS : GUI_MEMDEV_NOTRANS; /* * Currently we treat a desktop window as transparent, because per default it does not repaint itself. */ if (pWin->hParent == 0) { Flags = GUI_MEMDEV_HASTRANS; } GUI_MEMDEV_Draw(&r, _cbPaintMemDev, &Info, 0, Flags); } else #endif { Info.pWin = pWin; WM__PaintWinAndOverlays(&Info); //对窗口进行重绘 Ret = 1; /* Something has been done */ } } } pWin->Status &= ~WM_SF_INVALID; //窗口已经重绘,就将其无效标志清除 if (pWin->Status & WM_CF_MEMDEV_ON_REDRAW) { pWin->Status |= WM_CF_MEMDEV; } WM__NumInvalidWindows--; //窗口已经重绘,就将无效的窗口数减1 } return Ret; /* Nothing done */ }
8、WM_PaintWinAndOvlays
/********************************************************************* * * WM__PaintWinAndOverlays * * Purpose * Paint the given window and all overlaying windows * (transparent children and transparent top siblings) */ void WM__PaintWinAndOverlays(WM_PAINTINFO* pInfo) { WM_HWIN hWin; WM_Obj* pWin; hWin = pInfo->hWin; pWin = pInfo->pWin; if (!pWin) { pWin = WM_H2P(hWin); } #if WM_SUPPORT_TRANSPARENCY if ((pWin->Status & (WM_SF_HASTRANS | WM_SF_CONST_OUTLINE)) != WM_SF_HASTRANS) { #endif _Paint1(hWin, pWin); /* Draw the window itself */ //重绘窗口自己 #if GUI_SUPPORT_MEMDEV /* Within the paint event the application is alowed to deal with memory devices. So the pointer(s) could be invalid after the last function call and needs to be restored. */ pWin = WM_H2P(hWin); #endif #if WM_SUPPORT_TRANSPARENCY } if (WM__TransWindowCnt != 0) { _PaintTransChildren(hWin, pWin); /* Draw all transparent children */ #if GUI_SUPPORT_MEMDEV /* Within the paint event the application is alowed to deal with memory devices. So the pointer(s) could be invalid after the last function call and needs to be restored. */ pWin = WM_H2P(hWin); #endif _PaintTransTopSiblings(hWin, pWin); /* Draw all transparent top level siblings */ } #endif }
9、__Paint1()
static void _Paint1(WM_HWIN hWin, WM_Obj* pWin) { int Status = pWin->Status; /* Send WM_PAINT if window is visible and a callback is defined */ if ((pWin->cb != NULL) && (Status & WM_SF_ISVIS)) {//如果是可视窗口而且窗口的回调函数定义了,才会重绘 WM_MESSAGE Msg; WM__PaintCallbackCnt++; if (Status & WM_SF_LATE_CLIP) { Msg.hWin = hWin; Msg.MsgId = WM_PAINT; //发送重绘消息 Msg.Data.p = (GUI_RECT*)&pWin->InvalidRect; WM_SetDefault(); WM__SendMessage(hWin, &Msg); } else { /* 这是一个循环,不断的给需要重绘的窗口发送消息进行重绘 */ WM_ITERATE_START(&pWin->InvalidRect) { //需要注意的是:外来发送重绘的消息只有一个 //这里将消息分成几次发送,每次发送只重绘一部分,当发送完成的时候 //完整的重绘效果才会展现 Msg.hWin = hWin; //接收消息的窗口 Msg.MsgId = WM_PAINT; //重绘消息 Msg.Data.p = (GUI_RECT*)&pWin->InvalidRect; WM_SetDefault(); WM__SendMessage(hWin, &Msg); //发送消息,启动回调函数 } WM_ITERATE_END(); } WM__PaintCallbackCnt--; } }
10、WM__SendMessage()
void WM__SendMessage(WM_HWIN hWin, WM_MESSAGE* pMsg) { static int _EntranceCnt; WM_Obj* pWin; if (_EntranceCnt < GUI_MAX_MESSAGE_NESTING) { pWin = WM_HANDLE2PTR(hWin); //获取操作窗口句柄对应的动态内存地址 pMsg->hWin = hWin; //打包消息 if (pWin->cb != NULL) { _EntranceCnt++; (*pWin->cb)(pMsg); //调用回调函数对消息进行处理 _EntranceCnt--; } else { WM_DefaultProc(pMsg); //如果没有回调函数,信息也会默认处理 } } #if GUI_DEBUG_LEVEL >= GUI_DEBUG_LEVEL_CHECK_PARA else { GUI_DEBUG_ERROROUT("Max. message nesting exceeded, Message skipped."); } #endif }
发送消息的时候,会调用窗口的回调函数,从而对消息进行处理。
参考资料:《uCGUI中文手册》