原文地址:pratikone.github.io/c++/2020/06…html
原文做者:twitter.com/pratikoneios
2020年6月7日c++
自從引入Win32 api後,Windows編程就處於不斷變化的狀態--不管是移到.NET,仍是再次移到WPF,引入Modern Apps後又改爲UWP,如今終因而Project Reunion。這其中有一點是徹底沒有改變的,那就是如何在Windows中寫一個hello world程序。自1995年Windows 95發佈以來,它一直沒有改變。可是,內部發生了不少變化。Windows爲了確保簡單的hello world在25年後和大規模的操做系統變化後還能繼續工做,在下面作了不少工做。本篇博客試圖深刻到hello世界中去,用一段歷史來展現windows hello world豐富的技術背景。git
Win32 api是系統級的api,用於對Windows進行最底層的編程。當Windows 95過渡到32位系統時,他們但願有一種方法來區分這些新的32位api和現有的只有16位的Windows api。這就是這些新的api選擇Win32這個名字的緣由。Windows 95大受歡迎,不少開發者開始使用這些api來開發Windows。這些api越是流行,重命名就越是困難。若是把64位的Windows改爲Win64,或者直接改爲Windows Api,就會引發更多的混亂,也會修改文件,把32從名字中去掉。Win32這個名字被卡住了。如今每個Windows api都是Win32,無論它是32位、64位仍是將來的任何位數。github
自Win32的hello world代碼誕生以來,除了那個臭名昭著的3頁長的hello world程序由於對初學者來講太過嚇人而被一個較短的版本所取代外,其餘的代碼基本沒有變化。這個解剖學不是那個臭名昭著的代碼,而是自Win95時代以來的繼任者,它的尺寸至關大,並且自己就抓住了不少Windows功能。讓咱們先把這個hello world和它的控制檯對應的代碼進行比較。編程
#include <iostream>
int main() {
std::cout << "Hello World!\n";
}
複製代碼
這是很直接的。你有IO頭,切入點是main(),它調用std命名空間中的cout來打印hello world。比較一下Win32的hello world。這段代碼直接來自MSDN官方的Win32 hello world系列教程,未做改動。windows
#ifndef UNICODE
#define UNICODE
#endif
#include <windows.h>
LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)。
int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE, PWSTR pCmdLine, int nCmdShow)
{
const wchar_t CLASS_NAME[] = L "Sample Window Class";
WNDCLASS wc = { };
wc.lpfnWndProc = WindowProc.hInstance; wc.hInstance = hInstance; wc.lpfnWndProc = { }; wc.lpfnWndProc = WindowProc;
wc.hInstance = hInstance。
wc.lpszClassName = CLASS_NAME;
RegisterClass(&wc);
// 建立窗口。
HWND hwnd = CreateWindowEx(
0,//可選窗口樣式。
CLASS_NAME, // 窗口類別
L "學習Windows編程", // 窗口文本
WS_OVERLAPPEDWINDOW, // 窗口樣式。
// 尺寸和位置
CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT。
NULL, // 父窗口
NULL, //菜單
hInstance, // Instance handle
NULL // 附加應用數據
);
if(hwnd == NULL)
{
return 0。
}
ShowWindow(hwnd, nCmdShow)。
// 運行消息循環。
MSG msg = { };
while (GetMessage(&msg, NULL, 0, 0))
{
TranslateMessage(&msg).DispatchMessage(&msg);。
DispatchMessage(&msg);
}
return 0。
}
LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)。
{
switch (uMsg)
{
case WM_DESTROY.PostQuitMessage(0);。
PostQuitMessage(0);
return 0。
case WM_PAINT.PostQuitMessage(0); return 0; {
{
PAINTSTRUCT ps.HDC hdc = BeginPaint(hwnd, &ps);。
HDC hdc = BeginPaint(hwnd, &ps);
FillRect(hdc, &ps.rcPaint, (HBRUSH) (COLOR_WINDOW+1));
EndPaint(hwnd, &ps);
}
return 0。
}
return DefWindowProc(hwnd, uMsg, wParam, lParam);
}
複製代碼
乍一看,這看起來大得多,並且沒必要要的複雜。但它展現了不少建立GUI窗口應用程序的功能。它啓動了一個典型的矩形UI窗口,充滿了背景顏色。它有一個功能性的用戶界面,適當的鍵盤和鼠標支持,甚至事件處理。與其餘平臺的hello world程序不一樣,它在教程和第一個程序以外沒有什麼用處,這個hello world是任何Win32巨型代碼庫的基礎。它教會了你在Windows上編程所需的全部基本東西。不管是Photoshop仍是Windows的Firefox,它們的巨型代碼庫中都會有這段代碼。 如今,它已經被淘汰了,讓咱們按照這段代碼逐塊進行學習。api
#ifndef UNICODE
#define UNICODE
#endif
#include <windows.h>
複製代碼
多年來,Windows的編程經歷了很大的變化,好比Win95的api從16位到32位,XP的NT內核,以及後來Vista和Windows 8的變化。無論是1995年仍是2008年開發的應用程序,只要調用windows.h,均可以在最新版本的Windows中繼續工做(極好的向後兼容性)。Windows.h和Win32 apis作了不少繁重的工做,以確保全部這些應用程序保持兼容,即便它們都包含相同的頭。例如,若是你正在開發一個新的應用程序,其中使用了一個遺留的組件(來自Windows 95時代,指的是那個時代的windows.h頭文件),那麼你的新組件和遺留組件有可能會使用相同的windows.h(來自最新的Windows SDK),但卻可使用適合時代的功能。app
它是一個包含了大多數常見的Windows系統調用的頭文件。Windows.h,自己就是一個小文件,爲多個頭文件進行了前向聲明。對於這段hello world代碼,apis在winuser.h中,使用user32.dll連接。對於任何其餘功能,可能須要一些其餘的頭文件。Windows.h經過做爲全部這些頭文件的 「路由器」使其變得簡單。你只須要包含windows.h頭,你就能夠獲得全部這些功能。正如MSDN頁面提到的,你能夠經過定義一些全局標識符來仔細選擇不一樣的或較小的功能子集,好比這裏的UNICODE表示使用特定apis的unicode變體,用W表示。這裏介紹了CreateFoo
如何解釋爲CreateFooW
框架
int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE, PWSTR pCmdLine, int nCmdShow) {
const wchar_t CLASS_NAME[] = L "Sample Window Class";
WNDCLASS wc = { };
wc.lpfnWndProc = WindowProc.hInstance; wc.hInstance = hInstance; wc.lpfnWndProc = { }; wc.lpfnWndProc = WindowProc;
wc.hInstance = hInstance。
wc.lpszClassName = CLASS_NAME;
RegisterClass(&wc);
// 建立窗口。
HWND hwnd = CreateWindowEx(
0,//可選窗口樣式。
CLASS_NAME, // 窗口類別
L "學習Windows編程", // 窗口文本
WS_OVERLAPPEDWINDOW, // 窗口樣式。
// 尺寸和位置
CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT。
NULL, // 父窗口
NULL, //菜單
hInstance, // Instance handle
NULL // 附加應用數據
);
if(hwnd == NULL)
{
return 0。
}
ShowWindow(hwnd, nCmdShow)。
// 運行消息循環。
MSG msg = { };
while (GetMessage(&msg, NULL, 0, 0))
{
TranslateMessage(&msg).DispatchMessage(&msg);。
DispatchMessage(&msg);
}
return 0。
}
複製代碼
它是程序的入口點。若是須要的話,Win32容許使用/entry linker選項來選擇4個不一樣的入口點。一眼望去,函數中傳遞了不少0和NULL做爲參數。這些函數中的許多自Win32 apis誕生以來就已經存在,而且在行爲上發生了變化。所以,不少參數已經沒有任何做用,爲了兼容性而留下。這些參數老是NULL或0,對於其他的參數,標誌能夠與邏輯OR |相結合。
這段代碼使用了一個如今已通過時的匈牙利符號來命名變量,變量的類型是在名字前加上的。變量bvalue表示它是一個類型爲bool的變量。lpszClassName中的lpsz表明長指針(歷史上是16位指針,但在現代是普通的32位/64位指針)到字符串(以/0結尾)。lpfn中的lpfnWndProc表明函數指針。微軟建議不要在現代Windows編程中使用匈牙利符號,由於它增長的價值很小,卻讓代碼更難讀。Joel On Software也有一篇不錯的文章。
另外一個有趣的過去的產物是wParam和lParam參數。在Win95以前,Windows是16位的操做系統,wParam是WORD param,是16位的,lparam是LONG param,是32位的。Win95以後,Windows進入了32位時代,WORD和LONG如今都是32位的(或基於arch的64位),因此wParam和lParam之間沒有區別。 HINSTANCE是另外一個曾經的例子--一個實例的句柄。Raymond Chen的這篇博文很好地解釋了背後的緣由。
「很明顯,窗口是Windows的核心。它們是如此重要,以致於他們用它們來命名操做系統。」 - MSDN官方引語
窗口是屏幕上的一個矩形區域,它接受用戶的輸入,並以文本和圖形的形式顯示輸出。在Win32編程慣例中,一個窗口用HWND--窗口的句柄來引用。句柄是一個與該窗口相關聯的數值。在內核中,每個窗口都會被建立一個具備惟一id的對象。若是一個窗口是一我的,hwnd就是它的名字。
按照電影《竊聽風雲》的精神,典型的hwnd中的每個UI元素自己就是一個hwnd(想一想遞歸)。這是用子窗口/自有窗口實現的。這就致使了在一箇中等複雜的應用程序中會有不少hwnd。
現代Windows HWNDs是硬件加速的,即利用圖形管道(如Direct2D),使用GPU更快地繪製像素。在現代Windows中,桌面窗口管理器(DWM)處理繪製像素。
窗口註冊和窗口消息系統是一些讓人想起80年代面向對象(OO)設計的代碼。微軟很早就搭上了OO的列車,即便它尚未被業界徹底接受。那時候微軟一直只用C語言來編程Windows。C語言的OO須要在Windows中選擇不少奇怪的設計,因爲向後兼容,不少設計一直保留到今天。
// 註冊窗口類。
const wchar_t CLASS_NAME[] = L "Sample Window Class";
WNDCLASS wc = { };
wc.lpfnWndProc = WindowProc.hInstance; wc.hInstance = hInstance; wc.lpfnWndProc = { }; wc.lpfnWndProc = WindowProc;
wc.hInstance = hInstance。
wc.lpszClassName = CLASS_NAME;
RegisterClass(&wc).wc.lpfnWndProc = WindowProc; wc.lpfnWndProc = hInstance; wc.lpszClassName = CLASS_NAME; RegisterClass(&wc);
複製代碼
Windows有一種奇怪的繼承方式。你必須向OS註冊你新建立的hwnd對象,它才能開始與之通訊。
// 建立窗口。
HWND hwnd = CreateWindowEx(
0,//可選窗口樣式。
CLASS_NAME, // 窗口類別
L "學習Windows編程", // 窗口文本
WS_OVERLAPPEDWINDOW, // 窗口樣式。
// 尺寸和位置
CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT。
NULL, // 父窗口
NULL, //菜單
hInstance, // Instance handle
NULL // 附加應用數據
);
if (hwnd == NULL)
{
return 0。
}
複製代碼
這段代碼建立了HWND。咱們提供了與這個HWND相關聯的窗口類(咱們剛剛註冊)。窗口建立api是很是豐富的,能夠建立100多個具備不一樣配置和行爲的窗口。一個很好的例子是建立子窗口。由於全部的東西都是一個窗口。一個UI窗口中的按鈕就是這個hwnd的子窗口。這意味着父窗口能夠處理該子窗口的消息處理。這種層次結構是很是有用的。它是OO範式中繼承概念的一種實現。經過這種方式,您能夠添加一個新窗口做爲父窗口的子窗口,而且您沒必要爲其基本操做(如調整大小、最大化、關閉等)編寫任何額外的代碼。
ShowWindow(hwnd, nCmdShow)。
複製代碼
不出所料,它在屏幕上顯示了窗口。顯式調用show window有什麼用?有不少狀況下,一個窗口是不能直接顯示的。它能夠用上面的命令建立,而後用UpdateWindow更新,當準備好後,顯示在屏幕上。也有AnimateWindow作90年代的PPT幻燈片同樣的過渡動畫,在現代社會,沒有人應該使用。
Windows消息系統是一個利用多態性實現的事件驅動系統。操做系統經過傳遞消息與程序進行交流,它會在你的程序中調用一個特殊的函數,讓你能夠選擇處理這些消息。這些消息能夠是與程序交互時產生的鍵盤、鼠標、觸摸事件,也能夠是操做系統建立的事件,好比當你的程序最小化、最大化或關閉時。對於這種消息傳遞模型,Windows爲一個線程建立了一個單一的消息隊列,處理該線程上建立的全部HWND的消息。
// 運行消息循環。
MSG msg = { };
while (GetMessage(&msg, NULL, 0, 0))
{
TranslateMessage(&msg).DispatchMessage(&msg);。
DispatchMessage(&msg);
}
複製代碼
消息循環代碼是負責將這個消息隊列歸檔的。每一個線程只能有一個消息循環。這個消息隊列是隱藏的,你的代碼沒法訪問。它徹底由操做系統處理。你的代碼能夠作的就是使用GetMessage()
api調用從這個隊列中移除最上面的消息。而後,這條消息會被翻譯成鍵盤輸入,這樣它就能夠處理快捷鍵和進行其餘鍵盤輸入處理(在這裏閱讀更多關於TranslateMessage的內容)。以後,它被派發處處理函數WndProc(下面討論)。GetMessage
是一個阻塞函數,因此若是循環爲空,它將會等待。但這並不意味着你的UI將是無響應的。一個替代的方法是PeekMessage
函數,它能夠偷看並判斷隊列頂部是否有消息。由於它不會阻塞,因此在 "Get "以前作一個 "Peek "對某些場景是有好處的。
LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)。
{
switch (uMsg)
{
case WM_DESTROY.PostQuitMessage(0);。
PostQuitMessage(0);
return 0。
case WM_PAINT.PostQuitMessage(0); return 0; {
{
PAINTSTRUCT ps.HDC hdc = BeginPaint(hwnd, &ps);。
HDC hdc = BeginPaint(hwnd, &ps);
FillRect(hdc, &ps.rcPaint, (HBRUSH) (COLOR_WINDOW+1));
EndPaint(hwnd, &ps);
}
return 0。
}
return DefWindowProc(hwnd, uMsg, wParam, lParam);
}
複製代碼
WndProc
是每一個Win32程序代碼中必須直接或間接存在的特殊函數。每當Windows操做系統須要與你的運行代碼進行任何通訊時,它都會調用這個函數。它一般有一個巨大的開關語句來處理窗口消息,好比WM_DESTROY
(當用戶點擊窗口右上角的小x時該怎麼作)。它能夠選擇忽略它,它不會關閉窗口。值得慶幸的是,還有其餘方法能夠關閉窗口。這顯示了Windows操做系統爲開發者提供的控制和靈活性水平,這多是有益的,但也可能被濫用,最近的Windows編程模型已經發展到了應對這個問題的程度。PostQuitMessage將WM_QUIT
消息添加到消息隊列中,這將致使GetMessage()
爲false,退出循環並退出程序。你的程序不必定要處理全部的消息。它能夠處理一些感興趣的特殊消息,而後調用DefWindowProc
——OS提供的默認處理程序來處理其他的消息。WndProc
能夠選擇處理一個線程中全部窗口的消息,也能夠把它們交給各自的WndProc處理。請參閱子類做爲動態多態的例子來實現這一點。
case WM_PAINT
內的代碼是在窗口中繪製任何東西的模板代碼。Windows圖形驅動接口(GDI)是即時模式(連接到它)。不少新的UI庫,好比WPF和WinUI,由於內存和性能的緣由,都是保留模式的GUI框架。MSDN的Painting the Window - Win32 apps對這個代碼作了很好的解釋。
MSVC是首選的編譯器。它能夠用Visual Studio編譯,也能夠在終端使用msbuild編譯。它須要kernel32.dll、user32.dll、gdi32.dll和/SUBSYSTEM:WINDOWS。是的,從Windows NT開始,子系統的概念就已經存在了(由於Windows原本應該以子系統的形式運行OS/2,但沒有實現)。這對30多年後Windows推出Windows Subsystem for Linux(WSL)頗有幫助。
謝謝你能走到這一步。這個帖子的想法是在我開始學習Win32 apis的時候產生的。MSDN在解釋api方面作得很好,但除此以外,不多有文章和博客存在。這篇文章的目的就是爲了改善這一點。 若是你發現博客上有什麼錯誤,或者有什麼其餘建議,請在twitter上告訴我。
經過www.DeepL.com/Translator(免費版)翻譯