00 Windows API 入门

Post date: 2012/3/28 上午 06:05:09

网上发现一个不错的网站:天蓝工作室http://itzone.hk/,里面有几个不错的技术文章,刚好和我要做的Windows字体有关,他分了10个文章,我感觉没必要,就整理了放在我这里,避免以后找不到,文章不长但是讲的非常透彻。原文,目录的连接如下,也有一些别的文章:

http://itzone.hk/article/index.php?tid=18

(一)前言

自1985年Windows 1.0 推出,到現在的 Windows 2000 及 Windows XP ,微軟增強其核心和穩定性的決心,可謂「路人皆見」。為了不讓 Windows 的支持者失望,其 Windows API (Application Programming Interface) 的複雜性和多樣性亦隨該視窗版本的更新而上升,務求讓各開發者能控制和駕御電腦的每一部分。

當然沒有多少開發者能咬緊牙關與微軟「同步過冬」, Windows API 千變萬化,每當微軟推出新的視窗系統,就會嚇怕傳統的程式開發者,例如 Windows 2000 推出時,微軟發佈了 Windows 2000 新增約 300 條非後向兼容性(non-backward compatible) 的 Network API 後,很多 Windows API 開發者已感疲乏,那堆 Windows ME 的 System API 剛剛掛在耳邊, Windows 2000 API 就勢如破竹攻陷眾位的弱小的心靈,使 API 開發者走上 MFC (Microsoft Foundation Class) 的道路,微軟從此就能依靠 Visual Studio 的利潤來補貼盜版猖厥所帶來的虧蝕。

你要用Windows API的原因

為何還要用 API 呢?本人絕不反對 MFC 的架構,亦不反對別人用 Visual Studio ,只是 API 能給予傳統的 C 程序開發人員一種自信和安全感 ------ API 確保了 Charles Petzold 能寫的高尖程式,你也能寫得到,是一種自信;而且與所有視窗程序源代碼相容,是一種安全感。或許你希望用簡單的程式碼來獲取輸入法狀態、螢幕大小、解像度、視窗顏色、桌面圖畫檔案名,甚至乎想知道你的滑鼠是左手還是右手用的, API 中的 GetSystemMetric 能為你效勞,這就是 Windows API 的存在價值 ------ 方便、易懂、結構簡單,是視窗核心同模樣的影子,只是有些開發人員害怕打多一點英文字,才厭惡那「八十行,一 Windows 」的日子。

Windows API的強大威力

Windows API 真的是萬能嗎?在視窗環境下,答案是肯定的。作為高等 C 語言程式開發,它可代替 MFC 的地位 (同理, MFC 也能取代 API);作為低等程式開發 (低等一詞並不是解作低層次、沒用,而是解作硬件直接操作) ,它提供一套比組合語言更有語意的函數庫,供開發人員直接存取記憶體中的資料。那麼,上至圖形介面,下至電子信號也是 API 的「管轄範圍」, API 還不是萬能嗎?其實也並不然,最起碼微軟沒有提供一套有效直接讀取硬盤或軟盤的 API ,那些硬盤控制狂唯有用組合語言的十五號中斷來讀取硬盤資料吧……。其實 Charles Petzold 在創造 API 時已想到 C 的安全性其實很低,像我這些控制狂會利用 C 的指針胡亂更改數據類型,為求達到目的而不擇手段。其實 C 本身就是幫兇,所以 Charles Petzold 並沒純粹打開標準 C 的函數庫照抄一次,而是寫了一堆很精美易懂的指令 (例如: CreateWindow, RegisterClass, TextOut, BeginPaint, etc) 來壓抑各控制狂對記憶體的強烈操縱慾望,同時其圖型程式庫亦減少了開發人員為繪畫圖型介面所花的時間。

Windows API的弊端

但現時壟斷和流行的,卻是 .NET、MFC(VC) 和 VB 。不難想像,要一個滿有憧憬的視窗設計學徒,每寫一個程式就動輒打上過百行程式碼,除非這是功課上的要求,否則我會選擇用 VB 來保護我的指頭。問題的徵結在於 ------ 一般開發人員不太關心視窗的訊息流程和樣式,但 API 卻要你花 80% 時間去編寫一堆只供電腦「欣賞」的程式碼,說起上來, API 的開發者是真真正正的傻子。另外, OOP (Object-Oriented Programming) 的掘起,使得 API 像似過渡性質的程式介面遭人唾棄,亦是主因。

(二):如何建立視窗

若是已習慣了長期文字模式編程,而又想嘗試利用視窗的圖型介面尋求突破,這一章將會是你的嚮導。

由於當年創立Windows API的時候是仿照標準C程式庫的函數去設計及簡化,故Windows API與C語言必能百分之百配合,以下的程式碼將會以C語言表示(註:API在所有的程式語言的名稱和參數是一致的,大家不需憂慮API在各電腦語言的統一性。)

視窗的種類

視窗共分三種,包括 Overlapped Window, Popup Window 和 Child Window ,按其屬性直譯作 API Style 分別為 WS_OVERLAPPED, WS_POPUP 和 WS_CHILD。若按其功能來區分,視窗還有 Control Window, Dialog Box 和 Message Box ,Control Window 屬於 child window ,而 Dialog Box 和 Message Box 通常屬於 popup window。

Handle概念

視窗程式全都是模組化的程式 - 每個視窗元件,例如選單、捲軸、按鈕等,都是一個獨立的個體,每個個體或元件都有自己的資料和函數,由於一個程式有可能會有多個相同種類的元件(例如一個程式裡有很多按鈕),那麼在眾多相同的元件中,要決定控制哪個元件,就需要設定一Handler,來指明控制哪個元件,例如視窗程式的主視窗,就需要一個 Handler,來指明用者現在正控制該程式的視窗,而非其他。故Handler的概念非常重要,各程式員會看見幾乎所有函數都需要輸入Handler (以「H」字開頭的參數)。

視窗程式的進入點 (Entry Point)

要利用 API 建立視窗,首先就是要登錄該程式是屬於哪一種視窗,稱為 Window Registration ,整個登錄過程約有二十句,包括設置視窗外型、圖示、選單、背景顏色等,都依靠 WNDCLASS 或 WNDCLASSEX 來記下以上資料,再存入 RegisterClass 或 RegisterClassEx 來登錄成正式視窗。

每個程式總會有一個 (只此一個) 程式進入點 (Entry Point) ,而 Windows API 程式的進入點為 WinMain。請看看以下宣告:

hInstance 是本程式的 handle ,或者可以稱作 PID (Process ID),hInstance 所存儲的數值是唯一的。 HINSTANCE 是特殊的 windows handler ,負責儲存主程式父視窗 (parent window) 的 handle。

hPrevInstance 在 Win32 環境下必定為 NULL。szCmdLine 儲存了外部參數 (e.g. 在 MS-DOS 文字模式中,緊隨程式名稱的參數)。iCmdShow 決定視窗的顯示形式,可以是以下任何一值 (共有十種,以下列出最常用的四種):

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow)

SW_HIDE : 隱藏視窗在工作列上

SW_SHOW : 顯示視窗 (最常用)

SW_SHOWMAXIMIZED : 把視窗放到最大

SW_SHOWMINIMIZED : 把視窗縮到最小

iCmdShow 的預設值為 SW_SHOW,大部份程式都不需要遮遮掩掩,故我們不需刻意設定 iCmdShow 這值,讓它是 SW_SHOW 就行了。

請留意,變數的命名方法都跟隨 Hungarian Notation ,在每個變數名稱前都需加上該變數的屬性代表字母:

c = char

by = byte (unsigned char)

n = short

i = int

x, y = int (coordinates)

cx, cy = int (dimension, c = count)

b, f = BOOL, flags

w = word (unsigned short)

l = long

dw = dword (unsigned long)

fn = function

s = string

