Hello Windows
文章目錄
如果要從空項目開始,需要額外設置:調試 -> <解決方案名> 屬性… -> 鏈接器 -> 系統 -> 子系統,下拉設置爲窗口(/SUBSYSTEM:WINDOWS)。
前綴 P- 和 LP- 等效,這是歷史遺留問題。
經常會稱一些函數的聲明爲 function signature(函數簽名)。
窗口程序的總體結構:
- 運行窗口程序會創建至少一個線程;
- 一個線程可以有多個窗口;
- 線程負責接收並分發消息——在一個循環中調用 DispatchMessage,該函數會調用目標窗口的窗口進程(並將消息通過其參數傳遞給它);
- 窗口進程中可以通過一個 switch 結構處理得到的消息,可以關閉窗口甚至關閉進程(接收到 WM_DESTROY 信息時調用 PostQuitMessage 函數)。
這篇文章其實算不上翻譯,更像是筆記,但是有不少地方是直接翻譯過來的,所以就當是翻譯吧。對了,翻譯中有少數句子借鑑了谷歌翻譯,不過大部分都進行了修改。
另外,有些地方會有些捨棄和修改,建議對照原文閱讀。
原文地址:Module 1. Your First Windows Program
字符和字符串
類型定義
typedef | definition |
---|---|
CHAR | char |
PSTR 或 LPSTR | char* |
PCSTR 或 LPCSTR | const char* |
PWSTR 或 LPWSTR | wchar_t* |
PCWSTR 或 LPCWSTR | const wchar_t* |
Unicode 和 ANSI 函數
Windows SDK 通過檢查是否定義了 UNICODE 宏來決定將宏(函數等)解析爲哪個版本,例如:
#ifdef UNICODE
#define SetWindowText SetWindowTextW
#else
#define SetWindowText SetWindowTextA
#endif
Windows SDK 提供了根據平臺將字符串映射到Unicode或ANSI的宏。
宏 | Unicode | ANSI |
---|---|---|
TCHAR | wchar_t |
char |
TEXT(“ x”) | L"x" |
"x" |
結合以上兩者,可以將代碼:
SetWindowText(TEXT("My Application"));
自動轉換爲以下兩者之一:
SetWindowTextW(L"My Application"); // Unicode function with wide-character string.
SetWindowTextA("My Application"); // ANSI function.
微軟 C run-time libraries 中具有類似的機制,如:
#ifdef _UNICODE
#define _tcslen wcslen
#else
#define _tcslen strlen
#endif
注意:有些頭文件使用 UNICODE 宏,而有些使用 _UNICODE。創建新項目時,Visual C++ 默認將兩者都進行定義。
窗口
UI controls(UI 控件) 也是窗口。
窗口關係:Parent Windows 和 Owner Windows
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-jCQGcvqw-1588473180902)(1. Hello Windows.assets/window04.png)]
父窗口:爲子窗口提供座標系,子窗不能超出父窗口顯示等。
所有者窗口:父窗口的一些行爲會對被所有的窗口產生影響,如最小化、關閉等。
Windows Handles
Windows 是對象(同時有代碼和數據),程序通過使用 handle 來引用 Windows 對象。handle 本質上是操作系統用來標識對象的數字(操作系統維護了一個包含了所有已創建窗口的表)。注意句柄不是指針!
窗口句柄的數據類型爲 HWND,由函數 CreateWindow 和 CreateWindowEx 返回。
座標
WinMain:應用程序入口點
每個Windows程序都包含一個名爲 WinMain 或 wWinMain 的入口點函數。
除了 WinMain 接受的命令行參數被視爲 ANSI 編碼外,WinMain 與 wWinMain 等價。
int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, PWSTR pCmdLine, int nCmdShow);
INT WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR lpCmdLine, INT nCmdShow)
{
return 0;
}
四個參數:
- hInstance 被稱爲“實例的句柄”或“模塊的句柄”。當可執行文件(EXE)加載到內存中時,操作系統使用該值來標識該可執行文件(EXE)。Windows 的某些功能需要實例句柄,例如,加載圖標或位圖。
- hPrevInstance 沒有任何意義。它在 16 位 Windows 中使用,但現在始終爲零。
- pCmdLine 包含命令行參數作爲 Unicode/ANSI 字符串。
- nCmdShow 是一個標誌,指示是否將主應用程序窗口最小化,最大化或正常顯示。
返回值:返回一個 int 值,操作系統不使用這個值,但可以將其作爲狀態代碼傳達給用戶編寫的其他程序。
創建窗口
創建一個簡單的窗口:
// Register the window class.
const wchar_t CLASS_NAME[] = L"Sample Window Class";
WNDCLASS wc = { };
wc.lpfnWndProc = WindowProc;
wc.hInstance = hInstance;
wc.lpszClassName = CLASS_NAME;
RegisterClass(&wc); // register windows class
// Create the window.
HWND hwnd = CreateWindowEx(
0, // Optional window styles.
CLASS_NAME, // Window class
L"Learn to Program Windows", // Window text
WS_OVERLAPPEDWINDOW, // Window style
// Size and position
CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT,
NULL, // Parent window
NULL, // Menu
hInstance, // Instance handle
NULL // Additional application data
);
if (hwnd == NULL)
{
return 0;
}
ShowWindow(hwnd, nCmdShow);
窗口類
每一個窗口都必須與一個窗口類相關聯。這裏的類不同於 C++ 中的 class,而是操作系統內部使用的數據結構。窗口類在運行時需要向系統註冊,而要註冊則需先填充 WNDCLASS結構。
// Register the window class.
const wchar_t CLASS_NAME[] = L"Sample Window Class";
WNDCLASS wc = { };
wc.lpfnWndProc = WindowProc;
wc.hInstance = hInstance;
wc.lpszClassName = CLASS_NAME;
上面代碼中的三個結構成員是必須設置的:
- lpfnWndProc 是指向由應用程序定義的函數的指針,該函數稱爲 window procedure 或 window proc。window procedure 定義了窗口的大多數行爲。
- hInstance 是應用程序實例的句柄。從 wWinMain 的 hInstance 參數獲取此值。
- **lpszClassName **是標識窗口類的字符串。
注意用戶定義的窗口類的名稱必須不與其他用戶定義窗口類和標準 Windows 控件類的名稱衝突。
創建窗口
要創建窗口的新實例,請調用CreateWindowEx函數:
HWND hwnd = CreateWindowEx(
0, // Optional window styles.
CLASS_NAME, // Window class
L"Learn to Program Windows", // Window text
WS_OVERLAPPEDWINDOW, // Window style
// Size and position
CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT,
NULL, // Parent window
NULL, // Menu
hInstance, // Instance handle
NULL // Additional application data
);
if (hwnd == NULL)
{
return 0;
}
窗口消息
GUI應用程序必須響應來自用戶和操作系統的事件。
- 來自用戶的事件包括某人與您的程序進行交互的所有方式:鼠標單擊,按鍵,觸摸屏手勢等。
- 操作系統中的事件包括程序“外部”的所有可能影響程序行爲的內容。例如,用戶可能插入了新的硬件設備,或者Windows可能進入了低功耗狀態(睡眠或休眠)。
Windows使用了消息傳遞模型。操作系統通過將消息傳遞給您的應用程序窗口與之通信。
#define WM_LBUTTONDOWN 0x0201
一些消息具有與之關聯的數據。例如,WM_LBUTTONDOWN 消息包括鼠標光標的x座標和y座標。
消息循環
對於每個創建一個窗口的線程,操作系統都會爲其創建並維護一個消息隊列,這個隊列保存了發送給該線程創建的所有窗口的消息。
用戶不能直接操控消息隊列,但是可以使用 GetMessage 函數從中提取消息。
MSG msg;
GetMessage(&msg, NULL, 0, 0);
GetMessage 的第一個參數是 MSG 結構的地址。如果函數成功,它將使用有關消息的信息填充 MSG 結構。這包括目標窗口和消息代碼。其他三個參數可以過濾從隊列中收到的消息。在幾乎所有情況下,都可以將這些參數設置爲零。
TranslateMessage(&msg);
DispatchMessage(&msg);
- TranslateMessage 函數與鍵盤輸入。它將擊鍵(按下鍵,按下鍵)轉換爲字符。您實際上不必知道此功能的工作原理。只要記住在 DispatchMessage 之前調用它即可。
- DispatchMessage 函數告訴操作系統來調用消息的目標窗口的窗口程序。換句話說,操作系統在其窗口表中查找窗口句柄(與一個稱爲 window procedure 的函數相關聯,在填充窗口類時指定),找到與該窗口關聯的函數指針,然後調用該函數。
操作系統在 DispatchMessage 函數中調用 window procedure ,並返回到其中。爲了持續接收和處理消息,定義了一個循環:
while (GetMessage(&msg, NULL, 0, 0))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
一般情況下,GetMessage 返回一個非零數,如果要向操作系統請求終止線程,可以調用 PostQuitMessage 函數。
PostQuitMessage(0);
該函數將 WM_QUIT 消息放到消息隊列中。WM_QUIT 可以使 GetMessage 返回零。
Posted Messages Vs Sent Messages
- Posted Messages:這類消息被放入消息隊列,並由目標窗口的消息循環處理。
- Sent Messages:這類消息被直接發送給窗口,繞過了消息隊列。
編寫窗口過程
DispatchMessage 會調用信息的目標窗口的 window procedure 來處理信息。window procedure 的函數簽名如下:
LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam);
window procedure 的名稱不必是 WindowProc,可以由用戶自行決定。
四個參數:
-
hwnd:當前窗口句柄,可以用來操控當前窗口,如:
SetWindowText(hwnd, L"Hello World!");
-
uMsg:消息代碼;例如,WM_SIZE 消息在窗口大小被調整後(窗口大小數據改變,但窗口顯示的大小需要由這時的 WindowProc 函數處理)發送給該窗口。
-
wParam 和 lParam:包含與該消息有關的其他數據。確切含義取決於消息代碼。
返回值:
- LRESULT 類型的值,是程序返回給 Windows 的一個整型值。
- CALLBACK is the calling convention for the function. 回調函數可以不用 CALLBACK 修飾,CALLBACK 只不過是一種calling convention,用來說明一個函數是Windows API函數。
典型的窗口過程只是處理消息代碼的大型 switch 語句,爲要處理的每條消息添加 case 即可。
switch (uMsg)
{
case WM_SIZE: // Handle window resizing
// etc
}
一種更加模塊化的處理方式是將用於處理每個消息的邏輯放在單獨的函數中。
LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
switch (uMsg)
{
case WM_SIZE:
{
int width = LOWORD(lParam); // Macro to get the low-order word.
int height = HIWORD(lParam); // Macro to get the high-order word.
// Respond to the message:
OnSize(hwnd, (UINT)wParam, width, height);
}
break;
}
}
void OnSize(HWND hwnd, UINT flag, int width, int height)
{
// Handle resizing
}
LOWORD 和 HIWORD 宏從 lParam 獲得的16位寬度和高度值。每個消息都有其處理方式,可以參考 MSDN。
默認的消息處理:
return DefWindowProc(hwnd, uMsg, wParam, lParam);
避免在窗口進程中進行過長的處理
當窗口進程運行時,它會阻止所有傳遞給由同一個線程創建的窗口。這時的其他窗口不能處理輸入、不能刷新,甚至不能關閉。
要進行耗時長的操作,考慮使用以下幾種方法中的一種將工作轉移:
- 創建一個新線程;
- 使用線程池(thread pool);
- Use asynchronous I/O calls;
- Use asynchronous procedure calls.
繪製窗口
Sometimes your program will initiate painting to update the appearance of the window. 其他時候,操作系統將通知您必須重新繪製窗口的一部分。發生這種情況時,操作系統將向該窗口發送 WM_PAINT 消息。窗口中必須繪製的部分稱爲 update region。
使用者只需負責 client 區域即可,其他部分由操作系統管理。
繪製結束後,清除 update region,這樣可以告訴操作系統不需要發送 WM_PAINT 發生了某些改變。
switch (uMsg)
{
case WM_PAINT:
{
PAINTSTRUCT ps;
HDC hdc = BeginPaint(hwnd, &ps);
// All painting occurs here, between BeginPaint and EndPaint.
FillRect(hdc, &ps.rcPaint, (HBRUSH) (COLOR_WINDOW+1));
EndPaint(hwnd, &ps);
}
return 0;
}
通過調用 BeginPaint 函數開始繪製。此函數用與重新繪製請求有關的信息填充 PAINTSTRUCT 結構體。當前的 update region 在 PAINTSTRUCT 結構體的 rcPaint 成員中給出。
繪製時有兩個基本選項:
- 繪製整個 client 區域,無視給定的 update region ,但是操作系統還是會忽略 update region 之外的區域。這樣可以使代碼更加簡單。
- 通過僅繪製在更新區域內的一部分來進行優化。當繪製邏輯很複雜時,跳過 update region 外的區域會使程序更有效率。
下面的代碼使用單色填充 update region,其中的 COLOR_WINDOW 取決於用戶當前使用的顏色主題。關鍵點在於第二個參數使用了 PAINTSTRUCT 結構體的 rcPaint 成員,也就是整個 update region(不一定是整個 client 區域)。
FillRect(hdc, &ps.rcPaint, (HBRUSH) (COLOR_WINDOW+1));
關閉窗口和窗口進程
- 通過點擊關閉按鈕或使用快捷鍵 Alt+F4 等會使窗口接收到 WM_CLOSE 信息,接下來進行的行爲可以自行定義。
- 實際關閉窗口需調用 DestroyWindow 函數。如果不想關閉窗口,可以直接
return 0;
。
case WM_CLOSE:
if (MessageBox(hwnd, L"Really quit?", L"My application", MB_OKCANCEL) == IDOK)
{
DestroyWindow(hwnd);
}
// Else: User canceled. Do nothing.
return 0;
另外,還可以讓 DefWindowProc 自行處理。對於 WM_CLOSE,DefWindowProc 處理方式是自動調用 DestroyWindow。
銷燬窗口時,WM_DESTROY 消息會被髮送到被銷燬窗口的窗口進程,然後發送到被銷燬窗口的子窗口(如果有)。通常將通過調用 PostQuitMessage 來響應 WM_DESTROY。
case WM_DESTROY:
PostQuitMessage(0);
return 0;
管理應用狀態
CreateWindowEx 函數提供了一種可以將任何數據結構傳遞到一個窗口的方法。調用此函數時,它將以下兩條消息按以下順序發送到您的窗口過程(還有其他消息,這裏不予討論):
這兩條消息在窗口變得可見(被繪製)之前就會被髮送出去,是初始化 UI 界面的好選擇。
傳遞應用狀態
首先,定義一個保存狀態信息的類或結構。
// Define a structure to hold some state information.
struct StateInfo {
// ... (struct members not shown)
};
調用 CreateWindowEx ,將此結構的指針傳遞給最後一個參數 LPVOID lpParam
。
StateInfo *pState = new (std::nothrow) StateInfo;
if (pState == NULL)
return 0;
// Initialize the structure members (not shown).
HWND hwnd = CreateWindowEx(
0, // Optional window styles.
CLASS_NAME, // Window class
L"Learn to Program Windows", // Window text
WS_OVERLAPPEDWINDOW, // Window style
// Size and position
CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT,
NULL, // Parent window
NULL, // Menu
hInstance, // Instance handle
pState // Additional application data
);
窗口進程收到 WM_NCCREATE 和 WM_CREATE 消息時,lParam 參數都是一個指向 CREATESTRUCT 結構的指針,而 CREATESTRUCT 結構中則包含了傳遞給 CreateWindowEx 函數的那個指針。
通過轉換 lpCreateParams 獲得指向自己定義的數據結構的 指針。
CREATESTRUCT *pCreate = reinterpret_cast<CREATESTRUCT*>(lParam);
pState = reinterpret_cast<StateInfo*>(pCreate->lpCreateParams);
接下來,調用 SetWindowLongPtr 函數以將指針傳遞給窗口。
SetWindowLongPtr(hwnd, GWLP_USERDATA, (LONG_PTR)pState);
可以將 GWLP_USERDATA 換成其他值,以實現其他功能。
這麼做的目的是將 StateInfo 指針存儲在窗口的實例數據中。完成此操作後,可以通過調用 GetWindowLongPtr 從窗口獲取指針:
LONG_PTR ptr = GetWindowLongPtr(hwnd, GWLP_USERDATA);
StateInfo *pState = reinterpret_cast<StateInfo*>(ptr);
以上功能的使用有些麻煩,可以將一些代碼封裝進一個小的輔助函數,如:
inline StateInfo* GetAppState(HWND hwnd)
{
LONG_PTR ptr = GetWindowLongPtr(hwnd, GWLP_USERDATA);
StateInfo *pState = reinterpret_cast<StateInfo*>(ptr);
return pState;
}
現在的窗口進程如下:
LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
StateInfo *pState;
if (uMsg == WM_CREATE)
{
CREATESTRUCT *pCreate = reinterpret_cast<CREATESTRUCT*>(lParam);
pState = reinterpret_cast<StateInfo*>(pCreate->lpCreateParams);
SetWindowLongPtr(hwnd, GWLP_USERDATA, (LONG_PTR)pState);
}
else
{
pState = GetAppState(hwnd);
}
switch (uMsg)
{
// Remainder of the window procedure not shown ...
}
return TRUE;
}
面對對象的方法
現在想要一種設計,以實現這樣的代碼使用方式:
// pseudocode
LRESULT MyWindow::WindowProc(UINT uMsg, WPARAM wParam, LPARAM lParam)
{
switch (uMsg)
{
case WM_SIZE:
this->HandleResize(...);
break;
case WM_PAINT:
this->HandlePaint(...);
break;
}
}
一個問題是,MyWindow::WindowProc
指向一個非靜態成員函數,也就是說,在編譯期間,它指向的函數並不存在。爲此,需要在類聲明中將其聲明爲靜態函數。以下是類模板:
template <class DERIVED_TYPE>
class BaseWindow
{
public:
static LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
DERIVED_TYPE *pThis = NULL;
if (uMsg == WM_NCCREATE)
{
CREATESTRUCT* pCreate = (CREATESTRUCT*)lParam;
pThis = (DERIVED_TYPE*)pCreate->lpCreateParams;
SetWindowLongPtr(hwnd, GWLP_USERDATA, (LONG_PTR)pThis);
pThis->m_hwnd = hwnd;
}
else
{
pThis = (DERIVED_TYPE*)GetWindowLongPtr(hwnd, GWLP_USERDATA);
}
if (pThis)
{
return pThis->HandleMessage(uMsg, wParam, lParam);
}
else
{
return DefWindowProc(hwnd, uMsg, wParam, lParam);
}
}
BaseWindow() : m_hwnd(NULL) { }
BOOL Create(
PCWSTR lpWindowName,
DWORD dwStyle,
DWORD dwExStyle = 0,
int x = CW_USEDEFAULT,
int y = CW_USEDEFAULT,
int nWidth = CW_USEDEFAULT,
int nHeight = CW_USEDEFAULT,
HWND hWndParent = 0,
HMENU hMenu = 0
)
{
WNDCLASS wc = {0};
wc.lpfnWndProc = DERIVED_TYPE::WindowProc;
wc.hInstance = GetModuleHandle(NULL);
wc.lpszClassName = ClassName();
RegisterClass(&wc);
m_hwnd = CreateWindowEx(
dwExStyle, ClassName(), lpWindowName, dwStyle, x, y,
nWidth, nHeight, hWndParent, hMenu, GetModuleHandle(NULL), this
);
return (m_hwnd ? TRUE : FALSE);
}
HWND Window() const { return m_hwnd; }
protected:
virtual PCWSTR ClassName() const = 0;
virtual LRESULT HandleMessage(UINT uMsg, WPARAM wParam, LPARAM lParam) = 0;
HWND m_hwnd;
};
上述的 BaseWindow
類是一個抽象基類。下面是一個簡單的派生類:
class MainWindow : public BaseWindow<MainWindow>
{
public:
PCWSTR ClassName() const { return L"Sample Window Class"; }
LRESULT HandleMessage(UINT uMsg, WPARAM wParam, LPARAM lParam);
};
調用 BaseWindow::Create
以創建窗口:
int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE, PWSTR pCmdLine, int nCmdShow)
{
MainWindow win;
if (!win.Create(L"Learn to Program Windows", WS_OVERLAPPEDWINDOW))
{
return 0;
}
ShowWindow(win.Window(), nCmdShow);
// Run the message loop.
MSG msg = { };
while (GetMessage(&msg, NULL, 0, 0))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
return 0;
}
純虛函數 BaseWindow::HandleMessage
方法用於實現窗口進程。
// LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
LRESULT MainWindow::HandleMessage(UINT uMsg, WPARAM wParam, LPARAM lParam)
{
switch (uMsg)
{
case WM_DESTROY:
PostQuitMessage(0);
return 0;
case WM_PAINT:
{
PAINTSTRUCT ps;
HDC hdc = BeginPaint(m_hwnd, &ps);
FillRect(hdc, &ps.rcPaint, (HBRUSH) (COLOR_WINDOW+1));
EndPaint(m_hwnd, &ps);
}
return 0;
default:
return DefWindowProc(m_hwnd, uMsg, wParam, lParam);
}
return TRUE;
}
由於窗口句柄保存在成員變量 m_hwnd
中,因此 BaseWindow::HandleMessage
只需要另外三個參數即可。