7 使用者界面与图形子系统.docx
《7 使用者界面与图形子系统.docx》由会员分享,可在线阅读,更多相关《7 使用者界面与图形子系统.docx(36页珍藏版)》请在冰豆网上搜索。
![7 使用者界面与图形子系统.docx](https://file1.bdocx.com/fileroot1/2023-2/28/2d9977dd-0d32-4809-88ed-e58041786ea6/2d9977dd-0d32-4809-88ed-e58041786ea61.gif)
7使用者界面与图形子系统
第七章使用者介面與圖形子系統
與MicrosoftXP作業系統不同,WindowsCE將Win32API的使用者介面(User32)和圖形設備介面(GDI32)合併成一個新的模組gwes.exe,稱為GWE子系統。
GWE是一個縮寫詞,其中G代表Graphics(圖形),W代表WindowManager(視窗管理器),E代表EventManager(事件管理器)。
GWE子系統是使用者、應用程式和作業系統之間的圖形使用者介面。
GWE支援組成WindowsCE圖形使用者介面的所有視窗、對話方塊、控制項、功能表和資源,使使用者能夠通過執行功能表命令、單擊按鈕等操作來控制應用程式。
GWE還以點陣圖、游標、文字以及圖示等形式為使用者提供資訊。
即使不具備圖形使用者介面的基於WindowsCE的平臺也使用了GWE的基本視窗和訊息功能,這些功能提供了在使用者、應用程式和作業系統之間進行通訊的方法。
本章主要分析GWE子系統的體系結構以及相關的實作程式碼,主要涉及的根源程式位於WindowsCE.NET原始程式碼樹中的[CEROOT]\Private\Winceos\Coreos\GWE目錄下。
需要說明的是,受MicrosoftSharedSourceLicense的限制,GWE子系統中只有GDI部分公開了少量的原始程式碼,User部分的原始程式碼均未公佈,因此本章內容主要著重於GWE子系統體系結構的分析。
7.1GWE概述
對應於桌面Windows作業系統中的User32,WindowsCEGWE子系統中的USER部分包含了使用者輸入系統(UserInputSystem)、事件管理器(EventManager)和視窗管理器(WindowManager)三個組件。
其中使用者輸入系統接收來自鍵盤、滑鼠和手寫筆等設備的訊息,事件管理器管理訊息和訊息佇列,而視窗管理器將訊息回應發送到對應的視窗以實作特定的顯示。
GWE子系統中的GDI(GraphicsDeviceInterface,圖形設備介面)部分依靠二維圖形包中的API函式將使用者的繪圖操作通過直線、曲線、填充區域、點陣圖和文字等GDI基本操作來實作,此外GDI還支援點陣字體和TrueType字體。
基於WindowsCE的程式設計要通過訊息迴圈。
訊息迴圈是在Windows應用程式中的一種迴圈,它負責接收系統傳送過來的訊息,並且把它們發送到相對應的視窗,直到系統表明所有的訊息都發送完畢,訊息迴圈才結束。
它包含在WinMain函式中。
處理訊息迴圈的函式是WndProc。
下面是GWE的一些特殊功能:
GWE即時追蹤執行系統的工作情況,所以如果沒有在三分鐘內給設備一些指令的話,GWE將關閉這個設備。
GWE還添加了儲存空間不足時的提示和解決方案。
雖然在技術實作上儲存空間不夠的提示和解決沒有必要一定成為GWE的一部分,但是在這裏加上一些程式碼實作這種功能是最方便的。
因為GWE和輸入相關。
如果儲存空間不足,將出現記憶體不足的對話方塊。
同時會彈出一個視窗,給出正在執行的程式碼,讓使用者選擇關掉其中的一個或幾個。
WindowsCE添加儲存空間不足時的解決程式是基於執行設備的考慮。
因為執行WindowsCE的設備一般沒有硬碟,同時也沒有足夠的記憶體,一旦在執行時超過了記憶體的容量,將沒有硬碟保存,所以必須在有可能超過儲存容量的地方結束一些程式以節省空間。
當然這在執行桌面Windows作業系統的設備上不常見到。
在使用者輸入系統的設計上,WindowsCE也儘量減少執行緒數。
它將鍵盤設備和觸控設備的處理直接交給了GWE,並且將這些設備所傳送的訊息直接放在全域(Global)設備佇列中。
GWE在設計之初有以下的一些目標:
•在小螢幕設備上執行比較穩定:
GWE設計的時候提供了足夠的函式以便使用者寫出的程式碼在小螢幕上能夠執行的很好。
•和Win32的相容性:
和Win32API的相容性是GWE設計時最重要的目標,因為這可以讓那些會Windows程式設計的人能夠在WindowsCE上程式設計。
•支援廣泛的顏色位元深度和色彩模式,如1、2、4、8、16、24和32位顏色。
這些在桌面Windows作業系統中是沒有提供的。
•加上了調節低電壓和對電源的管理的功能設計:
這使得系統在判斷使用者沒有使用設備後關掉設備。
在上述目標之中,WindowsCE最基本的設計目標是和Win32API相容,它的大部分程式碼也沒有重新寫。
和桌面Windows作業系統一樣,WindowsCE的視窗管理器也包括對話方塊管理器、Splash類別和控制項等。
具體的結構如圖7.1所示。
WindowsCE中的非使用者輸入區和桌面Windows作業系統中的有一些區別。
它和視窗管理器結合在一起。
在WindowsCE中,一個獨立的功能表列和工具列將佔據太多的空間,所以WindowsCE將功能表列和工具列結合成一個新的控制項,稱為命令列。
以前的功能表被放在非使用者區而工具列放在使用者區,現在功能表和別的控制項一樣被放在命令列中,並且由命令列實作功能表控制項的功能。
對話方塊管理器位於視窗管理器的上層。
當實作一個對話方塊管理器時,同時會有一個訊息方塊產生。
訊息方塊位於對話方塊管理器的上層,並且所有的控制項(比如編輯框、列表框、下拉式列示方塊等)都位於對話方塊管理器的上層。
功能表與以前有一些不同,現在也作為控制項處理。
GWE還包括記憶體不足(OOM,outofmemory)對話方塊和記憶體不足控制碼。
記憶體不足對話方塊在使用者記憶體空間不夠時彈出,並將正在執行的程式列出來,讓使用者選擇關閉其中的一個或幾個。
同樣,記憶體不足控制碼也是由於儲存空間比較小才被加到WindowsCE系統中去的。
圖7.1WindowsCE圖形子系統的結構
7.2使用者輸入系統
GWE的USER部分包括訊息佇列、事件管理器和使用者輸入系統三個核心元件。
其中最重要的是使用者輸入系統,它負責接收從鍵盤、滑鼠以及觸控板發出的訊息。
使用者輸入系統將使用者從鍵盤、滑鼠等設備輸入的訊息通過訊息傳送機制送到相對應的視窗中。
USER利用產生視窗和訊息傳送這兩部分功能來實作使用者輸入系統。
使用者輸入系統的結構如圖7.2所示,主要組件包括:
•Msgque:
訊息佇列,這是任何需要訊息傳遞的地方所必須的部分,因為要實作使用者輸入就必須能夠把輸入的資訊傳給所需要的視窗。
•Wmbase:
這部分元件的作用是建立視窗,為視窗提供視窗處理函式WndProc,並且給它發送訊息。
•Winmgr:
視窗管理器,它負責把繪圖操作的結果在螢幕上顯示出來。
下面分兩部分對以上內容進行詳細闡述,其中將視窗的產生和管理合併成輸入系統。
圖7.2GWE的USER部分的主要結構
7.2.1訊息佇列
訊息佇列有兩個功能:
它不僅負責接收訊息並將訊息發送到相對應的視窗,而且它還負責保存輸入狀態資訊,比如游標的大小、提示符閃爍率等。
在訊息傳送時,有兩個最基本的函式:
SendMessage和PostMessage。
其中SendMessage函式採用的是同步訊息傳送機制:
發送者發出訊息,接收者接收訊息,而發送者則等待訊息被處理完成。
訊息佇列和執行緒存在一一對應的關係。
通過深入瞭解API函式可以發現,當把訊息傳送到對應的視窗時,每一個視窗對應一個執行緒,SendMessage函式先將訊息發送到相對應的視窗,在後臺可以發現和這個視窗聯繫的執行緒在同步的回應。
如果呼叫SendMessage函式的執行緒和視窗所在的執行緒是同一個執行緒,這次呼叫就會退化為呼叫WndProc函式的一個子程式(因為WndProc是這個視窗的預設處理函式)。
函式PostMessage的工作和SendMessage有所不同。
PostMessage採用的是非同步訊息傳送機制:
它僅僅將訊息封裝以後送進訊息佇列中,訊息的發送者繼續執行,並不管訊息在什麼時候被處理。
一段時間後,訊息從訊息佇列中被取出,送到相對應的地方等待處理。
所以,每一個視窗都和一個與特定執行緒相關的訊息佇列聯繫在一起,視窗成為訊息傳送的目的地。
執行緒、訊息佇列和視窗以及視窗處理函式緊密聯繫在一起,它們之間的關係是一個視窗擁有它自己的執行緒、自己的訊息佇列和相對應的視窗處理函式。
在WinMain()函式中經常看見這樣的訊息迴圈:
while(GetMessage((&msg...)){
TranslateMessage(&msg);
DispatchMessage(&msg);
}
當一個執行緒呼叫GetMessage函式時,相對應執行緒的訊息佇列會發生下述變化。
在訊息處理過程中最簡單的部分是已發送的訊息(將訊息放到別的執行緒的訊息佇列中,但是對本執行緒來講是發送訊息)。
在執行程式碼過程中,訊息佇列中的一部分指標指向即將被回應的訊息佇列,當函式GetMessage被呼叫時,它查看將被回應的訊息佇列。
如果所需要的訊息在佇列中,函式GetMessage將這個訊息送到特定的地方然後返回。
接著主迴圈將呼叫函式DispatchMessage。
函式DispatchMessage首先查看送到的訊息所包含的資訊,找到和這條訊息相關的視窗,以及和視窗相關的處理程式,將訊息和相對應的參數發送給處理程式,讓相對應的處理程式做出回應。
對訊息的處理就是這樣,將訊息從佇列中取出,送到相對應的位置等待,當輪到處理這條訊息時,找到相對應的視窗和視窗處理程式,呼叫處理程式做出訊息回應。
從訊息發送方面看,呼叫函式PostMessage作用就是將訊息封裝以後送到相對應的訊息佇列中去,不管訊息是通過和函式PostMessage同一個執行緒處理還是不同的執行緒處理,結果都一樣,就是訊息被從佇列中取出然後送到了相對應的視窗處理函式等待回應。
同樣,如果訊息處理的執行緒正確執行,將訊息從佇列中取出然後發送出去,從外面看不出有什麼變化,如圖7.3所示。
圖7.3PostMessage的訊息處理流程
函式SendMessage和函式PostMessage相比有一點區別,它和訊息處理是同步的,所以當發現函式SendMessage返回時,這說明訊息已經被處理完了。
訊息有兩種途徑被發送到處理程式,這主要取決於訊息發送到的視窗是在函式SendMessage的執行緒還是別的執行緒。
最簡單的情況是將訊息傳送到和函式SendMessage在同一個執行緒的視窗。
函式SendMessage發現產生視窗的執行緒和自己所在的執行緒是同一個執行緒,就直接呼叫視窗處理程式(而不是將它放到訊息佇列中),然後返回,這裏的訊息處理僅僅是呼叫一個子函式。
SendMessage的處理流程如圖7.4所示。
圖7.4SendMessage的同執行緒訊息處理流程
複雜一點的情況是將訊息傳送到屬於另外一個執行緒的視窗。
此時,訊息佇列有一個子佇列負責處理傳送到特定視窗和執行緒的訊息。
函式SendMessage發現這個訊息將要傳送到另一個執行緒的訊息佇列,就將它封裝然後放到這個執行緒的訊息佇列中去,然後等待。
所以現在就有一個正在呼叫的執行緒等待一個內部Win32事件物件的回應。
這時,執行緒的等待並不影響它接收訊息,執行緒可以接收訊息並且對它做出處理。
這裏的關鍵點是當訊息傳送到屬於別的執行緒的視窗時,實際上是視窗所屬的主執行緒在執行程式碼。
發送訊息的執行緒呼叫函式SendMessage,但是是別的執行緒的WndProc函式在接收訊息。
如圖7.5所示。
說的更詳細一點,當送出訊息的執行緒被掛起(Hook)等待回應時,它還必須即時檢查訊息是否被送回來。
接收到送出執行緒送出訊息的視窗可以立即回發一個訊息,所以當執行緒被掛起等待回應時,它依然可以接收送過來的訊息。
在發送訊息的區別上,除了函式SendMessage發出訊息後必須等待訊息被處理而函式PostMessage不需要外,函式SendMessage還被函式GetMessage所操作。
應用程式的主迴圈從不檢查函式GetMessage的呼叫是否返回。
函式GetMessage執行時僅僅按優先順序處理訊息,並將訊息分別發送到相對應的地方。
表7.1列舉了一些訊息的等級。
圖7.5SendMessage的異執行緒訊息處理流程
表7.1訊息的級別
級別
訊息類型
1
發送訊息—呼叫SendMessage發送的訊息,有最高級的優先順序
2
發送訊息—呼叫PostMessage發送的訊息,有次高級的優先順序
3
WM_QUIT訊息
4
WM_PAINT訊息
5
WM_TIMER訊息
所以,如果一個視窗或者一個執行緒被掛起,最常見的原因是一些執行緒在等待Win32事件物件的回應而不是在自己的訊息迴圈中。
同時如果這個執行緒被掛起,其中發送訊息的程式一樣被掛起等待回應的返回。
那樣將會呼叫MsgWaitForMultipleObjects,它是一個API函式,作用是等待一個Win32事件物件,同時如果有訊息送來將立即處理。
這種方式將是最好的混合模式,因為它既可以等待Win32事件物件,還能處理訊息。
如果不用混合模式,那麼必須讓執行緒掛起等待Win32事件物件或者讓執行緒僅僅處理訊息。
下面是本小節最重要的幾點:
•函式SendMessage和訊息處理是同步的。
•函式PostMessage不等待訊息被處理,和訊息處理是不同步的。
•所有的訊息,從呼叫函式GetMessage進行分配到發送到後臺,會被不同的視窗程式處理。
•當用訊息傳送機制時,執行緒產生一個視窗,並且相對應的視窗處理程式會執行相對應的程式碼來處理訊息。
不會有一個執行緒呼叫函式SendMessage並且這個執行緒還執行另外一個視窗的處理程式。
不必要在視窗處理程式中自己將訊息進行序列化,因為一旦訊息通過SendMessage傳送,它會自動被訊息佇列序列化。
7.2.2輸入管理
輸入管理由一整套子系統來完成,在WindowsCE中,該子系統負責處理前臺視窗、活動視窗和焦點視窗。
每一個執行緒有一個特定的視窗稱為活動視窗。
這是被特定執行緒擁有和啟動的最高等級的視窗。
活動視窗和它的子視窗可以是焦點視窗(具有輸入焦點的視窗)。
焦點視窗能夠接收來自鍵盤的訊息。
系統中一個特定的執行緒或者訊息佇列稱為前臺執行緒,前臺執行緒中的活動視窗是前臺視窗。
這三個視窗是相互關聯的,設定輸入焦點可以改變活動視窗。
同樣,改變活動視窗也可以改變輸入焦點。
設定前臺視窗或者活動視窗可以改變視窗的座標原點位置,同樣,改變視窗的位置可以改變前臺視窗。
所以,三個視窗有類似“石頭、剪、布”之間的關係,它們之間的任何一個改變,其他的都會受到影響,如圖7.6所示。
圖7.6三個視窗相互關聯
活動視窗和焦點視窗的資訊都保存在訊息佇列的結構中,所以它們都有一個基本的執行緒。
當呼叫函式SetActiveWindow時,一個執行緒將把和它同一個執行緒的視窗啟動。
函式SetFocus將改變其所在執行緒中任意視窗的輸入焦點(這個視窗可以是優先順序最高的視窗也可以是一個子視窗)。
當輸入焦點改變或者別的視窗被啟動時,WM_SETFOCUS,WM_KILLFOCUS會被發送。
因為在輸入系統中有一個執行緒負責輸入事件,所以上面所說的工作在系統這個大的框架下面完成,而不是在更低一級的函式中處理。
這個執行緒會通過觀察它的訊息佇列的情況即時追蹤前臺執行緒的執行情況。
它從系統中產生一個執行緒作為前臺執行緒,當使用者按下某個鍵,鍵盤的輸入會被傳送到前臺執行緒的輸入焦點處。
在舊的Windows版本中,可以呼叫SetActiveWindow函式來使不同的視窗啟動。
但是呼叫這個函式並不是像前面所說的那樣工作。
WindowsCE在輸入方面和桌面Windows作業系統一樣,由一個基本執行緒管理。
現在可以呼叫函式SetForegroundWindow來負責改變輸入系統中前臺視窗的資訊,鍵盤輸入也可以順利地發送出去。
使用者可能會疑惑,因為他執行的應用程式可能並不在前臺執行緒中。
其實,他的應用程式已經內部呼叫了SetActiveWindow函式和SetFocus函式,但是使用者並沒有看見應用程式所屬的視窗被啟動。
整個系統好像沒有變化。
但是函式GetActiveWindow會表明上面所說的視窗是活動視窗。
函式GetFocus說明那個應用程式的視窗已經獲得了輸入焦點。
視窗還沒有放到前臺的原因是它所在的執行緒還沒有成為前臺執行緒。
所以,這就是函式SetForegroundWindow(設置前臺視窗)被加上的原因。
它會告訴系統“這就是你所要的接收使用者輸入的前臺視窗”。
這個呼叫會產生一系列的反應:
系統希望的視窗將被置於前臺,它可以從內部將另一個執行緒啟動,將輸入焦點改變從而使合適的視窗接收到鍵盤輸入。
類似地,如果現在的執行緒是前臺執行緒,並且將別的具有最高等級的視窗置於頂層(比如呼叫函式SetWindowPos將它的位置改變),那麼這個視窗將變成前臺視窗。
並且如果你將輸入焦點從一個視窗轉向另一個視窗,活動視窗也隨之改變。
這中間沒有改變的是具有輸入焦點的視窗總是活動視窗或者是活動視窗的子視窗,當然,它也可能為空。
因此總體的結構是這樣的:
在所有儲存結構中,一個處理程序有一個埠。
GWE就是其中一個處理程序。
GWE內部是一個執行緒等待著輸入事件。
鍵盤或者別的觸控式設備將一個事件放入主輸入佇列,主輸入佇列相當於系統輸入佇列,但是它沒有桌面Windows作業系統中系統佇列描述的那麼細緻。
執行緒在那兒得到相對應的事件。
如果得到的事件是一個觸控輸入事件,事件管理器會呼叫視窗管理器來找出是哪一個視窗被點選,接著使用函式PostMessage將訊息封裝,然後放到所屬視窗合適的訊息佇列中去。
如果事件是一個鍵盤輸入事件,事件管理器會將與資訊相關的視窗和訊息佇列交給前臺執行緒處理後返回。
相對應的資訊會表現為函式PostMessage所發送的訊息。
所以,這種發送的機制會同步執行,獲得所有的輸入情況並且將它發送到相對應的執行緒。
當別的執行緒執行時,它們將相對應的訊息從訊息佇列中取出並且處理它。
如圖7.7所示。
圖7.7觸控輸入事件和鍵盤輸入事件的處理
7.3圖形設備介面
在別的圖形包(Package)中,可以使用所有的畫筆、畫刷和字體等來完成每一個繪圖操作。
在Win32GDI(圖形設備介面)中,設備描述表(DeviceContext,DC)描述了圖形的輸出模版。
通過將使用的繪圖工具(畫筆、畫刷等)物件選入設備描述表中來完成對繪圖工具的選擇。
設備描述表是所有繪圖工具的集合。
繪圖操作使用所有被選入設備描述表的工具物件。
當通過呼叫相對應的API函式來使用畫筆和畫刷時,畫筆和畫刷必須轉化成和介面目標一致的形式。
所以,如果你呼叫的API函式和介面與畫筆都有關時,必須明白畫筆和介面要一致。
在WindowsCE中,將畫筆選入設備描述表,同時和畫筆一致的介面物件也被選入了設備描述表中。
畫筆只需要實作一次,但是能畫出一百萬條線和矩形。
對所有的GDI圖形物件來說,實作是很模糊的概念,並且它對不同的物件也代表了不同的操作。
你想用想像中的顏色的畫筆和畫刷,也許這種顏色在系統中並不存在,但是系統會儘量選擇一個最接近的顏色。
從某種意義上說這就是一種實作。
字體的實作是這樣的:
指定一種理想化的字體並且將它選入設備描述表,它和現實中的某種物理字體是匹配的。
調色板也是這樣實作的,通過函式RealizePalette將一個理想中的調色板選入物理設備中。
當將某種式樣的畫刷選入設備描述表中(它可以是一個點陣圖或者更大規格),必須建立一個和所需要的點陣圖格式相匹配的畫刷。
這種概念上的實作和別的可以選擇的圖形物件的實作是一樣的。
一般情況下,當繪圖時,僅僅是選擇相對應的資源並且把它們拷貝到對應的地方。
但是別的邏輯操作會把它們連接起來。
映射模式的操作是選擇邏輯操作的方法。
當用畫筆在介面上繪圖時,可以使用的一種可能的映射模式是將和畫筆規格具有同樣像素的線條複製到指定的區域。
當然,也可以通過一些別的操作組合來實作別的。
資源有16種組合方式,這些組合方式產生的基本區域被記作第二類映射模式。
還有第三類映射模式,它將組合出3個像素寬度的區域作為畫刷的寬度。
7.3.1基本GDI物件
在WindowsCE的圖形設備介面(GDI)中,所有的東西都是一個C++物件。
基礎類別是一個被稱作GDIOBJ的類別,其定義如程式碼7.1所示。
程式碼7.1GDIOBJ類別
//摘自[CEROOT]\Private\Winceos\Coreos\GWE\MGDI\inc\GDIOBJbase.hpp
classGDIOBJ
{
public:
staticHTABLE*m_pHTable;//控制碼表
INT16m_nCount;//引用計數
UINT16m_nIndex;//控制碼表索引
GDIOBJ(void);
~GDIOBJ(void);
ULONGIncrement(void);
ULONGDecrement(void);
voidRemoveFromHandleTable(void);
BOOLIsStockObject(void);
virtualBOOLDeleteObject(void);
virtualintGetObject(intCntBytesBuffer,void*pObject)=0;
virtualDWORDGetObjectType(void)=0;
virtualGDIOBJ*SelectObject(DC*)=0;
};
由GDIOBJ類別的定義可知所有的GDI物件都擁有一個16位的引用計數m_nCount。
但是GDI對象沒有元件物件模型(COM)物件那麼強的引用計數,當COM物件的引用計數為0時,物件可以自我刪除。
對於GDI物件來說,由程式師負責刪除資源:
當引用計數為0時,應該呼叫DeleteObject函式。
基本GDI物件定義了一些抽象函式,規定了實際的GDI物件需要完成的任務,例如刪除物件自身(DeleteObject)、將物件自身選入設備描述表(SelectObject)等。
控制碼表(m_pHTable)是物件控制碼的一個列表,它保存著交給使用者程式處理的控制碼。
m_nIndex是一個16位元的索引,指向控制碼表。
應用程式在失效的控制碼上有很多問題。
比如,一個應用程式呼叫函式CreatePen建立一個畫筆物件,同時得到物件相對應的控制碼(HPEN)。
其後,程式刪除了這個畫筆物件,建立一個畫刷對象。
但是,畫筆控制碼依然存在著。
當應用程式呼叫函式SelectObject將畫刷控制碼選入設備描述表中時,畫筆控制碼才會轉化成一個畫刷控制碼。
有時候程式師以為選擇的是一個畫筆,而實際上選擇的卻是畫刷,應用程式可能將這一切搞得很混亂。
這是Win32API存在的問題,一個多態性的問題。
因此,當選擇一個控制碼時,必須判斷準確這個控制碼是什麼類型的控制碼。
當應用程式退出而沒有釋放所佔用的記憶體空間時,作業系統負責將多餘物件所占的記憶體空間釋放。
比如一個應用程式建立了一個畫筆的控制碼(HPEN)並且在程式結束之前忘記把它刪除。
程式GWEs.exe接收到應用程式終止的訊息以後,從控制碼表裏面可以查明哪些處理程序建立了GDI物件。
從而,系統就可以在控制碼表中尋找已經結束處理程序的物件,然後刪除它們。
所以,儘管應用程式沒有將多餘的空間釋放,但是作業系統可以確保不讓多餘的物件佔據記憶體空間。
當然,正確的做法是讓應用程式要盡力使它在退出時不會留下一些佔據記