前言

在進入代碼環節之前,首先來思考一個問題,一個基本的桌面應用程序,包含了甚麼?

  1. 視窗: 用於顯示內容,包含菜單欄、圖標、名字等
  2. 消息循環器: 循環監聽消息發生
  3. 消息處理器: 包含了滑鼠、鍵盤的事件等的處理

也就是說,一個最基本的桌面應用程序包含了一個視窗以及一個消息體。

代碼說明

視窗的注冊及顯示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37

UINT window_width = 1280;
UINT window_height = 720;

// 使用WNDCLASS結構體,描述視窗的屬性
WNDCLASS wc;
wc.style = CS_HREDRAW | CS_VREDRAW;
wc.lpfnWndProc = MsgProc; // 消息處理器
wc.cbClsExtra = 0;
wc.cbWndExtra = 0;
wc.hInstance = hInstance;
wc.hIcon = LoadIcon (0, IDI_APPLICATION);
wc.hCursor = LoadCursor (0, IDC_ARROW);
wc.hbrBackground = (HBRUSH)GetStockObject (NULL_BRUSH);
wc.lpszMenuName = 0;
wc.lpszClassName = L"HelloWorld";

// 注冊視窗
if (!RegisterClass (&wc)) {
MessageBox (0, L"RegisterClass Failed.", 0, 0);
return 0;
}

// 重新計算整個視窗的大小
RECT rect = {0, 0, window_width, window_height};
AdjustWindowRect (&rect, WS_OVERLAPPEDWINDOW, false);

// 創建視窗
HWND hwnd = CreateWindow (L"HelloWorld", L"Hello World",
WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT,
rect.right - rect.left, rect.bottom - rect.top,
0, 0, hInstance, 0);

// 顯示視窗
ShowWindow (hwnd, SW_SHOW);
// 更新視窗
UpdateWindow (hwnd);

消息處理器

