一、概述
µ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中文手冊》