Win32API入门简介
Win32API是用于Microsoft Windows操作系统应用程序开发的最基础的API。Win32API提供了一系列的C语言函数、结构体、宏定义、常量等,允许开发者创建Windows桌面应用程序并与操作系统进行交互。Win32API提供了访问Windows操作系统底层功能的基础接口,包括窗体管理、图形设备接口(GDI)、键盘鼠标输入处理、文件和文件系统操作、网络通信等。
Win32API在Windows平台上的应用十分广泛,但它相对底层,需要开发者处理许多细节,Win32API并不“难”,但是内容多而杂。随着时间的推移,微软也推出了若干种更高层次的框架,如.NET Framework、Windows Presentation Foundation(WPF)等,使开发者更容易创建Windows窗体应用程序。然而,Win32API仍然是许多传统Windows应用程序和系统级开发的基础。无论你使用哪种图形界面框架,只要在Windows下开发窗体程序,它的底层就绕不开Win32API,即使上层框架采用自绘控件,创建窗口仍然还是调用Win32API实现的。我们学习Win32API不是真的要完全用它写桌面软件,而是作为一种对Windows底层实现细节的了解。
本系列笔记主要学习如何使用C语言调用Win32API的方式编写Windows桌面应用程序,以此对Windows的上层应用架构进行简单了解。微软的开发环境通常有着非常完善的文档,我们遇到问题查阅MSDN即可。
MSDN文档:https://learn.microsoft.com/en-us/windows/win32/
注:笔记内容参考的书籍为《Windows程序设计第五版珍藏版》,由于原书作者使用的操作系统还是Windows98,珍藏版出版时Windows7才刚刚发布,但得益于Win32API的良好兼容性,在最新的Windows操作系统和Visual Studio集成开发环境中运行书中的范例代码也问题不大,不过这里还是会删减一些已经严重过时的内容,关于书中对16位兼容的相关内容这里也不再过多提及。
基本概念
在具体使用Win32API前,我们还需要大致理解一些基本概念。
控制台程序和Win32窗体程序
控制台程序我们都能理解,运行后会弹出一个黑漆漆的终端,里面可能打印一些写到标准输出的文本;而Win32程序不同,Win32程序是窗体程序,它不会弹出任何终端,也没有标准输出的概念了,Win32程序通常会调用Win32API注册窗体类并显示一个窗体。
在代码层面我们知道,一个可执行的C语言程序必然有一个入口函数,对于控制台工程来说,入口函数是我们熟知的main,然而Win32程序的入口函数则不同,它是WinMain。Win32程序除了依赖C语言标准库,它还会调用Win32API,这些API依赖于Windows操作系统的几个核心动态链接库。
什么是Win32API
Win32API的本质是一组用于访问Windows操作系统核心功能的应用程序编程接口。它提供了一组标准C语言接口,使应用程序能够与Windows操作系统进行交互,利用其提供的各种服务和功能。Win32API的具体实现包含在Windows操作系统中,涉及操作系统内核以及一系列的系统库和动态链接库,具体来说,它们的实现位于C:\Windows\System32下的kernel32.dll、user32.dll、gdi32.dll等。我们开发Win32程序,实际上就是调用这些动态链接库中的各种功能接口。
什么是句柄(Handle)
Win32编程中,我们经常使用一个概念叫“句柄”。句柄是一种用来间接代表某个内核对象的整数值。内核对象是Windows操作系统管理的一些重要的资源,比如文件,线程,进程,窗口等。句柄不是指针,不能直接访问对象的内部数据,而是通过操作系统提供的API函数来操作对象。句柄是一个比较有创意的设计,使用句柄而非指针的原因有以下几点:
- 隐藏了内核对象的实现细节,保护了对象的安全性和完整性,防止用户程序随意修改对象的状态。
- 使得内核对象可以跨进程共享,通过句柄表的映射,不同的进程可以使用同一个句柄来访问同一个对象。
- 使得内核对象可以动态分配和回收,操作系统可以根据需要移动或销毁对象,而不影响句柄的有效性。
- 使得内核对象可以与不同的编程语言兼容,因为句柄只是一个整数,而不是一个特定的数据类型。
保证兼容性
Win32API中使用typedef自定义了大量的数据类型,我们实际开发中也推荐使用Win32API中定义的数据类型,而非C语言的标准数据类型,这是出于兼容性考虑。使用Win32API的数据类型,当API发生变化时,它能最大保证我们的代码仍能正常编译运行。在之后的例子代码中,我们会深刻理解这一点。
Hello world
实际开发中我们一般都是使用Visual Studio开发Win32程序,但IDE隐藏了很多细节,这里我们直接编写一个最简单的例子程序,并通过最底层的命令行方式手动调用MSVC的工具链进行编译和链接,来体验Win32窗体程序的开发和构建流程。我们这里准备了如下3个文件作为示例。
|_ main.c // C源代码文件
|_ main.rc // 资源脚本文件
|_ main.ico // 一个ico格式的图片,用于设置exe可执行程序的图标
编写源码
main.c
#include <windows.h>
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nShowCmd)
{
MessageBox(NULL, TEXT("Hello"), TEXT("Hello, Win32API!"), MB_OK);
return 0;
}
这段代码虽然简短,但其中包含了不少信息值得我们关注。
首先是入口函数WinMain,它的返回值是int类型,即程序退出的返回值。WINAPI则是_stdcall的宏定义,WindowsAPI的函数约定都是使用stdcall形式进行参数传递的(注:stdcall是一种函数调用约定,发生函数调用时,其参数从左至右通过栈传递)。函数的具体参数如下:
- hInstance:唯一标识我们当前程序运行实例的句柄
- hPrevInstance:用于16位程序,现已不用该参数,永远是NULL,因历史原因保留
- lpCmdLine:运行该程序的命令行参数字符串
- nShowCmd:指定程序起始时如何显示,是最大化、正常显示、还是最小化到任务栏
其次我们的程序引用了windows.h头文件。Win32API是很庞大的一组API,其中有大量的自定义数据类型、函数等,我们的程序中要使用这些内容就要引入对应的头文件,Win32API实际上有许多头文件,不过我们开发时一般引入windows.h就行了,其余的头文件都会间接的通过windows.h引入。
在函数体中,我们用到了MessageBox函数,它用于创建一个对话框。以下是MSDN文档中对MessageBox的定义。
int MessageBox(
HWND hWnd,
LPCTSTR lpText,
LPCTSTR lpCaption,
UINT uType
);
- hWnd:拥有该对话框的父窗口,没有就传NULL
- lpText:对话框显示的文字
- lpCaption:对话框的标题
- uType:对话框的行为,文档中有一系列十六进制代码定义了这些行为,如代码中用到的
MB_OK表示该对话框拥有一个确认按钮,多个行为可以使用按位与|进行连接
调用MessageBox函数后,我们的主线程将阻塞等待其返回。
上面代码中,我们还用到了TEXT()写法,它实际上是一个宏定义,有关字符编码的内容将在后续章节详细介绍。这里我们可以简单理解,C语言中对字符和宽字符的编写方式是不同的,调用的字符串处理函数也不同,Win32API提供的TEXT宏能够根据编译选项自动处理这些区别,以保证我们的程序在多种语言的操作系统下的兼容性。
main.rc
1 ICON main.ico
main.rc是一个资源文件,它实际上也是一个脚本,其中包含了引入的资源内容,我们这里引入了一个ico格式的图标文件。在实际开发中,资源文件脚本由Visual Studio维护,我们不需要手动编写,这里只是作为一个示例,我们不需要理解它的语法。
编译和链接
编译和链接需要使用MSVC工具链,一般来说,安装Visual Studio后这些工具就已经安装好了。Visual Studio通常不会将MSVC工具链加入环境变量,但我们也无需手动设置任何环境变量,我这里使用的是Visual Studio 2022,以该版本的Visual Studio为例,我们可以直接点击开始菜单中的Developer PowerShell for VS 2022终端,该终端默认会包含MSVC工具链的环境变量。
MSVC中具体的C/C++编译器是cl.exe,首先我们执行以下命令,编译C语言源代码。
cl.exe main.c -c
注意cl.exe是基于文件的扩展名判断使用C还是C++语法来编译源码的,因此我们要正确的设置源码文件的扩展名。此时如果编译成功将生成一个main.obj目标文件,-c参数类似Linux下的GCC编译器,用于生成中间目标文件。
然后我们执行以下命令编译资源文件,rc.exe是MSVC中的资源文件编译器。
rc.exe main.rc
如果编译成功将生成一个main.res文件,它也是一个二进制文件,其中包含了ico图片的数据。
编译资源文件这个操作可能对于Linux下开发的程序员比较陌生,在Windows编程中,资源文件可以编译成二进制格式然后链接到可执行文件中,这是出于提升资源的加载性能,增加程序的便携性,保护知识产权,以及简化部署等目的。当然,你实际上可以不编译链接任何资源到可执行文件,仍然从磁盘加载资源,但这不是微软推荐的方式。
最后,我们使用链接器link.exe将中间文件和user32.dll链接,生成最终的可执行程序。我们代码中调用了MessageBox函数,该Win32API的具体实现实际上就位于user32.dll中。不过这里注意Windows下链接动态链接库时需要使用导入库,因此这里我们命令中使用的参数是user32.lib,但实际链接的是user32.dll(Linux下没有导入库的概念,这和Windows下开发有一些区别)。
link.exe main.obj main.res user32.lib
运行Win32窗体程序