1
2
3
4
5
6
7
8
9
LRESULT CALLBACK MsgProc (HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) {
switch (msg) {
case WM_DESTROY:
PostQuitMessage (0); // 發送一個WM_QUIT
break;
}

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

消息循環

1
2
3
4
5
6
7
MSG msg = {};
while (msg.message != WM_QUIT) {
if (PeekMessage (&msg, 0, 0, 0, PM_REMOVE)) {
TranslateMessage (&msg); // 將,例如鼠標或鍵盤事件等轉換成消息
DispatchMessage (&msg); // 發送消息
}
}

在按照代碼輸入WNDCLASS的時候,可能會被WNDCLASSAWNDCLASSEX等的相似結構體所迷惑。根據微軟官方文檔的說法,WNDCLASS已經全面被WNDCLASSEX所取代。在新系統上的新功能,只能通過WNDCLASSEX來設置。而WNDCLASSAWNDCLASSW中的A與W的區別在於程序是否使用了Unicode。WNDCLASS以及WNDCLASSEX是Microsoft官方已經為用戶設計好的宏,根據用戶的配置來自動切換到A或W。其它還有很多類似存在EX、A和W的相似方法和結構體都是相似的原理。

除了PeekMessage()外,還有GetMessage()GetMessage()會一直等待直到有消息到來才有返回值,屬於阻塞函數。而PeekMessage是以查看的方式從系統中獲取消息,屬於非阻塞函數。

UpdateWindow()相當於執行一次Paint,會發送一次WM_PAINT消息,而PeekMessage()以及GetMessage()也會生成WM_PAINT消息。

AdjustWindowRect()是因為在創建視窗時,最初所設置的window_width以及window_height是不包含窗單欄、邊框等的大小,所以需要使用AdjustWindowRect(),根據視窗的類型來計算真實的視窗大小。

桌面應用程序

完整代碼

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
LRESULT CALLBACK MsgProc (HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) {
switch (msg) {
case WM_DESTROY:
PostQuitMessage (0);
break;
}

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

int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE prevInstance, LPSTR cmdLine, int nCmdShow) {
WNDCLASS wc;
wc.style = CS_HREDRAW | CS_VREDRAW;
wc.lpfnWndProc = MsgProc;
wc.cbClsExtra = 0;
wc.cbWndExtra = 0;
wc.hInstance = hInstance;
wc.hIcon = LoadIcon (0, IDI_APPLICATION);
wc.hCursor = LoadCursor (0, IDC_ARROW);
wc.hbrBackground = (HBRUSH)GetStockObject (NULL_BRUSH);
wc.lpszMenuName = 0;
wc.lpszClassName = L"HelloWorld";

if (!RegisterClass (&wc)) {
MessageBox (0, L"RegisterClass Failed.", 0, 0);
return 0;
}

RECT rect = {0, 0, window_width, window_height};
AdjustWindowRect (&rect, WS_OVERLAPPEDWINDOW, false);

HWND hwnd = CreateWindow (L"HelloWorld", L"Hello World",
WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT,
rect.right - rect.left, rect.bottom - rect.top,
0, 0, hInstance, 0);

ShowWindow (hwnd, SW_SHOW);
UpdateWindow (hwnd);

MSG msg = {};
while (msg.message != WM_QUIT) {
if (PeekMessage (&msg, 0, 0, 0, PM_REMOVE)) {
TranslateMessage (&msg);
DispatchMessage (&msg);
}
}

UnregisterClass (wc.lpszClassName, wc.hInstance);
return 0;
}

前書き

コードを見る前、サンプルなデスクトップアプリケーションではどのようなものが存在しているかを考えましょう。

  1. ウィンド: コンテンツを表示する
  2. メッセージループ: メッセージがあるかどうか確認する
  3. メッセージ処理機: メッセージにより、具体的な処理を行う

簡単に言うと、ウィンドとメッセージシステムがあります。

コードの説明

ウィンドの登録と表示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37

UINT window_width = 1280;
UINT window_height = 720;

// WNDCLASSで、ウィンドの属性を表す
WNDCLASS wc;
wc.style = CS_HREDRAW | CS_VREDRAW;
wc.lpfnWndProc = MsgProc; // メッセージの処理機
wc.cbClsExtra = 0;
wc.cbWndExtra = 0;
wc.hInstance = hInstance;
wc.hIcon = LoadIcon (0, IDI_APPLICATION);
wc.hCursor = LoadCursor (0, IDC_ARROW);
wc.hbrBackground = (HBRUSH)GetStockObject (NULL_BRUSH);
wc.lpszMenuName = 0;
wc.lpszClassName = L"HelloWorld";

// ウィンドの登録
if (!RegisterClass (&wc)) {
MessageBox (0, L"RegisterClass Failed.", 0, 0);
return 0;
}

// ウィンドのサイズを再計算する
RECT rect = {0, 0, window_width, window_height};
AdjustWindowRect (&rect, WS_OVERLAPPEDWINDOW, false);

// ウィンドの作り
HWND hwnd = CreateWindow (L"HelloWorld", L"Hello World",
WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT,
rect.right - rect.left, rect.bottom - rect.top,
0, 0, hInstance, 0);

// ウィンドの表示
ShowWindow (hwnd, SW_SHOW);
// ウィンドの更新
UpdateWindow (hwnd);

メッセージの処理機

1
2
3
4
5
6
7
8
9
LRESULT CALLBACK MsgProc (HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) {
switch (msg) {
case WM_DESTROY:
PostQuitMessage (0); // WM_QUITというメッセージを発送
break;
}

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

メッセージループ

1
2
3
4
5
6
7
MSG msg = {};
while (msg.message != WM_QUIT) {
if (PeekMessage (&msg, 0, 0, 0, PM_REMOVE)) {
TranslateMessage (&msg); // キーボード等の操作をメッセージに訳する
DispatchMessage (&msg); // メッセージを発送
}
}

上のコードのような入力したら、WNDCLASSAWNDCLASSEXなどこういったキーワードに戸惑うかもしれません。公式の文書によりますと、WNDCLASSは既にWNDCLASSEXに置き換えられました。新Windowsの新機能はWNDCLASSEXだけ設定できます。また、WNDCLASSAWNDCLASSWのAとWの違いはプロジェクトがUnicodeを使用しているかどうかです。WNDCLASSWNDCLASSEXは自動的にUnicodeを使用しているかどうかによって変わるdefineです。つまり、自動的にAかWかを決めます。他のxxxEXxxxAxxxWはほとんど同じ意味です。

PeekMessage()の以外、GetMessage()が存在しています。GetMessage()はずっとメッセージを待っていて、アプリケーションの運行が止まります。PeekMessage()はシステムは今はメッセージがあるかを聞いて、運行が止まりません。

UpdateWindow()は一回のPaintを行って、WM_PAINTのメッセージを送ります。また、GetMessage()PeekMessage()を行うと、WM_PAINTを発想します。

AdjustWindowRect()はウィンドの境界やメニューバーなどによって、ウィンドのサイぞを再計算します。

デスクトップアプリケーション

全コード

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
LRESULT CALLBACK MsgProc (HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) {
switch (msg) {
case WM_DESTROY:
PostQuitMessage (0);
break;
}

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

int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE prevInstance, LPSTR cmdLine, int nCmdShow) {
WNDCLASS wc;
wc.style = CS_HREDRAW | CS_VREDRAW;
wc.lpfnWndProc = MsgProc;
wc.cbClsExtra = 0;
wc.cbWndExtra = 0;
wc.hInstance = hInstance;
wc.hIcon = LoadIcon (0, IDI_APPLICATION);
wc.hCursor = LoadCursor (0, IDC_ARROW);
wc.hbrBackground = (HBRUSH)GetStockObject (NULL_BRUSH);
wc.lpszMenuName = 0;
wc.lpszClassName = L"HelloWorld";

if (!RegisterClass (&wc)) {
MessageBox (0, L"RegisterClass Failed.", 0, 0);
return 0;
}

RECT rect = {0, 0, window_width, window_height};
AdjustWindowRect (&rect, WS_OVERLAPPEDWINDOW, false);

HWND hwnd = CreateWindow (L"HelloWorld", L"Hello World",
WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT,
rect.right - rect.left, rect.bottom - rect.top,
0, 0, hInstance, 0);

ShowWindow (hwnd, SW_SHOW);
UpdateWindow (hwnd);

MSG msg = {};
while (msg.message != WM_QUIT) {
if (PeekMessage (&msg, 0, 0, 0, PM_REMOVE)) {
TranslateMessage (&msg);
DispatchMessage (&msg);
}
}

UnregisterClass (wc.lpszClassName, wc.hInstance);
return 0;
}