sz = \0 terminated string

h = handle

p = pointer

視窗框架的屬性 (Frame Properties)

在 WinMain 裡,我們還要宣告 windows class 結構來儲存 windows 的屬性 (property):

WNDCLASS wc;

//或

WNDCLASSEX wc

(或 WNDCLASSEX wc , 比沒有「EX」多了 cbSize 和 hIconSm)

WNDCLASS (WNDCLASSEX) 是一個龐大的數據結構:

wc.cbSize = sizeof(wc); //記錄 window class 的大小

wc.style = CS_HREDRAW | CS_VREDRAW; //記錄 window 的外觀

wc.lpfnWndProc = WndProc; //記錄程式處理視窗訊息的函數

wc.cbClsExtra = 0; //記錄額外分配給 Class 結構的記憶體大小

wc.cbWndExtra = 0; //記錄額外分配給 Instance構的記憶體大小

wc.hInstance = hInstance; //記錄程式的 handle

wc.hIcon = LoadIcon(NULL, IDI_APPLICATION); //程式的圖示

wc.hCursor = LoadCursor(NULL, IDC_ARROW); //程式的游標

wc.hbrBackground = (HBRUSH) GetStockObject(WHITE_BRUSH); //程式的背景顏色

wc.lpszMenuName = NULL; //記錄程式的選單 ID

wc.lpszClassName = API; //程式的唯一名稱

wc.hIconSm = LoadIcon(NULL, IDI_APPLICATION); //程式的小圖示

wc.style = CS_HREDRAW | CS_VREDRAW

更改大小後Client Area被重繪。

填好視窗的資料後,把 wc 的記憶體位址存入 RegisterClassEx 登記視窗屬性

RegisterClass(&wc);

//或

RegisterClassEx(&wc);

登錄後就要建立視窗的外框,我們使用 CreateWindow 函數,並發放 WM_CREATE 訊息:

hwnd = CreateWindow("API", // 程式的名稱

"Hello World", // 程式的 caption

WS_OVERLAPPEDWINDOW, // 視窗的屬性

CW_USEDEFAULT, // 視窗左上角的 x 座標

CW_USEDEFAULT, // 視窗左上角的 y 座標

CW_USEDEFAULT, // 視窗闊度

CW_USEDEFAULT, // 視窗高度

NULL, // 父視窗的 handle,若本視窗是父視窗,則設為 NULL

NULL, // 選單的 handle

hInstance, // 主程式的 handle

NULL); // 額外資料的指標,若無額外資料通常都設為 NULL

再利用 ShowWindow 顯示視窗

最後呼叫 UpdateWindow 繪畫視窗內容,並發放 WM_PAINT 訊息:

剛才我們提到 CreateWindow 和 UpdateWindow 都會發放訊息,那麼何謂訊息呢?Win32 環境是 event-driven 的環境,每當使用者碰一碰視窗 (e.g. 按下鍵盤、滑鼠、最大化按鈕、最小化按鈕等),就會發放相應的訊息進入訊息迴圈,然後執行相應的函式完成視窗重繪、結束程式等動作。

程式的訊息迴圈 (Message Loop)

程式迴圈通常是一個 while 迴圈:

ShowWindow(hwnd, iCmdShow);

UpdateWindow(hwnd);

while (GetMessage(&msg, NULL, 0, 0))

{

TranslateMessage(&msg);

DispatchMessage(&msg);

};

大部份的程式訊息迴圈都遵循以上結構。很多遊戲程式無時無刻都在重繪視窗 (尤其是使用 DirectX 的遊戲),程式迴圈就要特別處理 WM_PAINT 訊息,配合 PeekMessage 函數提早取得下一個訊息,以提高執行效率。最後程式結束時,要傳回一個整數值:

那些視窗訊息在哪裡被處理掉呢?答案就是我們剛才在宣告 windows class 時,儲存於 wc.lpfnWndProc 的函數裡:

hwnd 是主程式的 handle,iMsg 是將要處理的訊息,wParam 和 lParam 是訊息附帶的參數,在不同的訊息裡,wParam 和 lParam 都有很不同的意義和數值。iMsg 是一個無符號整數值,所以我們可以用 switch 方便地分開不同訊息:

return msg.wParam;

LRESULT CALLBACK WndProc(HWND hwnd, UINT iMsg, WPARAM wParam, LPARAM lParam)

switch (iMsg)

{

case WM_CREATE:

return 0;

case WM_PAINT:

hdc = BeginPaint(hwnd, &ps);

EndPaint(hwnd, &ps);

return 0;

case WM_DESTROY:

PostQuitMessage(0);

return 0;

};

return DefWindowProc(hwnd, iMsg, wParam, lParam);

Windows 的訊息多不勝數,以上三個是必不可少的訊息,每次處理完訊息後都需寫上 return 0,否則便會傳回 DefWindowProc。

其他訊息

順帶介紹一些常用的函數 (以後還會再次介紹): BeginPaint 和 EndPaint 會建立 device context handle (i.e. HDC),用作繪圖,也許你的程式不需繪圖,那你不要處理 WM_PAINT 便好了;假若你堅持要處理 WM_PAINT 但畫任何東西,你也不可直接 return 0 ,必需加上 BeginPaint 和 EndPaint,否則程式不斷發出 WM_PAINT 繼而當機。

PostQuitMessage 是一個快捷結束程式的函數,在此,「0」代表正常退出程式,執行 PostQuitMessage(0) 函數後會釋放所有記憶體。

本章的 Source Code 可按此下載。

相關文件:

- Chap2 Popup Windows

- Chap2 Overlapped Win

- Redraw window

- Windows Style

- Source Code Download

三。图形函数

繪圖對於視窗程式是非常重要的,否則我們想不出甚麼理由要用 API 而非 standard library。由於繪圖函數多不勝數,也沒有一個程式可用盡所有繪圖函數,故此初學者必先了解視窗程式繪圖的理論,了解視窗繪圖的機制和內部流程後,方能順利實踐。

用來繪字的GDI函數

以下我們所講的都是 GDI (Graphics Device Interface) 函數,GDI 函數專責處理程式介面和顯示卡的資料傳送,我們只需建立一個 device context handler (HDC) 就能使用 GDI 函數。例如:

hdc 的資料型態為 HDC,通常用以下兩種方法去建立及初始化:

TextOut(hdc, x, y, psString, iLength);

1)PAINTSTRUCT ps;

hdc = BeginPaint(hwnd, &ps);

2)hdc = GetDC(hwnd);

當中以第一種呼叫方法的效率較佳,繪圖速度也較高,因為 device context 的數目是有限的,故呼叫後必需利用以下函數釋放 DC:

1)EndPaint(hwnd, &ps);

2)ReleaseDC(hdc);

其實 GDI 函數都是一些易明、有語意的指令,初學者應能很快適應。

更新視窗畫面

每當程式接收到 WM_PAINT 指令時,就會把整個視窗或部分視窗重繪,在主程式的 CALLBACK 函數中,你可以在處理 WM_PAINT 訊息時加入以下繪圖指令:

HDC hdc;

PAINTSTRUCT ps;

switch(iMsg)

{

case WM_PAINT:

hdc = BeginPaint(hwnd, &ps);

TextOut(hdc, 100, 100, psString, strlen(psString));

EndPaint(hwnd, &ps);

return 0;

}

這樣每當視窗要重繪時 (e.g. 最大化或最小化時),就會在 (100,100) 上顯示 psString 的內容,在此我們不妨討論一下 TextOut 這個最常用的函數。

當中:

TextOut(hdc, x, y, psString, iLength);

hdc = handle of device context

x = x 座標

y = y 座標

psString = 字串指標

iLength = 字串長度

請留意,psString 中不應有 ASCII 控制字元 (i.e. ASCII code > 128)。

