用 Win32 API 設計視窗程式

在學習 Windows 桌面程式開發時,最經典的範例就是建立一個空白視窗。

本文將帶你了解 Win32 API 的基本架構,並透過一個最小化範例理解:

  1. 註冊視窗類別(Register Window Class)

  2. 建立視窗(Create Window)

  3. 執行訊息迴圈(Message Loop)

  4. 處理視窗訊息(Window Procedure)


程式完整架構

一個 Win32 視窗程式通常包含:

WinMain()
 ├─ RegisterClassEx()
 ├─ CreateWindowEx()
 ├─ ShowWindow()
 └─ Message Loop
       ↓
    WndProc()

其中:

  • WinMain() 是程式進入點

  • WndProc() 是視窗訊息處理中心

  • Message Loop 持續接收使用者操作


1. 定義視窗類別名稱

首先建立一個全域常數,用來識別視窗類別。

const char g_szClassName[] = "myWindowClass";

之後註冊視窗與建立視窗時,都會使用這個名稱。


2. 建立視窗程序(Window Procedure)

Windows 採用「訊息驅動(Message Driven)」架構。

所有使用者操作:

  • 點擊滑鼠

  • 按下鍵盤

  • 關閉視窗

都會轉換成訊息(Message)。

這些訊息會送到 WndProc() 處理。

LRESULT CALLBACK WndProc(HWND hwnd,
                         UINT msg,
                         WPARAM wParam,
                         LPARAM lParam)
{
    switch(msg)
    {
        case WM_CLOSE:
            DestroyWindow(hwnd);
            break;

        case WM_DESTROY:
            PostQuitMessage(0);
            break;

        default:
            return DefWindowProc(
                hwnd,
                msg,
                wParam,
                lParam
            );
    }

    return 0;
}

WM_CLOSE

當使用者:

  • 點擊右上角 X

  • 按下 Alt + F4

系統會送出:

WM_CLOSE

收到後呼叫:

DestroyWindow(hwnd);

開始銷毀視窗。


WM_DESTROY

當視窗真正被銷毀後:

WM_DESTROY

會被觸發。

此時通知程式結束:

PostQuitMessage(0);

它會送出:

WM_QUIT

讓訊息迴圈停止執行。


DefWindowProc()

若訊息不是我們要處理的:

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

交給 Windows 預設處理。

例如:

  • 視窗拖曳

  • 最小化

  • 最大化

  • 調整大小

都由系統自動完成。


3. WinMain:程式進入點

Windows GUI 程式不使用 main()

而是:

int WINAPI WinMain(
    HINSTANCE hInstance,
    HINSTANCE hPrevInstance,
    LPSTR lpCmdLine,
    int nCmdShow
)

這是所有 Win32 視窗程式的起點。


4. 註冊視窗類別

建立:

WNDCLASSEX wc;

並設定相關屬性。

wc.cbSize        = sizeof(WNDCLASSEX);
wc.style         = 0;
wc.lpfnWndProc   = WndProc;
wc.cbClsExtra    = 0;
wc.cbWndExtra    = 0;
wc.hInstance     = hInstance;

wc.hIcon         = LoadIcon(
                       NULL,
                       IDI_APPLICATION);

wc.hCursor       = LoadCursor(
                       NULL,
                       IDC_ARROW);

wc.hbrBackground =
    (HBRUSH)(COLOR_WINDOW + 1);

wc.lpszMenuName  = NULL;

wc.lpszClassName = g_szClassName;

wc.hIconSm       =
    LoadIcon(NULL,
             IDI_APPLICATION);

各欄位用途

欄位功能
lpfnWndProc指向訊息處理函式
hIcon大圖示
hIconSm小圖示
hCursor滑鼠游標
hbrBackground背景顏色
lpszClassName類別名稱

註冊類別

