原文地址: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(免费版)翻译