假若我們要在視窗不需重繪的情形下繪圖 (e.g. 顯示你在鍵盤上鍵入的字元),就要多做一個步驟。其實程式接收 WM_PAINT 後,只會重繪 Invalid 的範圍 (i.e. 被覆蓋或破壞的視窗範圍),只有在視窗被移動或被其他視窗覆蓋才會自動發放 WM_PAINT 訊息,繼而重繪視窗,但按鍵卻不能自動把某視窗範圍變成 Invalid,故此我們需人手呼叫 InvalidateRect 函數:

InvalidateRect(hwnd, &rect, TRUE);

hwnd //window's handler

rect //長方形的左上角及右下角的座標

rect.left //左上角的 x 座標

rect.top //左上角的 y 座標

rect.right //右下角的 x 座標

rect.bottom //右下角的 y 座標

請留意,InvalidateRect 的第二個參數是長方形結構的地址而非長方形結構變數本身。呼叫這函數後,視窗就會自動發放 WM_PAINT 訊息去重繪視窗。

其他 GDI 函數包括 DrawText、Ellipse、Pie、Rectangle、RoundRect等繪圖函數,還有繪圖物件函數,包括 LoadBitmap、LoadImage、CreateFont等,稍後會為大家介紹,或讀者可自行參看 API 參考書。

本章的 Source Code (TextOut函數)可按此下載。

本章的 Source Code (Invalidate函數)可按此下載。

相關文件:

- TextOut Sample

- TextOut Source Code

- Invalidate Source Co

四。點陣圖的應用

我們看似忽略了一些很重要的繪圖函數,例如 Ellipse、Rectangle 這些繪畫幾何圖形的函數,因為這些函數大都不管用 ---- 簡單的繪圖只會讓你變成抽象派畫家,也許我們喜歡使用點陣圖吧。本章將會為大家介紹點陣圖 (Bitmap)

如何載入點陣圖

為方便起見,Bitmap 通常在 resources script 裡宣告:

#define IDB_BITMAP1 1

IDB_BITMAP1 BITMAP "some_path/some_bitmap_file.bmp"

使 some_path/some_bitmap_file.bmp 這點陣圖在編譯主程式時嵌入可執行檔裡。圖片已嵌入程式中,可是我們該如何呼叫它呢?請看以下例子:

HBITMAP BitmapHandler;

BitmapHandler = LoadBitmap(hInstance, (LPCTSTR)IDB_BITMAP1);

Bitmap 是一個物件,在視窗程式中,每個物件都需有一個 handler 作為介面,這點要緊記。故使用點陣圖必先取其 handler,LoadBitmap 的第一個參數為主程式的 handler (i.e. 即 instance),第二個參數為 resource ID,在 resources script 中已經定義好。

請留意,載入點陣圖這步驟應該在程式的視窗畫好之前 (i.e. 即呼叫 ShowWindow 之前) 完成,否則就會發生「畫好圖而未有圖」的情況,這是初學者常犯的毛病。

使用GDI函數繪畫點陣圖

呼叫點陣圖有一點複雜,它有一套特別的機制來加快重繪速度。請看以下程式:

HDC hdc, bitdc;

BITMAP bitmap;

PAINTSTRUCT ps;

hdc = BeginPaint(hwnd, &ps);

bitdc = CreateCompatibleDC(hdc);

GetObject(BitmapHandler, sizeof(BITMAP), &bitmap);

SelectObject(bitdc, BitmapHandler);

BitBlt(hdc, x, y, bitmap.bmWidth, bitmap.bmHeight, bitdc, 0, 0, SRCCOPY);

DeleteDC(bitdc);

EndPaint(hwnd, &ps);

共有七行程式碼,BitBlt 把 bitdc 的每一個 bit 複製到 hdc:

BitBlt(hdcDest, xDest, yDest, widthDest, heightDest, hdcSrc, xSrc, ySrc, dwRop);

hdcDest //直接輸出螢幕的 DC

xDest //螢幕的 x 座標

yDest //螢幕的 y 座標

widthDest //點陣圖的闊度

heightDest //點陣圖的高度

hdcSrc //儲存點陣圖的 DC

xSrc //點陣圖左上角 x 的座標

ySrc //點陣圖左上角 y 的座標

dwRop //Raster Operation Code

dwRop 有以下預設值 (只列舉兩個常用值):

SRCCOPY //直接複製 hdcSrc 的內容到 hdcDest

SRCAND //把 hdcSrc 的每個 bit 與 hdcDest 的 bit 進行 AND 運算後才顯示

利用 StretchBlt 可擠壓或延伸點陣圖後才顯示:

BOOL StretchBlt(

HDC hdcDest,

int nXOriginDest,

int nYOriginDest,

int nWidthDest,

int nHeightDest,

HDC hdcSrc,

int nXOriginSrc,

int nYOriginSrc,

int nWidthSrc,

int nHeightSrc,

DWORD dwRop

);

利用以上兩個函數,我們可以抽取整幅點陣圖或其一部份,貼上螢幕的某一位置。

獲取系統對點陣圖的支援等級

因為並非所有電腦有安裝了比 Windows 95 或更新版本的視窗系統,而呼叫以上函數的視窗程式也未必能乎合系統的點陣圖處理能力,故Windows API 提供了 GetDeviceCaps (Get Device capability) 來測試你的 Windows 能否呼叫以上函數:

不同的 nIndex 值使 GetDeviceCaps 返回不同的系統資訊,而 nIndex 的值共有二十多種,而返回值也不下四十種,在此我們省略介紹。

剛才的程式中,使用了 Bitmap 結構,Bitmap 結構儲存了一幅點陣圖的闊度、高度、解析度等,我們可以在載入點陣圖的 Handle 後,利用 GetObject 把點陣圖的資料載入 Bitmap 結構:

Bitmap 的結構如下:

GetDeviceCaps(HDC hdc, int nIndex);

GetObject(BitmapHandler, sizeof(BITMAP), &bitmap);

struct BITMAP

{

LONG bmType; // 點陣圖的類型,必須置零

LONG bmWidth; // 點陣圖的闊度

LONG bmHeight; // 點陣圖的高度

LONG bmWidthBytes; // 點陣圖每行所佔的位元數

WORD bmPlanes; // 點陣圖共用了的色板數 //BMP使用一个调色板就可以表示这幅图的颜色,所以此值恒为1

WORD bmBitsPixel; // 點陣圖儲存一種顏色的位元數

LPVOID mBits; // 點陣圖的位元值

}

本章的 Source Code 可按此下載。

GetDeviceCaps 函數的詳細說明(英文版) 可按此下載。

相關文件:

- DrawBitmap snapshot

- Chap4 Source Code

- GetDeviceCaps detail

(五):更改視窗外觀

很多時候我們都想終途改變視窗程式的外觀,例如背景顏色或圖示等,你可以使用 SetClassLong 函數 (只適用於透過 WNDCLASS 所建立的視窗):

例如你想按鍵盤時改背景顏色為藍色,可在 WndProc 中加入:

SetClassLong(HWND hwnd, int iIndex, LONG dwNewLong)

switch (iMsg)

{

...

...

case WM_KEYDOWN:

SetClassLong(hwnd, GCL_HBRBACKGROUND,

(LONG) CreateSolidBrush(RGB(0, 0, 255)));

InvalidateRect(hwnd, NULL, TRUE);

return 0;

}

當中,把 GCL_HBRBACKGROUND 套入 SetClassLong 的 iIndex 代表更改背景顏色,(LONG) 把 CreateSolidBrush 所建立的 Brush 物件強制轉換為 32 bit 整數。

CreateSolidBrush 把 COLORREF 數值轉換為 Brush 結構,可用作區域填充 (Floodfill),在這個例子我們利用 CreateSolidBrush 所建立的 Brush 結構來填滿整個背景。