if(!RegisterClassEx(&wc))
{
    MessageBox(
        NULL,
        "Window Registration Failed!",
        "Error!",
        MB_ICONEXCLAMATION | MB_OK
    );

    return 0;
}

若註冊失敗:

RegisterClassEx()

回傳 0。

此時跳出錯誤訊息並結束程式。


5. 建立視窗

註冊完成後即可建立視窗。

hwnd = CreateWindowEx(
    WS_EX_CLIENTEDGE,
    g_szClassName,
    "The title of my window",
    WS_OVERLAPPEDWINDOW,

    CW_USEDEFAULT,
    CW_USEDEFAULT,

    320,
    240,

    NULL,
    NULL,
    hInstance,
    NULL
);

參數說明

參數功能
WS_EX_CLIENTEDGE凹陷邊框
g_szClassName使用的類別
視窗標題顯示在標題列
WS_OVERLAPPEDWINDOW標準視窗樣式
320寬度
240高度

建立後畫面如下:

+----------------------+
| The title of my ... |
+----------------------+
|                      |
|                      |
|      Client Area     |
|                      |
+----------------------+

檢查建立是否成功

if(hwnd == NULL)
{
    MessageBox(
        NULL,
        "Window Creation Failed!",
        "Error!",
        MB_ICONEXCLAMATION | MB_OK
    );

    return 0;
}

若回傳 NULL 表示建立失敗。


6. 顯示視窗

建立完成後仍然是隱藏狀態。

需要呼叫:

ShowWindow(hwnd, nCmdShow);
UpdateWindow(hwnd);

ShowWindow()

讓視窗顯示。

ShowWindow(hwnd, nCmdShow);

UpdateWindow()

要求立即重繪。

UpdateWindow(hwnd);

避免剛開啟時出現空白畫面。


7. 訊息迴圈(Message Loop)

這是整個程式的核心。

while(GetMessage(
          &Msg,
          NULL,
          0,
          0) > 0)
{
    TranslateMessage(&Msg);

    DispatchMessage(&Msg);
}

GetMessage()

取得訊息:

GetMessage(...)

例如:

滑鼠移動
鍵盤輸入
視窗關閉
重繪請求

TranslateMessage()

將鍵盤訊息轉換成字元訊息。

TranslateMessage(&Msg);

例如:

按下 A
↓
WM_CHAR

DispatchMessage()

把訊息送到:

WndProc()

處理。

DispatchMessage(&Msg);

流程如下:

使用者操作
      ↓
 GetMessage
      ↓
 TranslateMessage
      ↓
 DispatchMessage
      ↓
    WndProc

8. 程式結束

當收到:

PostQuitMessage(0);

時:

GetMessage()

會回傳:

0

訊息迴圈結束:

while(GetMessage(...) > 0)

跳出後回傳:

return Msg.wParam;

程式正式結束。


完整執行流程

WinMain
   │
   ├─ RegisterClassEx
   │
   ├─ CreateWindowEx
   │
   ├─ ShowWindow
   │
   └─ Message Loop
          │
          ▼
       WndProc
          │
          ├─ WM_CLOSE
          │      ↓
          │ DestroyWindow
          │
          └─ WM_DESTROY
                 ↓
          PostQuitMessage
                 ↓
              WM_QUIT
                 ↓
          Message Loop 結束

結語

這個範例雖然只有數十行程式碼,但已經涵蓋 Win32 GUI 程式最重要的四個核心概念:

  • 視窗類別(Window Class)

  • 視窗建立(Window Creation)

  • 訊息迴圈(Message Loop)

  • 訊息處理(Window Procedure)

理解這個架構後,就可以進一步加入:

  • 按鈕(Button)

  • 功能表(Menu)

  • 繪圖(GDI)

  • 鍵盤與滑鼠事件

  • 自訂控制項

逐步建立完整的 Windows 桌面應用程式。


完整程式碼如下:


