1 引言 在任何一個足夠復雜的 GUI 系統中,處理窗口之間的互相剪切是其首要解決的問題。因為多窗口系統首先要確保一個窗口中的繪制輸出不會影響到另外一個窗口。為此,GUI 系統一般要利用 Z 序來管理窗口之間的互相剪切關系。根據窗口在 Z 序中所處的位置,GUI 系統要計算每個窗口受剪切的區域,即剪切域。通常,窗口的剪切域定義為互不相交的矩形集合。GUI 系統的底層圖形引擎在進行輸出時,要根據當前輸出的剪切域進行輸出的剪切操作。從而保證窗口的繪制輸出不會互相影響。因為任何一個窗口的創建、銷毀、隱藏、顯示均有可能影響其他窗口的剪切域,所以首先要有一個高效的剪切域維護算法。本文將詳細描述 MiniGUI 中的剪切域生成算法。 許多人對控件(或者部件)的概念已經相當熟悉了。控件可以理解為主窗口中的子窗口。這些子窗口的行為和主窗口一樣,即能夠接收鍵盤和鼠標等外部輸入,也可以在自己的區域內進行輸出――只是它們的所有活動被限制在主窗口中。MiniGUI 也支持子窗口,並且可以在子窗口中嵌套建立子窗口。我們將 MiniGUI 中的所有子窗口均稱為控件。 在 Windows 或 X Window 中,系統會預先定義一些控件類,當利用某個控件類創建控件之後,所有屬於這個控件類的控件均會具有相同的行為和顯示。利用這些技術,可以確保一致的人機操作界面,而對程序員來講,可以像搭積木一樣地組建圖形用戶界面。MiniGUI 使用了控件類和控件的概念,並且可以方便地對已有控件進行重載,使得其有一些特殊效果。比如,需要建立一個只允許輸入數字的編輯框時,就可以通過重載已有編輯框而實現,而不需要重新編寫一個新的控件類。 在多語種環境中,輸入法是一個必不可少的模塊。輸入法提供了將標准鍵盤輸入翻譯為適當語種的文字的能力。MiniGUI 中也包含有標准的中文簡體輸入法,包括全拼、五筆和智能拼音等等。本文最後將介紹 MiniGUI 中的輸入法模塊實現。
2 窗口 Z 序 Z 序實際定義了窗口之間的層疊順序。說起“Z 序”這個名稱,實際是相對屏幕坐標而言的。一般而言,屏幕上的所有窗口均有一個坐標系,即原點在左上角,X 軸水平向右,Y 軸垂直向下的坐標系。Z 序就是相對於一個假想的 Z 軸而言的,這個 Z 軸從屏幕外指向屏幕內。窗口在這個 Z 軸上的值,就確定了其 Z 序。Z 序值大的窗口,覆蓋了 Z 序值小的窗口。 當然,在程序當中,Z 序一般表示為一個鏈表。越接近於鏈表頭的節點,其 Z 序值就越大。在 MiniGUI 中,我們維護了兩個 Z 序。其中一個 Z 序永遠位於另一個 Z 序之上。這樣,就可以創建始終位於其他窗口之上的窗口,比如輸入法窗口。如果在建立窗口時,指定了 WS_EX_TOPMOST 擴展屬性,就可以創建這樣的主窗口。因為 Z 序的操作實際就是鏈表的操作,這裡就不再贅述。
3 窗口剪切算法 有了窗口 Z 序,我們就可以計算每個窗口的剪切域。我們把因為窗口 Z 序而產生的剪切域稱為“全局剪切域”,這是相對於窗口自身定義的剪切域而言的,我們把後者稱為“局部剪切域”。窗口中的所有輸出,首先要受到全局剪切域的影響,其次受到局部剪切域的影響。我們在這裡重點講解窗口的全局剪切域的生成和維護。 3.1 全局剪切域的生成和維護 在 MiniGUI 中,剪切域表示為若干互不相交的矩形之並集,這些矩形稱為剪切矩形。最初,屏幕上沒有任何窗口時,桌面的剪切域由一個矩形組成,即屏幕矩形;當屏幕上只有一個窗口時,該窗口的剪切域由一個矩形組成,該矩形即為窗口在屏幕上的矩形,而桌面的剪切域卻可能是由多個矩形組成的。圖 1 說明了只有一個窗口時的桌面的剪切域組成。從圖中可以看出,此時桌面的剪切域由四個矩形組成,分別是 A、B、C 和 D。如果窗口在桌面的位置變化為圖 2 所示,則桌面的剪切域將由兩個矩形組成(A和B)。 圖 1 由四個矩形組成的桌面剪切域 圖 2 由兩個矩形組成的桌面剪切域 讀者很容易看出,在只有一個窗口的情況下,形成桌面剪切域的矩形最多只能有四個。 此時,如果有一個新的窗口出現,則新的窗口將同時剪切舊的窗口和桌面(圖 3。窗口的剪切矩形用空心矩形表示,而桌面的剪切矩形用實心矩形表示)。而這時,桌面和舊窗口的剪切域將多出一些矩形,這些矩形應該是原有剪切域中的每個矩形受到新窗口矩形影響之後生成的剪切矩形。同樣,原有剪切域中的每個矩形只能最多只能派生出4個新剪切域,而某些矩形根本不會受到新窗口矩形的影響。 點擊查看大圖 圖 3 有新窗口被創建時,桌面和舊窗口的剪切域 這樣,我們可以將某個窗口全局剪切域歸納為原有剪切域中排除(Exclude)某個矩形而生成的: 窗口的全局剪切域初始化為窗口矩形。 當窗口之上有其他窗口覆蓋時,則該窗口的全局剪切域為排除新窗口矩形之後的剪切域。 沿 Z 序迭代第 2 步,直到最頂層窗口。 清單 1 中的代碼是在顯示一個新窗口時,MiniGUI 處理被該窗口所覆蓋的其他所有窗口的代碼。這段代碼調用了剪切域維護接口中的 SuBTractClipRect 函數計算新的剪切域。 清單 1 顯示新窗口時計算被新窗口覆蓋的窗口的全局剪切域 // clip all windows under this window. static void clip_windows_under_this (ZORDERINFO* zorder, PMAINWIN pWin, RECT* rcWin) { PZORDERNODE pNode; PGCRINFO pGCRInfo; pNode = zorder->pTopMost; while (pNode->hWnd != (HWND)pWin) pNode = pNode->pNext; pNode = pNode->pNext; while (pNode) { if (((PMAINWIN)(pNode->hWnd))->dwStyle & WS_VISIBLE) { pGCRInfo = ((PMAINWIN)(pNode->hWnd))->pGCRInfo; pthread_mutex_lock (&pGCRInfo->lock); SubtractClipRect (&pGCRInfo->crgn, rcWin); pGCRInfo->age ++; pthread_mutex_unlock (&pGCRInfo->lock); } pNode = pNode->pNext; } } 與排除矩形相反的操作是包含(Include)某個矩形到剪切域中。這個操作用於隱藏或者銷毀某個窗口時。當一個窗口被隱藏或銷毀時,該窗口之下的所有窗口將受到影響,此時,要將被隱藏或銷毀窗口的矩形包含到這些受影響窗口的全局剪切域中。為此,MiniGUI 的剪切域維護接口中有一個函數專用於該類操作(IncludeClipRect)。為確保剪切域中矩形互不相交,該函數首先計算與每個剪切矩形的相交矩形,然後將自己添加到該剪切域中。 但是,在某些情況下,我們必須重新計算所有窗口的全局剪切域,比如在移動某個窗口時。 3.2 剪切矩形的私有堆 顯然,在剪切域非常復雜,或者窗口非常多時,需要大量的矩形來表示每個窗口的全局剪切域。而在 C 程序中,如果頻繁使用 malloc 和 free 申請和釋放每個剪切矩形,將帶來許多問題。第一,malloc 和 free 是非常耗時的操作;第二,頻繁的 malloc 和 free 將導致 C 程序堆的碎片化,從而可能導致將來的內存分配失敗。為了避免頻繁使用 malloc 和 free,MiniGUI 在初始化時,建立了一個私有的堆。我們可以直接從這個堆中分配剪切矩形,而不需要從進程的全局堆中分配剪切矩形。這個私有堆實際是由一些空閒待用的剪切矩形組成的。每次分配時返回該鏈表的頭節點,而在釋放時放進該鏈表的尾節點。如果該鏈表為空,則利用 malloc 從進程的全局堆中分配剪切矩形。清單 2 說明了這個私有堆的初始化和操作。 清單 2 從剪切矩形私有堆中分配和釋放剪切矩形 PCLIPRECT GUIAPI ClipRectAlloc(PFREECLIPRECTLIST pList) { PCLIPRECT pRect; #ifndef _LITE_VERSION pthread_mutex_lock (&pList->lock); #endif if (pList->head) { pRect = pList->head; pList->head = pRect->next; } else { if (pList->free < pList->size) { pRect = pList->heap + pList->free; pRect->fromheap = TRUE; pList->free ++; } else { pRect = malloc (sizeof(CLIPRECT)); if (pRect == NULL) fprintf (stderr, "GDI error: alloc clip rect failure!\n"); else pRect->fromheap = FALSE; } } #ifndef _LITE_VERSION pthread_mutex_unlock (&pList->lock); #endif return pRect; } void GUIAPI FreeClipRect(PFREECLIPRECTLIST pList, CLIPRECT* pRect) { #ifndef _LITE_VERSION pthread_mutex_lock (&pList->lock); #endif pRect->next = NULL; if (pList->head) { pList->tail->next = (PCLIPRECT)pRect;