RGB(紅,綠,藍) 是一個巨集,把紅綠藍三種顏色的深度填入巨集,傳回 COLORREF 型數值,再傳入 CreateSolidBrush 中。其中 COLORREF 是一個 32 bit 數值,以十六進數表示方法如下:

例如 0x00ff0000 等於純藍色,0x00ffffff 等於白色。

又例如你想更改程式的圖示,你可以利用以下指令:

當中,把 GCL_HICON 套入 iIndex 代表更改現有圖示,hInstance 代表提供圖示的應用程式,即本程式,而獲取本程式的 Instance 可在捕獲 WM_CREATE 時利用以下指令:

MAKEINTRESOURCE 是一個巨集將匯入程式的資源 ID 數值轉變為視窗程式內部的資源識別碼,其中 IDI_ICON1 在 resource script 中已定義好。

簡易應用實例

1. 我們再看看如何更改視窗框架的樣式:

請留意,視窗的外觀更改必需以 UpdateWindow、ShowWindow 或 InvalidateRect 函數來更新。

2. 大家使用 Notepad 或 IE 時,也會發現它們的 Caption 會隨 URL 或檔案名而改變,我們可使用 SetWindowText 來更改 title bar 上面的文字:

注意:SetWindowText 函數的第二個參數是字串的地址。

3. 若你想開發電腦遊戲或者是螢幕保護程式,你的視窗程式必需霸佔整個螢幕,並要把框架和 Title bar 移除,你可以在建立視窗框架時 (i.e. 呼叫 CreateWindow 時),設定樣式一欄為:

0x00bbggrr (b = blue, g = green, r = red)

SetClassLong(hwnd, GCL_HICON, (LONG) LoadIcon(hInstance, MAKEINTRESOURCE(IDI_ICON1)));

hInstance = ((LPCREATESTRUCT)lParam)->hInstance;

SetWindowLong(hwnd, GWL_STYLE, (long)WS_POPUP | WS_VISIBLE | WS_THICKFRAME | WS_CAPTION);

SetWindowText(hwnd, "Your New Caption");

CreateWindow(AppClassName, "Caption", WS_POPUP | WS_VISIBLE, 0, 0,

GetSystemMetrics(SM_CXSCREEN), GetSystemMetrics(SM_CYSCREEN),

NULL, NULL, hInstance, NULL);

把樣式設定為 WS_POPUP|WS_VISIBLE 就能消除框架和 Title bar,我們利用 GetSystemMetrics 取得電腦的螢幕大小,將不同數值插入 GetSystemMetrics 可傳回不同的系統設定值,這些數值約有四十個,在此不作詳細討論。

本章的 Source Code (更改底色)可按此下載。

本章的 Source Code (更改Caption)可按此下載。

本章的 Source Code (更改框架外觀)可按此下載。

本章的 Source Code (更改圖示)可按此下載。

本章的 Source Code (全螢幕)可按此下載。

GetSystemMetrics 函數的詳細說明(英文版) 可按此下載。

相關文件:

- Change Background Sn

- Change Icon snapshot

- Change Caption snaps

- Change background so

- Change Caption sourc

- Change frame source

- Change icon source

- Full screen source

- GetSystemMetrics det

(六):文字選單應用

為何要用選單(Menu)?

哪些視窗程式不需要選單呢?螢幕保護程式、安裝程式 (Installer)……平常所用的視窗程式,連「傷心小棧」也計算在內的話,不需視窗介面所提供的選單功能的程式,是寥寥可數的。選單的主要功能是讓用者快捷檢視程式的各項功能,以及讓用者有效方便地利用選單去執行這些功能。由於選單的應用範圍、種類和呼叫方法層出不窮,故本章篇幅亦略較其他文章大,請耐心細閱。

如何宣告選單

以下的例子利用了resource script宣告選單的方法:

#define IDM_MENU1 1

IDM_MENU1 MENU

{

POPUP "檔案(&F)"

{

MENUITEM "重新開始(&R)", 101

MENUITEM "載入遊戲(O)...", 102

MENUITEM "儲存遊戲(&S)...", 103

MENUITEM "結束遊戲(&E)", 104

}

}

選單的外觀如下所示:

當中,IDM_MENU1 是選單的ID,POPUP關鍵字代表「檔案」是主選單的選項,MENUITEM代表「重新開始」等詞為功能選項,每個功能選項有其各自的代表功能。

我們發現到某些選項的括號裡的英文字,部份加了底線,這代表使用者可透過 ALT+英文字鍵 快速選擇,我們只要在resource script中,在英文字母前加入「&」就能使緊隨的英文字母變成快捷鍵。

另外在傳統上,若按下選單時有別的視窗、對話盒或訊息盒彈出,而非直接執行某功能的話,我們在選單項目的最尾要加上「...」代表視窗控制權會轉移到別的視窗。

如何載入選單

在大多數情況我們都在宣告Window Class時載入選單:

WNDCLASS wc;

wc.lpszMenuName = LoadMenu(hInstance, MAKEINTRESOURCE(IDM_MENU1));

這是最常用的方法,以下是其餘特殊的方法:(假設選單的ID為1)

HMENU hMenu;

1. hMenu = LoadMenu(hInstance, "#1");

//或

2. hMenu = LoadMenu(hInstance, "IDM_MENU1");

SetMenu(hwnd, hMenu):

選單的迴圈訊息

case WM_COMMAND:

switch(LOWORD(wParam))

{

case CM_RESTART:

... // 加入指令

return 0;

case CM_LOAD:

...

return 0;

case CM_SAVE:

...

return 0;

case CM_EXIT:

PostQuitMessage(0);

return 0;

...

};

即每按一次選單的MENUITEM就會發放WM_COMMAND指令,而MENUITEM ID就會儲存於wParam的尾十六個bit中。

CM_RESTART、CM_LOAD等都是在resource script中設定的:

#define CM_RESTART 101

#define CM_LOAD 102

#define CM_SAVE 103

#define CM_EXIT 104

選單與加速鍵

當然,你也希望在選單上設定加速鍵(Accelerator),只要按Ctrl+O就能開啟檔案,我們可在resource script中加入:

IDA_ACCEL1 ACCELERATORS

{

VK_F2, 101, NOINVERT, VIRTKEY

"O", 102, NOINVERT, CONTROL, VIRTKEY

"S", 103, NOINVERT, CONTROL, VIRTKEY

VK_F4, 104, VIRTKEY, NOINVERT, CONTROL

}

當中,VIRTKEY(i.e. Virtual Key)即代表加速鍵的鍵盤掃描碼(keyboard scan code)並不能以ASCII字元顯示,例如Ctrl+A、F2、Alt+C等;CONTROL即代表Ctrl鍵,配合之前的「O」或「S」就成為 Ctrl+O和Ctrl+S了。

請留意,由於VK_F2和VK_F4是windows.h所定義的常數,在VC++中你必需在Resource script中加入#include ,在BC++則不需要。

若你想在選單上顯示各選項的加速鍵,可以TAB隔開選項名稱和加速鍵名稱:

MENUITEM "重新開始(&R)\tF2", 101

MENUITEM "載入遊戲(&O)...\tCtrl+O", 102

MENUITEM "儲存遊戲(&S)...\tCtrl+S", 103

MENUITEM "結束遊戲(&E)\tAlt+F4", 104

選單的外觀如下所示:

載入加速鍵

使用加速鍵先要宣告加速鍵表的Handler:

然後正式把Accelerator透過LoadAccelerator函數從Resource Script載入(此例子在WinMain主函式中載入):

最後把Message Loop變成翻譯Accelerator 鍵盤訊息:

HACCEL hAccel;

hAccel = LoadAccelerators(hInstance, "IDA_ACCEL1");

while (GetMessage(&msg, NULL, 0, 0))

{

if (!TranslateAccelerator(hwnd, hAccel, &msg))

{

TranslateMessage(&msg);

DispatchMessage(&msg);

};

};