#include <windows.h> // 全域變數:儲存視窗類別的名稱 [1, 2] const char g_szClassName[] = "myWindowClass"; // 第四步:視窗程序 (The Window Procedure) [3] // 這是程式的「大腦」,負責處理系統傳送給視窗的所有訊息。 LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) { switch(msg) { case WM_CLOSE: // 當使用者按下關閉按鈕或 Alt+F4 時發送 [3] // 我們在此處呼叫 DestroyWindow 來銷毀視窗 [4] DestroyWindow(hwnd); break; case WM_DESTROY: // 當視窗被銷毀時,發送退出訊息給訊息迴圈 [4] // PostQuitMessage 會發送 WM_QUIT,使 GetMessage 回傳 FALSE [4] PostQuitMessage(0); break; default: // 對於我們不感興趣的訊息,交給系統預設處理 [4] return DefWindowProc(hwnd, msg, wParam, lParam); } return 0; } // Windows 程式的進入點 [1] int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) { WNDCLASSEX wc; HWND hwnd; MSG Msg; // 第一步:註冊視窗類別 [1, 5] // 設定視窗類別的屬性,例如圖示、游標與背景顏色 [5, 6] wc.cbSize = sizeof(WNDCLASSEX); // 結構的大小 [1] wc.style = 0; // 類別樣式 [1] wc.lpfnWndProc = WndProc; // 指向處理訊息的視窗程序 [6] wc.cbClsExtra = 0; // 額外的類別資料 [6] wc.cbWndExtra = 0; // 額外的視窗資料 [6] wc.hInstance = hInstance; // 應用程式執行個體控制碼 [6] wc.hIcon = LoadIcon(NULL, IDI_APPLICATION); // 大圖示 [6] wc.hCursor = LoadCursor(NULL, IDC_ARROW); // 游標 [6] wc.hbrBackground = (HBRUSH)(COLOR_WINDOW+1); // 背景畫刷顏色 [6] wc.lpszMenuName = NULL; // 菜單名稱 [6] wc.lpszClassName = g_szClassName; // 類別識別名稱 [7] wc.hIconSm = LoadIcon(NULL, IDI_APPLICATION); // 工作列的小圖示 [7] // 檢查註冊是否失敗,若失敗則彈出警告並結束 [8] if(!RegisterClassEx(&wc)) { MessageBox(NULL, "Window Registration Failed!", "Error!", MB_ICONEXCLAMATION | MB_OK); return 0; } // 第二步:建立視窗 [8] // 設定樣式、標題、初始位置與大小 (320x240) [2, 9] hwnd = CreateWindowEx( WS_EX_CLIENTEDGE, // 擴展樣式:例如凹陷邊框 [8] g_szClassName, // 要使用的類別名稱 [2] "The title of my window", // 視窗標題文字 [2] WS_OVERLAPPEDWINDOW, // 視窗基本樣式 [2] CW_USEDEFAULT, CW_USEDEFAULT, 320, 240, // 位置與寬高 [9] NULL, NULL, hInstance, NULL); // 父視窗、選單、實例指標與額外資料 [10] // 務必檢查視窗是否成功建立,避免程式崩潰 [11] if(hwnd == NULL) { MessageBox(NULL, "Window Creation Failed!", "Error!", MB_ICONEXCLAMATION | MB_OK); return 0; } // 顯示視窗並確保它正確重繪 [12] ShowWindow(hwnd, nCmdShow); UpdateWindow(hwnd); // 第三步:訊息迴圈 (The Message Loop) [13] // 這是程式的「心臟」,持續取得並分派使用者產生的訊息 [13] while(GetMessage(&Msg, NULL, 0, 0) > 0) { TranslateMessage(&Msg); // 額外處理鍵盤事件 [13] DispatchMessage(&Msg); // 將訊息發送到 WndProc 處理 [13] } return Msg.wParam; }

若您覺得文章寫得不錯,請點選文章上的廣告,來支持小編,謝謝。

留言

這個網誌中的熱門文章