此时,我们就可以看到最终生成的可执行程序main.exe了,双击该程序,会弹出一个对话框。

此时,我们就手动完成了一个Win32应用程序的编写。
使用Visual Studio开发Win32程序
前面我们手动调用MSVC工具链编译链接了一个Win32可执行程序,这只是为了展示Win32程序编译链接的底层流程,实际开发中当然这一切都不需要也没必要手动去做,强大的Visual Studio会帮助我们完成这一切,我们唯一所要做的就是编写源码和点击“运行”按钮。
在Visual Studio中,我们所要创建的项目类型是Windows桌面应用程序。

创建好后,项目中通常会包含一些基础的示例代码,我们在其之上修改即可。
关于输出调试信息
前面我们说过,Win32窗体程序中没有标准输出的概念,也没有控制台终端,如果我们需要在程序开发阶段打印一些调试信息,总不能全部以MessageBox形式输出吧。这里我们介绍一个调试技巧,它能够给Win32程序分配一个终端。
我们首先定义一个全局变量,它是一个终端缓冲区的句柄。
HANDLE hConsoleOutput = 0;
然后我们分配终端并设置hConsoleOutput的值。
// 分配终端
AllocConsole();
hConsoleOutput = GetStdHandle(STD_OUTPUT_HANDLE);
// ...
// 程序关闭时释放终端
FreeConsole();
在需要打印日志的地方,我们调用如下代码写入数据到终端即可。
LPCSTR str = TEXT("测试输出");
WriteConsole(hConsoleOutput, str, wcslen(str), NULL, NULL);
WriteConsole函数的参数分别为:hConsoleOutput终端缓冲区句柄,lpBuffer写入数据的缓冲区通常是为字符串,nNumberOfCharsToWrite要写入的字符数,lpNumberOfCharsWritten可选的用于接收写入字符数(它是一个out参数),lpReserved保留参数固定传NULL。