Popup Menu

請看一看以下的選單外觀:

這種子選單叫sub-popup menu,可用以下resources script呼叫:

POPUP "選項(&O)"

{

POPUP "行棋時間(&T)"

{

MENUITEM "5秒", 3011

MENUITEM "10秒", 3012

MENUITEM "20秒", 3013

MENUITEM "30秒", 3014

MENUITEM "60秒", 3015

MENUITEM "120秒", 3016

MENUITEM "240秒", 3017

MENUITEM "無時限", 3018, CHECKED

}

MENUITEM "輸入姓名(&E)...", 302

}

即在POPUP宣告裡面再宣告一POPUP就能呼叫子選單。

利用API更動及控制選單

很多時候我們都想中途更改選單的項目或取消某項選單功能(i.e. 把選單的某項目變成「灰色」),我們可用EnableMenuItem來達到這目的(以下例子為一按鍵就取消「重新開始」一項:

HMENU hMenu;

switch(iMsg)

{

case WM_CHAR:

hMenu = GetMenu(hwnd);

EnableMenuItem(hMenu, CM_RESTART, MF_GRAYED);

SetMenu(hwnd.hMenu);

return 0;

};

我們先利用GetMenu取得現時被載入的選單handle,方可執行EnableMenuItem,第一個參數是選單的handle,第二個是選項的ID,第三個是控制選單選項的開關,MF_GRAYED為關,MF_ENABLED為開。

若你想更改選項的文字、刪除或插入選項,可使用ModifyMenu、DeleteMenu或InsertMenu函數:

ModifyMenu(hMenu, 101, MF_STRING, 101, "被更動了!");

DeleteMenu(hMenu, 101, MF_BYCOMMAND);

InsertMenu(hMenu, 101, MF_STRING, 102, "&Exit");

請留意InsertMenu這函數,第二個參數為前一個選項的ID,而你新增的選項ID為第四個參數,第五個參數就是選項文字。

程式自行建立選單(不建議使用)

請看看以下繁複的程式碼:

HMENU hMenu = CreateMenu(); // 建立主選單

hMenuPopup = CreateMenu(); // 建立子選單

// 填入選單MENUITEM選項

AppendMenu(hMenuPopup, MF_STRING, IDM_NEW, "重新開始(&N)");

AppendMenu(hMenuPopup, MF_STRING, IDM_OPEN, "載入遊戲(&L)");

AppendMenu(hMenuPopup, MF_STRING, IDM_SAVE, "儲存遊戲(&S)");

AppendMenu(hMenuPopup, MF_SEPARATOR, 0, NULL);

AppendMenu(hMenuPopup, MF_STRING, IDM_EXIT, "結束遊戲(&E)");

// 填入選單POPUP選項

AppendMenu(hMenu, MF_POPUP, (UINT)hMenuPopup, "檔案(&F)");

//填入第二個選單MENUITEM選項

hMenuPopup = CreateMenu(); // 再定義子選單

AppendMenu(hMenuPopup, MF_STRING, IDM_ITEM1, "選項一(&1)");

AppendMenu(hMenuPopup, MF_STRING, IDM_ITEM2, "選項二(&2)");

AppendMenu(hMenuPopup, MF_STRING, IDM_ITEM3, "選項三(&3)");

AppendMenu(hMenuPopup, MF_SEPARATOR, 0, NULL);

AppendMenu(hMenuPopup, MF_STRING, IDM_ITEM4, "選項四(&4)");

// 填入第二個選單POPUP選項

AppendMenu(hMenu, MF_POPUP, (UINT)hMenuPopup, "選項(&F)");

// 載入選單

SetMenu(hwnd, hMenu);

聰明的你,就算不用提醒也不會這樣建立選單,這裡我們只是介紹AppendMenu和CreateMenu的配合能夠建立選單而已,就算是Charles Petzold也不會這樣做。

另外,大家會看見「MF_SEPARATOR」這字,Separator即是選單上分開子選單的一條橫線,除了MF_SEPARATOR外,還有MF_CHECKED(即在選項左邊有剔號),請讀者自行測試。

最後一種Charles Petzold點名極不建議使用的宣告方法是LoadMenuIndirect,利用指針指向MENUITEMTEMPLATE結構,這是視窗內部呼叫的結構,一般程式員都不需理會其核心結構。若你是一個控制狂,企圖監督呼叫選單的整個機制和過程,你可自行研究LoadMenuIndirect這函數,在此我們沒有勇氣去試。

浮動選單

那些靜態選單也許不能吸引你,在大勢所趨下,動態浮動選單必能適合你的視窗程式。以下程式在使用者按下滑鼠右鍵後,利用TrackPopupMenu來顯示Popup選單:

HINSTANCE hInstance;

static HMENU hMenu;

POINT pt;

switch(iMsg)

{

case WM_CREATE:

hInstance = ((LPCREATESTRUCT)lparam)->hInstance; //取得視窗handle

hMenu = LoadMenu(hInstance, MAKEINTRESOURCE(IDM_MENU2)); //載入POPUP選單

hMenu = GetSubMenu(hMenu, 0); //取得POPUP選單的子選單

return 0;

case WM_RBUTTONDOWN:

pt.x = LOWORD(lParam); //從lParam取得滑鼠絕對螢幕X坐標

pt.y = HIWORD(lParam); //從lParam取得滑鼠絕對螢幕Y坐標

ClientToScreen(hwnd, &pt); //將絕對坐標改為以Client左上角為原點的相對坐標

TrackPopupMenu(hMenu, 0, pt.x, pt.y, 0, hwnd, NULL); //在滑鼠位置顯示選單

return 0;

}

其resources script也與一般靜態視窗有些不同:

IDM_MENU2 MENU

{

POPUP ""

{

POPUP "選單一"

{

MENUITEM "選項一", CM_ITEM1

MENUITEM "選項二", CM_ITEM2

}

}

}

和靜態選單一樣,都要使用LoadMenu載入選單,不過浮動選單就不需要選單主選項(i.e. 不需要「檔案」、「選項」或「說明」等),我們只對子選項有興趣,故次我們用GetSubMenu來取得子選項的handle來直接控制。

其他選單功能及注意事項

如前所述,更改選單可用AppendMenu、DeleteMenu、InsertMenu、ModifyMenu和RemoveMenu,在 Windows 3.0的時代我們要使用API中最複雜的ChangeMenu函數,但現在ChangeMenu已經撇除於API之外。另外,使用DeleteMenu會刪除Popup選單,而RemoveMenu則不會。

在你更改top-level選項(主選單選項)後並不會立刻改動選單的外觀,你可以呼叫DrawMenuBar來重繪選單:

若你想改動子選單的子選項(「子子選項」),可利用GetSubMenu先取得子選項的handle,然後利用AppendMenu、ModifyMenu等指令去修改選項:

DrawMenuBar(hwnd);

hMenu = GetMenu(hwnd); // 取得主選單handle

hMenuPopup = GetSubMenu(hMenu, 0); //取得子選單handle

當中GetSubMenu的第二個參數是主選單的主選項索引,以第一個選項為0,第二個選項為1(i.e. 例如「檔案」為0,「選項」為1,「說明」為2等等)。

你可呼叫GetMenuItemCount函數取得選單的選項數目:

呼叫GetMenuItemID函數取得選單的選項ID:

呼叫CheckMenuItem把選項加上剔號(參看「選單與加速鍵」)。在靜態選單中,可呼叫CheckMenuItem(hMenu,id, iCheck),第一個參數是選單handle,第二個參數是選單ID,第三個參數是MF_CHECKED或MF_UNCHECKED;

在Popup選單中,第二個參數不旦是ID,只要在第三個參數用OR運算子加入「MF_BYPOSITION」,就可把第二個參數變為索引值,省卻使用ID號碼的麻煩:

同理,只要在EnableMenuItem的三個參數加上MF_BYPOSITION,也可把第二個參數變成索引,HiliteMenuItem(Highlight menu item)這個罕用函數亦用到同一原理。

若你想取得選單上的選項文字,可呼叫GetMenuString:

函數把選單文字傳入pString(字元指針);iMaxCount是字串長度上限,置零時則返回字串長度;iFlag是MF_BYCOMMAND或MF_BYPOSITION,用來設定第二個參數為ID或索引。

呼叫GetMenuState可取得現時選單的狀態(e.g. 被highligted、checked、disabled等):

同樣地,iFlag是MF_BYCOMMAND或MF_BYPOSITION,iFlags傳回值可以是MF_CHECKED、MF_DISABLED、MF_GRAYED、MF_MENUBREAK、MF_MENUBARBREAK或MF_SEPARATOR等

最後,你不想再使用選單,可用DestroyMenu:

int iCount = GetMenuItemCount(hMenu);

int id = GetMenuItemID(hMenu, 0); //取得第一個選項ID

CheckMenuItem(hMenu, 2, MF_CHECKED | MF_POSITION); //把第三個選項加上剔號

iByteCount = GetMenuString(hMenu, id, pString, iMaxCount, iFlag);

iFlags = GetMenuState(hMenu, id, iFlag)

DestroyMenu(hMenu);

本章的 Source Code (加速鍵)可按此下載。

本章的 Source Code (利用API更改Menu)可按此下載。

本章的 Source Code (浮動選單)可按此下載。

本章的 Source Code (基本應用)可按此下載。

本章的 Source Code (Popup Menu)可按此下載。

相關文件:

- Menu snapshot

- Accelerator snapshot

- Popup menu snapshot

- (Dis)Enable menu sna

- Float menu snapshot

- Accelerator source

- ChangeMenu source

- Popup menu source

- Basic menu source

- Float menu source

(七):TrueType字型

電腦用戶、程式員、API心目中的字體

我們使用Microsoft Word時,只要把文字highlighted,再選擇工作列上的字體名稱,就能更改該段文字的字體,一切都來得很輕易,皆因Bill Gate在Windows 3.1時發明了TTF(True Type Font),這種向量字形,使得字體可以無限地放大而不失解像度,又可任意選擇粗幼、高度、闊度、斜度、字體間距等,一條數學式就能解決字體問題,在一般電腦用戶眼中,只需一隻手指、一隻滑鼠就能改變及設定字體。

當你身為一個視窗程式設計員,要確切知道如何呼叫字體嗎?其實不然,若你不使用API的話,所有視窗程式開發工具都簡化了載入字體這個繁複的工作, SetFont函數隨處可見,程式員一般都不需理解TTF的運作原理、如何處理/設定字體資訊和呼叫方法,而這種方便也是大部份程式設計員所盼望和要求的。

當你身為一個API程式設計員,你就要確設知道呼叫字體的機制、處理及設定字體,再加上數十行程式碼才可載入一種字體,這種對字體細緻的控制就是API心目中的字體處理。

建立字體handle

請先看CreateFont函數的原型:

HFONT hFont = CreateFont(int nHeight, //字體高度

int nWidth, //字體闊度

int nEscapement, //字體斜度

int nOrientation, //底線斜度

int fnWeight, //字體粗幼

DWORD fdwItalic, //設定字體為斜體

DWORD fdwUnderline, //設定字體底線

DWORD fdwStrikeOut, //設定刪線

DWORD fdwCharSet, //設定字元集

DWORD fdwOutputPrecision, //字體輸出解析度

DWORD fdwClipPrecision, //字型擷取解析度

DWORD fdwQuality, //字體輪廓質素

DWORD fdwPitchAndFamily, //字體的外觀參考(沒有所需字體時用)

LPCTSTR lpszFace //字體名稱

);

真嚇人,共有十四個參數,儘管你能記得設定字體要有哪些參數,但你也未必能記清楚每個參數的位置。CreateFont函數會傳回HFONT字體handle,這是最直接、最快、算是最易用的字體函數了。

當中,字體高度和闊度是電腦邏輯長度,可用以下公式計算:

nHeight = -MulDiv(PointSize, GetDeviceCaps(hDC, LOGPIXELSY), 72);

當中PointSize就是字體的大小(這數字就是在Microsoft Word中設定字體大小的數字)。

你亦可跟隨Charles Petzold的計算方法,不過會比以上方法較複雜難明,但這方法也是最準確的方法(能準確計算字體的邏輯高度):

FontSize = 120; //即12 points,Microsoft Word的預設字體大小

hFont = CreateFont(

-(int)(fabs(FontSize *GetDeviceCaps(hdc, LOGPIXELSY) / 72) / 10.0 + 0.5),

0, 0, 0, 500, FALSE, FALSE, FALSE, 0, 0, 0, 0, 0, "標楷體"); //建立字體

SelectObject(hdc, hFont); //令HDC選擇字體

TextOut(hdc, 100, 100, "標楷體12點字體", 8);

DeleteObject(SelectObject(hdc, hFont);

從以上例子可見,我們建立了十二點、中等粗幼、非斜體、沒加底線、沒加刪線的「標楷體」向量字型,利用 (點數 x 10 x 系統邏輯高度 / 72 + 0.5) 的絕對整數值 這公式就能計算字體的實際顯示大小。

另外,我們利用SelectObject來載入字體handle,以後的文字輸出均以該字體顯示,直至DeleteObject為止,我們絕大部份時間都以上述程式碼來設定字體。

第二種呼叫字體的方法

我們有CreateFont函數,亦有CreateFontIndirect函數來間接建立字體,但別以為這函數能簡化你呼叫字體的步驟,這函數只可讓你填入字體的外觀資料到LOGFONT結構後,才建立字體,而不需直接把十四個參數輸入CreateFont,先看看LOGFONT結構:

LOGFONT

{

LONG lfHeight;

LONG lfWidth;

LONG lfEscapement;

LONG lfOrientation;

LONG lfWeight;

BYTE lfItalic;

BYTE lfUnderline;

BYTE lfStrikeOut;

BYTE lfCharSet;

BYTE lfOutPrecision;

BYTE lfClipPrecision;

BYTE lfQuality;

BYTE lfPitchAndFamily;

TCHAR lfFaceName[LF_FACESIZE];

}

十四個參數一個不漏地呈現在LOGFONT結構中,把資料填入這結構,然後呼叫CreateFontIndirect就行了:

LOGFONT lf;

HFONT hFont;

...

... // 填入字體資料於LOGFONT結構中

hFont = CreateFontIndirect(&lf);

LOGFONT結構詳解

在此,我們不妨說說LOGFONT結構。剛才也說明過lfHeight是邏輯高度,而lfWidth就是邏輯闊度,需要呼叫GetDeviceCaps來取得螢幕的DPI(每一吋有多少pixel),通常我們都會把lfWidth置零,置零代表字體闊度會跟隨lfHeight來調整,以取得最合適最美觀的長闊比例,當然你也可以自行設定lfWidth。

lfEscapement是字體斜度,以0.1度為單位,這角度是escapement vector和X軸的角度,而escapement vector是平行於底線的。與lfEscapement一樣,lfOrientation也是設定斜度,應與lfEscapement的設定值一樣(但在Windows NT/2000上,若繪圖模式被設定為GM_ADVANCED,Orientation和Escapement是兩回事,由於涉及複雜計算,在此不作詳談)。

lfWeight是字體粗幼,由0至900,其設定值必需為100的倍數,一般來說400是正常粗幼,而700就是粗體,若該值設為0則代表以預計粗幼顯示文字。

隨後的三個值lfItalic、lfUnderline、lfStrikeOut就是Microsoft Word讓使用者設定為斜體、加底線和加刪線的功能,填入TRUE則開啟選項,FALSE則關閉。

lfCharSet則讓人設定字元集,例如我們想顯示簡體字,就會設定為GB2312_CHARSET,希臘符號就設定為SYMBOL_CHARSET,大五碼為CHINESEBIG5_CHARSET,ANSI為ANSI_CHARSET等等,若本值設零則為DEFAULT_CHARSET,預設值在中文視窗中是CHINESEBIG5_CHARSET。

lfOutPrecision是設定字型顯示的精確度,當大家設定了繁複的字體斜度粗幼之後,精密的raster operation總會有點差錯,這些差錯可以靠修正部份字體設定來減輕,但不能消除,這時候你要決定捨棄字體的輪廓還是清晰度。因為我們用的都是 TTF,所以我們都會把此值設為0,在TTF的世界不需要考慮字型會失真。

lfClipPrecision是設定部份字型被其他圖像或框架覆蓋時的顯示精確度,和lfOutPrecision一樣,我們一般都把此值設0。

lfQuality就關係到TTF,這是決定GDI處理字體顯示的精確度,與字體的性質無關,在大部份情況也會設0代表使用預設值,我們也可設為 PROOF_QUALITY使字體顯示更精確,但運算時間也較長;你也可算擇DRAFT_QUALITY來取得最乎合比例的字型,運算時間亦較短(別以為字體的運算時間是多不重要,大家用過Windows 2000的Notepad後就會認同我這番話)。

lfPitchAndFamily設定字型的端點和字系,假若你所選擇的字體並未能在你的電腦上找到,這個參數就會找出補充語系來顯示所需文字。

請留意,絕大部份情況下,我們都不需要刻意設定lfCharSet、lfOutPrecision、lfClipPrecision、lfQuality和lfPitchAndFamily,我們把它設為0或預計值便可

本章的 Source Code 可按此下載。

相關文件:

- Sample font snapshot

- Font size snapshot

- Escapement snapshot

- Font weight snapshot

- Width/Height snapsho

- Font source

(八):滑鼠

隨處可見的滑鼠

任何Windows程式都會有滑鼠的出現,無論你的程式已完全拋棄了滑鼠,游標仍然顯示在你的程式中。

滑鼠有三鍵(左、中、右鍵)和兩鍵(左、右鍵)之分,由Windows 95開始就完全支援三鍵滑鼠,不需額外添置驅動程式。一般來說,按左鍵的功用都是執行程式碼或選取物件(Button、Menu等),按中鍵控制 System Menu,按右鍵負責顯示Option,程式員應跟隨這慣例編寫程式。

常用滑鼠訊息

每當你按下或者放開滑鼠鍵,都會發出訊息,例如按下左鍵會發出WM_LBUTTONDOWN(left button down),放開會發出WM_LBUTTONUP(left button up),中間則是「M」,右邊則是「R」,即WM_MBUTTONDOWN和WM_RBUTTONUP等:

switch(iMsg)

{

case WM_LBUTTONDOWN:

... // Do Something

return 0;

}

用這個訊息Handler來處理滑鼠左鍵被按下後的動作。

case WM_LBUTTONDOWN:

LOWORD(lParam) = 滑鼠X坐標

HIWORD(lParam) = 滑鼠Y坐標

wParam //載有滑鼠按鍵時的按鍵狀態

MK_LBUTTON //按了左鍵

MK_MBUTTON //按了中鍵

MK_RBUTTON //按了右鍵

MK_SHIFT //按了鍵盤SHIFT鍵

MK_CONTROL //按了鍵盤CTRL鍵

大家也許更加關心,當你按滑鼠鍵後游標的目前位置是甚麼,又或者是否同時按下兩鍵或三個鍵,我們可從lParam和wParam得知:

請留意,程式員可利用「&」運算符來檢查wParam:

若wParam包含MK_LBUTTON(即按下了SHIFT鍵)就會得出非零,即TRUE,並執行以下程式碼

if (wParam &MK_SHIFT)

{

...

...

}

移動中的滑鼠

就算滑鼠游標有移動,Windows都會發出訊息,但並不是滑鼠每移一點都會發出訊息,而是根據滑鼠驅動程式每秒鐘設定發出訊息的頻率而決定,否則你程式的Message Loop很快便被炸爆了。

滑鼠移動時就會發出WM_MOUSEMOVE,和其他滑鼠訊息一樣,lParam都記載了游標的X和Y坐標。

Double Click的處理

為了安全起見,Double click能有效阻止因錯誤按了滑鼠,而執行一些危險程式,例如格式化硬碟、刪除檔案等,故你的程式使用Double click來加強安全性,當然會比接受Single click更安全。在使用Double Click之前,你必需更改Window Class的Style:

wc.style = CS_HREDRAW | CS_VREDRAW | CS_DBCLKS;

以 | 運算符加入CS_DBCLKS指示程式接受Double Click輸入。

之後你便可以處理WM_LBUTTONDBCLK(左鍵)、WM_MBUTTONDBCLK(中鍵)、WM_RBUTTONDBCLK(右鍵)訊息了。

滑鼠在Non-Client Area被按下

若滑鼠游標不在你的程式內被按下,那便不會發出WM_LBUTTONDOWN等訊息,而是發出WM_NCLBUTTONDOWN(多了「NC」)

剩餘的滑鼠訊息 - WM_NCHITTEST

WM_NCHITTEST這個訊息對於程式員沒有多大的用處,它的lParam和WM_LBUTTONDOWN一樣裝了X和Y坐標,wParam則被棄用。

但這個訊息卻是Windows的核心訊息,所有滑鼠訊息都要靠WM_NCHITTEST來建立,任何滑鼠動作都會觸發WM_NCHITTEST,觸發次數視乎滑鼠驅動程式每秒鐘派發多少訊息。既然這個訊息是其他滑鼠訊息的始祖,只要我們捕捉這個訊息而不把它處理,就能在你的程式完全廢止滑鼠,包括按左上角的System Menu和右上角的最小化和最大化按鈕:

case WM_NCHITTEST:

return (LRESULT) HTNOWHERE;

WM_NCHITTEST有四個數值可以傳回:

HTCLIENT //滑鼠在Client Area上

HTNOWHERE //滑鼠不在任何視窗上

HTTRANSPARENT //有Focus的視窗被其他視窗覆蓋著

HTERROR //令DefWindowProc產生「Beep」一聲

剛才的程式碼強行設定滑鼠不在任何Window上,即是Click「空氣」,那麼就產生不了其他滑鼠訊息。

滑鼠游標

滑鼠游標就是在螢幕上的箭咀(或是其他圖示),若我們想把游標隱藏,我們利用ShowCursor(FALSE)就行,相反,想顯示游標就用ShowCursor(TRUE)。你可以利用GetCursorPos和SetCursorPos來獲取和設定游標的位置:

POINT pt;

GetCursorPos(&pt); // 把游標X和Y坐標傳入pt結構

SetCursorPos(x, y); // 把游標坐標設為(x,y)

當游標離開了Window Frame後,以後滑鼠有甚麼動作,你的程式也不會接收到,假若你想接收Frame出面的滑鼠動作,可利用SetCapture函數:

SetCapture(hwnd)

此後,所有滑鼠訊息都會導入擁有hwnd的視窗中。有一點必需留意,你必需使用ReleaseCapture()把滑鼠釋放,否則其他程式就會接收不到滑鼠訊息而癱瘓。

滑鼠應用技巧

1. 若你希望在按死左鍵後不斷拖行滑鼠時,仍想執行某些程式碼,例如拖出框線直至放開滑鼠:

static POINT pt, cur_pt; // 矩形的左上角及右下角坐標

static BOOL Pressing = FALSE; // 有沒有按下左鍵的旗標

case WM_PAINT:

hdc = BeginPaint(hwnd, &ps);

if (Pressing) Rectangle(hdc, pt.x, pt.y, cur_pt.x, cur_pt.y); // 繪畫矩形

EndPaint(hwnd, &ps);

return 0;

case WM_LBUTTONDOWN:

// 按下滑鼠時取得游標X和Y坐標作為左上角坐標

pt.x = LOWORD(lParam);

pt.y = HIWORD(lParam);

return 0;

case WM_MOUSEMOVE:

// 拖動滑鼠時取得游標X和Y坐標作為右下角坐標

cur_pt.x = LOWORD(lParam);

cur_pt.y = HIWORD(lParam);

// 檢查有沒有按下左鍵

Pressing = (wParam &MK_LBUTTON) ? TRUE : FALSE;

InvalidateRect(hwnd, NULL, TRUE);

return 0;

2. 若你想按完鍵不放再按右鍵才執行命令(例如踩地雷利用數字開啟四周地雷):

switch(iMsg)

{

case WM_RBUTTONDOWN:

if (wParam & MK_LBUTTON)

PostQuitMessage(0);

return 0;

};

當你按下左鍵後再按右鍵便會結束程式(按右鍵後才按左鍵則沒有反應)

請留意,若是先按左再按右,就需先處理WM_RBUTTONDOWN;若是先按右再按左,就需先處理WM_LBUTTONDOWN。

本章的 Source Code (拖動滑鼠)可按此下載。

本章的 Source Code (二鍵同按)可按此下載。

相關文件:

- Drag-Drop snapshot

- Drag-Drop source

- Multi-click source

(九):模擬五子棋程式

讓我們綜合一下

剛才我們學了如何建立視窗、利用GDI寫字、用點陣圖繪圖、加入選單和滑鼠,那麼利用以上所學的東西,可以寫甚麼程式呢?不如試一試五子棋吧。

寫五子棋程式的元素

今次所模擬的五子棋就是黑方和白方,最先把自己的棋子放在棋盤,並且至少有一個連續五粒斜向、橫向或縱向的棋子組合,那方便勝利了,是「過三關」的進化版成「過五關」,不過棋盤就是標準的圍棋棋盤,共有19 x 19格共361格。(注意:真正的五子棋規則可不是這麼簡單)

所以最先要做的,就是繪畫棋盤,你可以利用Rectangle函數來畫出棋盤,或者預先利用小畫家把棋盤畫在點陣圖中,然後在程式中途載入(以下程式採用點陣圖方法):

按此觀看程式碼

利用選單列出程式功能及選項

你的程式最好有快捷的方法去開始或結束,可以輸入玩者姓名和行棋時間等等,這些功能都可以透過選單來簡化介面。

按此觀看程式碼

使用滑鼠

我們需要使用滑鼠游標來揀選棋盤位置下子,而且按下後我們還需要偵測是否已有連續五粒同款棋子出現,加入滑鼠功能和偵測是本程式的核心。

首先我們要加入Message Handler,來處理WM_LBUTTONDOWN訊息。若游標出了棋盤,我們就不用處理這訊息了;若游標進了棋盤,但按鍵位置卻早以有棋子放了,這個訊息亦不用處理。

當篩選好有用訊息後,就根據現在是黑方或白方行,放下相應顏色的棋子,放好後再把行棋權交給另一方。我們先設ChessArray來儲存棋盤上各位置有甚麼棋子,放棋時就把相應位置設1或2,代表黑子或白子,然後再重繪棋盤,之後便能像真的下棋一樣,把棋子放上棋盤了。

在放好每一步棋後,我們都要檢查有沒有五子連環的出現,我們可使用簡單的For Loop來完成。

我們利用旗標變量(並不是真的Boolean變量,只是用來識別狀態的變量)和常數變量來簡化程式。例如我們設了WhiteWin和BlackWin兩個旗標變量,代表白色或是黑色勝利了,我們利用自定的DetectWinLose函數來偵測五子連環的情況,有的話就著起WhiteWin或 BlackWin表示其中一方勝利。

放棋時我們利用自設的PlaceChess函數來操控ChessArray各元素的值,靠WhiteTurn來決定行棋權落入誰的手中。

我們設定了一堆常量(把數字旗標譯為有意義的文字)和旗標配合,例如若ChessArray的ChessArray[1][2]含有「WHITE_PLACE」(即1),即代表ChessArray[1][2]放了白子。

最後,我們還設有TotalRound、WhiteWinRound和BlackWinRound來記錄勝利累積總盤數。

按此觀看程式碼

相關文件:

- Load bitmap (webpage

- Load menu (webpage)

- Mouse (webpage)

- 五子棋 source

(十):計時器

計時器概述

計時器相信是Windows裡最簡單的元件了,是嗎?我們感受不到計時器在運作,因此也不知它是否真正運行著,或已被「殺」(Kill The Timer),有種恐?感嗎?就因為Timer純粹是一個隱藏著的計時器,成就了它簡單的用法和API函數。

其實Timer和鍵盤滑鼠一樣,都是一個Input Device,Timer會限時限刻輸入訊號到CPU中,產生WM_TIMER訊息。在Windows中,你可控制Timer由每1毫秒發放一個訊息,至每4,294,967,295毫秒(約50日)發放一個訊息,但只不過是理論值,實際上由於BIOS的限制,硬件只能每秒發出18.2次(即每次 54.925毫秒)訊號,即Timer最低只能每55毫秒發出一個訊號,低於此值一律當55毫秒計。

如何呼叫計算器

我們共有三種方法呼叫計時器:

SetTimer(HWND hwnd, UINT TimerID, UINT TimeInterval, TIMERPROC lpTimerProc);

1. 第一個參數當然是本程式的Window Handler,第二個就是本計時器的ID,若你的程式有多個計時器,你必需確保計時器ID沒有重覆。第三個參數就是每隔多久發放訊號,第四個參數就是專責處理計時器訊號的函數,通常都不需要並設為NULL。

SetTimer(hwnd,1,100,NULL);

代表該計時器每0.1秒就會發出

WM_TIMER訊號。

如果你有多個計時器,那我們應怎樣分別是哪個計時器發出WM_TIMER呢?我們可用wParam來分別:

case WM_TIMER:

switch(wParam)

{

case 1:

... // Do something

break;

case 2:

... // Do something different

break;

}

return 0;

}

當你用於計時器時,請呼叫KillTimer來結束計時器:

KillTimer(hwnd,1);

2. 第二個方法就是使用SetTimer第四個參數 - 呼叫專責處理計時器函數。計時器CALLBACK函數原型如下:

VOID CALLBACK TimerProc(HWND hwnd,UINT iMsg,UINT TimerID,DWORD Time)

在SetTimer時,我們也需要更改第四個參數為:

SetTimer(hwnd,1,100,(TIMERPROC)TimerProc)

3. 第三個方法類似第二個方法,但這次我們把Window Handler設為NULL,並且忽略Timer ID,這是最罕見的用法,亦都是不建議使用的方法:

SetTimer(NULL,0,100,(TIMERPROC)TimerProc)

記得五子棋程式要計時嗎

我們在選單上「選項」一欄中,我們有「行棋時間」一項,它的Popup Menu有多個時限值可設置,這時我們需要計時器的幫助。

在這個程式中,我們在白方按下滑鼠左鍵後,清除之前的Timer,然後建立一個新的Timer開始計時,時段為一秒鐘,每隔一秒就發送WM_TIMER,在這Message Handler中,會把TimeElapsed這個記錄行棋時間的變數增加一,代表行棋時間共增加了一秒,若行棋時間超出了時限設定值,就會自動把行棋權交給對方,並將TimeElapsed重新置零。

當然,在對方沒有超時的情況下,只要對方按下放了棋,即在發放WM_LBUTTONUP後,就會把行棋控制權交給自己,這時我們也需重設計時器,重新計時。在分出勝負時亦需要停止計時。

本章的 Source Code (五子棋計時版)可按此下載。

相關文件:

- 五子棋 source