HelloCE
Windows 編程中典型的SDK風格飽受責難的地方就是在窗口過程中總是使用巨大的switch語句。switch語句分析傳給窗口過程的消息,這樣每個消息可以被獨立的處理。這種標準結果的優勢之一是強制把一個類似的結構加到幾乎所有Windows應用程序中,這使一個程序員可以更容易理解另一個人的代碼。劣勢是 整個窗口過程的所有的變量通常會比較雜亂的出現在過程的開頭。
這麼多年來,我爲我的Windows程序探索出一個不同的風格。主要想法是將WinMain和WinProc過程分解成更易理解和更易轉換到其它Windows程序中的可管理單元。WinMain被分解成幾個過程,包括執行應用程序初始化、實例初始化和實例終止。作爲所有Windows程序核心的消息循環也在WinMain裏。
窗口過程被分解爲幾個獨立過程,每個處理一個具體的消息。窗口過程自身是一個代碼框架,只是查找傳入的消息,看是否有過程來處理這個消息。如果有,則調用該過程;如果沒有,則把消息傳遞給默認的窗口過程。
把對消息的處理劃分成獨立的塊,使該結構更容易被理解。並且,由於將一個消息處理代碼段同另一個極大的隔離開來,可以使您更容易把處理特定消息的代碼從一個程序轉換到另一個程序。還是在很多年前,我從Ray Duncan在《PC雜誌》的“編程力量”專欄裏,第一次看到對這種結構的描述。在MS-DOS和OS/2編程領域裏,Ray是傳奇人物之一。雖然爲了適合我的需要,我對這種設計做了一些修改,但這還是應該歸功於Ray。
代碼
HelloCE的源代碼如清單1-4所示:
清單1-4 HelloCE程序
HelloCE.h
//======================================================================
// Header file
//
// Written for the book Programming Windows CE
// Copyright (C) 2003 Douglas Boling
//================================================================
// Returns number of elements
#define dim(x) (sizeof(x) / sizeof(x[0]))
//----------------------------------------------------------------------
// Generic defines and data types
//
struct decodeUINT { // Structure associates
UINT Code; // messages
// with a function.
LRESULT (*Fxn)(HWND, UINT, WPARAM, LPARAM);
};
struct decodeCMD { // Structure associates
UINT Code; // menu IDs with a
LRESULT (*Fxn)(HWND, WORD, HWND, WORD); // function
};
//----------------------------------------------------------------------
// Function prototypes
//
HWND InitInstance (HINSTANCE, LPWSTR, int);
int TermInstance (HINSTANCE, int);
// Window procedures
LRESULT CALLBACK MainWndProc (HWND, UINT, WPARAM, LPARAM);
// Message handlers
LRESULT DoPaintMain (HWND, UINT, WPARAM, LPARAM);
LRESULT DoDestroyMain (HWND, UINT, WPARAM, LPARAM);
//======================================================================
// HelloCE - A simple application for Windows CE
//
// Written for the book Programming Windows CE
// Copyright (C) 2003 Douglas Boling
//======================================================================
#include <windows.h> // For all that Windows stuff
#include "helloce.h" // Program-specific stuff
//----------------------------------------------------------------------
// Global data
//
const TCHAR szAppName[] = TEXT("HelloCE");
HINSTANCE hInst; // Program instance handle
// Message dispatch table for MainWindowProc
const struct decodeUINT MainMessages[] = {
WM_PAINT, DoPaintMain,
WM_DESTROY, DoDestroyMain,
};
//======================================================================
// Program entry point
//
int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance,
LPWSTR lpCmdLine, int nCmdShow) {
MSG msg;
int rc = 0;
HWND hwndMain;
// Initialize this instance.
hwndMain = InitInstance (hInstance, lpCmdLine, nCmdShow);
if (hwndMain == 0) return 0x10;
// Application message loop
while (GetMessage (&msg, NULL, 0, 0)) {
TranslateMessage (&msg);
DispatchMessage (&msg);
}
// Instance cleanup
return TermInstance (hInstance, msg.wParam);
}
//----------------------------------------------------------------------
// InitInstance - Instance initialization
//
HWND InitInstance (HINSTANCE hInstance, LPWSTR lpCmdLine, int nCmdShow) {
WNDCLASS wc;
HWND hWnd;
// Save program instance handle in global variable.
hInst = hInstance;
#if defined(WIN32_PLATFORM_PSPC)
// If Pocket PC, only allow one instance of the application
hWnd = FindWindow (szAppName, NULL);
if (hWnd) {
SetForegroundWindow ((HWND)(((DWORD)hWnd) | 0x01));
return 0;
}
#endif
// Register application main window class.
wc.style = 0; // Window style
wc.lpfnWndProc = MainWndProc; // Callback function
wc.cbClsExtra = 0; // Extra class data
wc.cbWndExtra = 0; // Extra window data
wc.hInstance = hInstance; // Owner handle
wc.hIcon = NULL, // Application icon
wc.hCursor = LoadCursor (NULL, IDC_ARROW);// Default cursor
wc.hbrBackground = (HBRUSH) GetStockObject (WHITE_BRUSH);
wc.lpszMenuName = NULL; // Menu name
wc.lpszClassName = szAppName; // Window class name
if (RegisterClass (&wc) == 0) return 0;
// Create main window.
hWnd = CreateWindow (szAppName, // Window class
TEXT("HelloCE"), // Window title
// Style flags
WS_VISIBLE | WS_CAPTION | WS_SYSMENU,
CW_USEDEFAULT, // x position
CW_USEDEFAULT, // y position
CW_USEDEFAULT, // Initial width
CW_USEDEFAULT, // Initial height
NULL, // Parent
NULL, // Menu, must be null
hInstance, // Application instance
NULL); // Pointer to create
// parameters
if (!IsWindow (hWnd)) return 0; // Fail code if not created.
// Standard show and update calls
ShowWindow (hWnd, nCmdShow);
UpdateWindow (hWnd);
return hWnd;
}
//----------------------------------------------------------------------
// TermInstance - Program cleanup
//
int TermInstance (HINSTANCE hInstance, int nDefRC) {
return nDefRC;
}
//======================================================================
// Message handling procedures for main window
//
//----------------------------------------------------------------------
// MainWndProc - Callback function for application window
//
LRESULT CALLBACK MainWndProc (HWND hWnd, UINT wMsg, WPARAM wParam,
LPARAM lParam) {
INT i;
//
// Search message list to see if we need to handle this
// message. If in list, call procedure.
//
for (i = 0; i < dim(MainMessages); i++) {
if (wMsg == MainMessages[i].Code)
return (*MainMessages[i].Fxn)(hWnd, wMsg, wParam, lParam);
}
return DefWindowProc (hWnd, wMsg, wParam, lParam);
}
//----------------------------------------------------------------------
// DoPaintMain - Process WM_PAINT message for window.
//
LRESULT DoPaintMain (HWND hWnd, UINT wMsg, WPARAM wParam,
LPARAM lParam) {
PAINTSTRUCT ps;
RECT rect;
HDC hdc;
// Get the size of the client rectangle
GetClientRect (hWnd, &rect);
hdc = BeginPaint (hWnd, &ps);
DrawText (hdc, TEXT ("Hello Windows CE!"), -1, &rect,
DT_CENTER | DT_VCENTER | DT_SINGLELINE);
EndPaint (hWnd, &ps);
return 0;
}
//----------------------------------------------------------------------
// DoDestroyMain - Process WM_DESTROY message for window.
//
LRESULT DoDestroyMain (HWND hWnd, UINT wMsg, WPARAM wParam,
LPARAM lParam) {
PostQuitMessage (0);
return 0;
}
如果您瀏覽HelloCE的源代碼,會從中看到本書中所有程序都使用的標準模板。在包含的頭文件和宏定義之後是一些全局變量。我知道有很多好的建議勸說全局變量不應該出現在程序裏,但我使用他們是爲了便於簡化本書中的例子,並使它們更條理清晰。每個程序都定義了一個Unicode字符串szAppName,它用在程序的很多地方。hInst 也同樣用在很多地方,並且在詳述InitInstance 過程時,我會提到它。最後那個全局結構是一個消息列表,包含處理相關消息的過程。窗口過程使用這個結構將消息及處理消息的過程關聯到一起。
HelloCE中,WinMain有兩各基本功能:調用InitInstance(放有應用程序初始化的代碼),在消息循環中處理消息,當消息循環退出時調用TerminateInstance。在這個程序模版裏,WinMain成爲一個基本不用修改的例程。總的來說,WinMain唯一的改變之處是對消息循環處理過程的修改,涉及對鍵盤加速鍵的處理以及對無模式對話框消息及其他任務的監控。
實例初始化
InitInstance 的主要任務是註冊主窗口的窗口類,創建應用程序主窗口,按傳給WinMain的nCmdShow 參數指定的格式顯示主窗口。如果是爲Pocket PC編譯,則還有一些條件編譯代碼,用來防止同一時刻運行一個程序的多個實例。
InitInstance做的第一個工作就是把程序實例句柄hInstance 保存在全局變量hInst中。程序的實例句柄在Windows應用程序的很多地方是很有用的。我之所以在這裏保存這個值是因爲此時已經知道實例句柄並且這是程序中保存實例句柄的一個很方便的地方。
當在Pocket PC上運行時,HelloCE用FindWindow來查看是否有自身的副本正在運行。該函數在頂層窗口中搜索,看是否有類名或窗口標題匹配或者兩者都匹配的窗口。如果找到匹配的,用SetFroegroundWindow函數將窗口放到最前面,之後該例程退出,返回碼爲0,這將使WinMain退出,終止應用程序。我將花更多的時間在17章裏討Pocket PC相關的代碼。
Pocket PC相關的代碼包含在#if and #endif行裏。這些行告訴編譯器只有當#if語句的條件爲真的時候才包含它們,在本例中,如果定義了常量WIN32_PLATFORM_PSPC,則條件爲真。該常量定義在[項目設置]中。快速瀏覽一下[項目設置]中的 C/C++ 頁,可以看到完整的關於預處理器定義的區域。在這個區域裏,定義之一是$(CePlatform-Ce平臺),$註冊值佔位符。在Key [HKEY_LOCAL_MACHINE]/Software/Microsoft/Windows CE Tools/Platform Manager 裏,你可以找到一系列註冊key,每個代表一個在eVC++中安裝的目標平臺。CePlatform(Ce平臺)值取決於目標項目的不同。對Pocket PC 和老式Palm-size PC 項目,CePlatform定義爲WIN32_PLATFORM_PSPC。
窗口類的註冊和主窗口的創建過程與Hello3中的過程很相似。唯一的不同是使用了用做主窗口類類名的全局字符串szAppName。每次我用這個模板,我將szAppName 改成程序的名字即可。這可以使不同的應用程序窗口類名保持唯一,方便FindWindow工作。
到此InitInstance 算是完成了,應用程序主窗口已經創建和更新了,因爲在進入消息循環之前,已經有消息發送給窗口的窗口過程了。現在是時候看看這部分程序了。
窗口過程
當你寫Windows程序的時候,編程的大部分時間是花在窗口過程上的。窗口過程是程序的核心,也是窗口行爲創建程序個性的地方。
同大部分不使用MFC等類庫的Windows程序相比,我的編程風格明顯不同的地方正是在窗口過程裏。對於我的幾乎所有程序,窗口過程都和前面HelloCE裏的一樣。在進一步深入之前,我重申:這種程序結構並不特別用在Windows CE中。我的所有Windows應用程序都使用這種風格,不管是Windows 3.1、Me、XP,還是CE。
這種風格將窗口構成簡化成一個簡單的表查找函數。主要思想是在早先定義在C文件裏的MainMessages表裏查找消息值。如果消息被找到,關聯的過程就被調用,並把原始參數傳遞給處理該消息的過程。如果沒有找到,調用默認過程DefWindowProc。DefWindowProc是Windows函數,爲系統裏所有消息提供默認行爲,這使得Windows程序不用處理傳遞給窗口的每個消息。
消息表將消息值和處理消息的過程關聯在一起。該表如下所示:
// Message dispatch table for MainWindowProc
const struct decodeUINT MainMessages[] = {
WM_PAINT, DoPaintMain,
WM_DESTROY, DoDestroyMain,
};
該表定義爲常量,這不僅僅是好的編程習慣,而且因爲這樣可以幫助節約內存。因爲Windows CE程序可以在ROM裏執行,不變的數據應該標記爲常量。這樣允許Windows CE程序裝載器將常量數據放在ROM裏,而不是裝載一個副本到RAM裏,因此節約了寶貴的RAM。
表自身是一個簡單的結構數組,該結構包含2個原素。第一項是消息值,第二項是指向處理消息的函數的指針。因爲函數可以起任何名字,所以在整本書裏我使用了一致的結構,好幫助您理解它們。名字由Do前綴(符合面向對象的經驗),後跟一個消息名及一個用來指明與表相關的窗口類的後綴。例如DoPaintMain是爲程序的主窗口處理Wm_PAINT消息的函數名。
DoPaintMain 和DoDestroyMain
HelloCE裏兩個消息處理例程是PaintMain 和DoDestroyMain。它們和Hello3裏case子句的功能很類似。獨立例程的好處是代碼和本地局部變量是例程隔離的。而在Hello3的窗口過程裏,針對繪畫代碼的局部變量則都集中在窗口過程的頂部。代碼的封裝使您很容易複製代碼到您的下一個應用程序裏。
運行HelloCE
當在eVC++中輸入和編譯本程序後,可以在VC++裏選擇 [Build]->[Execute] HelloCE.exe或者按Ctrl+F5遠程執行本程序。程序在空白窗口的中間顯示一行"Hello Windows CE"字樣,圖1-3和圖1-4顯示了HelloCE運行在PocketPC上的樣子。點一下標題欄上的[Close]按鈕會讓Windwos CE給窗口發送WM_CLOSE消息。雖然HelloCE沒有明確的處理WM_CLOSE消息,但DefWindowProc過程使用銷燬主窗口作爲默認的處理方式。因爲窗口正在銷燬,會發出WM_DESTORY消息,這又會導致對PostQuitMessage的調用。
圖1-3(略)
運行在嵌入式Windows CE系統上的HelloCE窗口。
正象我說過的,HelloCE是一個非常基本的Windows CE程序,但它給您展示了一個應用程序基本框架,您可以在上面添加更多內容。如果您用瀏覽器看HelloCE.exe文件,會發現該程序使用的是通用圖標。當HelloCE運行的時候,任務條上HelloCE對應的按鈕裏,文字旁邊也沒有圖標。關於給程序增加定製圖標以及DrawText函數是如何工作的,我將在後面幾章作爲兩個主題來講解。
圖1-4(略)
運行在Pocket PC上的HelloCE窗口
圖1-4顯示了HelloCE在Pocket PC上運行時的問題。HelloCE窗口伸展到了屏幕底部。和程序間具體切換方式有關,SIP(輸入法軟鍵盤)按鈕可能顯示在HelloCE窗口的上面。爲Pocket PC設計的應用程序會在屏幕底部創建一個菜單條,用來顯示軟鍵盤等事物的按鈕會包含在上面。
必須人工調整窗口尺寸,避免覆蓋菜單條或者被菜單條覆蓋。稍後我們會討論如何爲Pocket PC的用戶界面設計應用程序。可以確信的是:書中頭幾章關於Windows CE的內容一樣適用於Pocket PC設備及其它Windows CE系統。