C++ Win32 创建窗口 —— 基于 main 入口的完整拆解
C Win32 创建窗口 —— 基于 main 入口的完整拆解在 Windows 下用 C 写 GUI最底层、最直接的方式就是调 Win32 API。这套接口从 Windows 1.0 就有所有高级框架MFC、Qt、WPF的窗口最终都逃不过调用它。这篇文章只用main入口不涉及 MFC 或任何第三方库代码粘贴到 Visual Studio 的“控制台空项目”中直接就能编译运行。窗口只实现基础功能显示、拖拽、缩放、最小化、最大化、关闭没有任何多余业务逻辑。完整代码先看全貌// win32_main_window.cpp#includewindows.h#includestdio.h// 窗口过程函数声明LRESULT CALLBACKWndProc(HWND hwnd,UINT uMsg,WPARAM wParam,LPARAM lParam);intmain(){// 控制台输出方便调试时看程序走到哪一步printf( 控制台启动正在创建 Win32 窗口...\n);// 1. 获取当前实例句柄// 在 main 入口中没有 hInstance 参数必须通过 GetModuleHandle 获取HINSTANCE hInstanceGetModuleHandle(NULL);// 2. 设计窗口类WNDCLASSWNDCLASS wc{0};wc.lpfnWndProcWndProc;// 窗口过程函数处理所有消息wc.hInstancehInstance;// 实例句柄wc.lpszClassNameLMainWindowClass;// 类名创建窗口时用wc.hbrBackground(HBRUSH)(COLOR_WINDOW1);// 背景刷系统默认窗口颜色// 3. 注册窗口类if(!RegisterClass(wc)){printf(注册窗口类失败\n);return-1;}// 4. 创建窗口HWND hwndCreateWindowEx(0,// 扩展样式无LMainWindowClass,// 窗口类名必须与注册时一致LWin32 主窗口,// 窗口标题WS_OVERLAPPEDWINDOW,// 窗口样式标题栏、边框、系统菜单、最大/最小化按钮CW_USEDEFAULT,CW_USEDEFAULT,// X、Y 坐标让系统自动选位置800,600,// 宽度、高度NULL,// 父窗口句柄无NULL,// 菜单句柄无hInstance,// 实例句柄NULL// 额外创建参数无);if(!hwnd){printf(创建窗口失败\n);return-1;}// 5. 显示并刷新窗口ShowWindow(hwnd,SW_SHOW);UpdateWindow(hwnd);printf(窗口已显示进入消息循环...\n);// 6. 消息循环 —— 程序的核心驱动力MSG msg;while(GetMessage(msg,NULL,0,0)0){TranslateMessage(msg);// 将按键消息转换为字符消息DispatchMessage(msg);// 将消息派发给窗口过程函数}printf(窗口已关闭程序正常退出\n);return0;}// 7. 窗口过程函数 —— 处理所有发送给该窗口的消息LRESULT CALLBACKWndProc(HWND hwnd,UINT uMsg,WPARAM wParam,LPARAM lParam){switch(uMsg){// 用户点击了关闭按钮标题栏的 ×caseWM_DESTROY:PostQuitMessage(0);// 向消息队列发送 WM_QUIT让 GetMessage 返回 0return0;// 系统要求重绘窗口例如窗口被遮挡后重新显示caseWM_PAINT:{PAINTSTRUCT ps;HDC hdcBeginPaint(hwnd,ps);// 如果这里有绘图代码就写在这里// 目前我们什么都不画只调用 BeginPaint/EndPaint 配对告诉系统已处理EndPaint(hwnd,ps);return0;}// 其他所有消息拖拽、缩放、最小化、最大化等交给系统默认处理default:returnDefWindowProc(hwnd,uMsg,wParam,lParam);}}分步拆解逐行讲清楚第一步入口函数与实例句柄intmain(){HINSTANCE hInstanceGetModuleHandle(NULL);为什么这里要用GetModuleHandle(NULL)拿实例句柄WinMain会把hInstance作为参数传进来但main没有这个参数。GetModuleHandle(NULL)返回的就是当前可执行文件的基地址在 Win32 里就是实例句柄。必须拿到它因为注册窗口类和创建窗口时都要传给系统。控制台的printf可以帮你观察程序的启动和退出时机对调试非常有帮助。第二步设计窗口类WNDCLASSWNDCLASS wc{0};wc.lpfnWndProcWndProc;wc.hInstancehInstance;wc.lpszClassNameLMainWindowClass;wc.hbrBackground(HBRUSH)(COLOR_WINDOW1);WNDCLASS是一个结构体描述了一类窗口的共有属性。我们只填充了四个关键字段其他字段如style、hIcon、hCursor全部置 0使用系统默认值。字段作用lpfnWndProc窗口过程函数指针。所有消息绘制、键盘、鼠标、关闭等都会调用这个函数。这是窗口的灵魂。hInstance实例句柄表明这个类属于哪个进程。lpszClassName类的名字。后续CreateWindowEx靠这个名字找到它。hbrBackground背景画刷。COLOR_WINDOW 1是系统预设的标准窗口背景色通常是白色。如果不设置WM_PAINT没处理好的话窗口背景全是乱码。第三步注册窗口类if(!RegisterClass(wc)){printf(注册窗口类失败\n);return-1;}RegisterClass把设计好的类告诉操作系统。如果类名已经被其他程序注册、或者参数非法函数返回 0。注册成功后这个类就可以用来创建窗口了。第四步创建窗口CreateWindowExHWND hwndCreateWindowEx(0,LMainWindowClass,LWin32 主窗口,WS_OVERLAPPEDWINDOW,CW_USEDEFAULT,CW_USEDEFAULT,800,600,NULL,NULL,hInstance,NULL);CreateWindowEx是真正生成窗口的函数它返回一个窗口句柄HWND后续所有对该窗口的操作都靠这个句柄。参数说明扩展样式0 表示无扩展样式。可以设置WS_EX_TOOLWINDOW等这里不需要。类名必须与RegisterClass注册时的名字完全一致。窗口标题显示在标题栏的文字。窗口样式WS_OVERLAPPEDWINDOW是最常用的样式包含标题栏、系统菜单、边框、最大化按钮、最小化按钮、关闭按钮。你要求的“拖拽、关闭、最小化、最大化”全在这里。X、Y、宽、高CW_USEDEFAULT让系统自动选初始位置后面 800、600 是窗口尺寸。父窗口、菜单都是NULL表示没有父窗口也没有菜单。实例句柄传入当前进程的hInstance。额外参数NULL表示不需要向窗口过程传递额外的创建数据。第五步显示窗口ShowWindow(hwnd,SW_SHOW);UpdateWindow(hwnd);ShowWindow让窗口可见。第二个参数SW_SHOW表示按正常大小显示。UpdateWindow会立即发送一个WM_PAINT消息给窗口过程强制刷新窗口客户区避免窗口显示时是一片未初始化的垃圾。第六步消息循环 —— 驱动窗口的引擎MSG msg;while(GetMessage(msg,NULL,0,0)0){TranslateMessage(msg);DispatchMessage(msg);}Windows 程序是消息驱动的。操作系统把键盘敲击、鼠标点击、窗口重绘、关闭请求等所有事件都转换成消息放进程序的消息队列里。消息循环就是从队列里不断取消息然后分发给对应的窗口过程去处理。GetMessage从消息队列中取一条消息。如果没有消息线程会阻塞在这里不占 CPU。当取到WM_QUIT时返回 0退出循环。TranslateMessage把键盘按键消息WM_KEYDOWN转换成字符消息WM_CHAR让程序能正确处理文字输入。我们虽然不处理文字输入但保留它是标准写法。DispatchMessage把消息交给窗口过程函数WndProc去处理。为什么GetMessage的返回值要判断 0因为出错时返回 -1正常返回非 0收到WM_QUIT返回 0。严谨的写法是判断 0而不是! 0。第七步窗口过程函数WndProcLRESULT CALLBACKWndProc(HWND hwnd,UINT uMsg,WPARAM wParam,LPARAM lParam){switch(uMsg){caseWM_DESTROY:PostQuitMessage(0);return0;caseWM_PAINT:{PAINTSTRUCT ps;HDC hdcBeginPaint(hwnd,ps);EndPaint(hwnd,ps);return0;}default:returnDefWindowProc(hwnd,uMsg,wParam,lParam);}}窗口过程是一个回调函数系统在发生各种事件时会调用它。参数uMsg是消息编号wParam和lParam是消息附带的额外数据。我们只处理两个消息消息触发时机我们的处理WM_DESTROY用户点击标题栏的关闭按钮×调用PostQuitMessage(0)向消息队列扔一个WM_QUIT让GetMessage返回 0结束消息循环程序退出。WM_PAINT窗口需要重绘初次显示、被遮挡后恢复、尺寸改变等调用BeginPaint获取绘图设备上下文然后EndPaint释放。两者成对出现告诉系统“我已处理完重绘请求”。如果这里什么都不画窗口背景就是注册时指定的COLOR_WINDOW 1白色。最关键的是default分支里的DefWindowProc。拖拽标题栏移动窗口、拖动边框缩放、点击最小化/最大化按钮——这些功能全在DefWindowProc里。我们不需要写一行代码实现它们系统默认都处理好了。如果你把default分支删掉直接返回 0窗口就会变成一个僵尸拖不动、缩不了、关不掉因为WM_NCLBUTTONDOWN等消息没人处理。入口函数变体对比main / wmain / WinMain虽然这篇文章主推main入口但实际项目中三种写法都有人用。它们本质上都是 Win32 程序区别仅在于入口函数签名和子系统配置。入口函数控制台黑框子系统要求实例句柄来源适用场景int main()✅ 有CONSOLE默认GetModuleHandle(NULL)调试阶段需要看printf日志或不想改项目配置int wmain()✅ 有CONSOLE默认GetModuleHandle(NULL)需要宽字符命令行参数wchar_t* argv[]int WINAPI WinMain()❌ 无WINDOWS需手动改直接由参数传入纯 GUI 应用发布版无黑框选择建议如果你用 Visual Studio 新建“控制台空项目”main版本直接粘贴就能跑不用动任何设置。如果你不想看到黑框用WinMain版本但需要把“链接器 → 系统 → 子系统”改成“窗口 (/SUBSYSTEM:WINDOWS)”。常见疑问QWM_PAINT里为什么非要写BeginPaint/EndPaintA如果WM_PAINT里什么都不调用系统会认为你没处理这个消息它会持续发送WM_PAINT导致 CPU 飙升。BeginPaint会验证窗口的无效区域EndPaint告诉系统该区域已更新两者必须成对出现。QPostQuitMessage(0)的参数 0 是什么含义A这是程序退出码会传递给GetMessage返回的msg.wParam最终作为main的返回值。Q为什么我关闭窗口后控制台还卡着不退出A检查WM_DESTROY里有没有写PostQuitMessage。如果漏了GetMessage永远不会收到WM_QUIT消息循环不会结束控制台就会一直挂着。文章末尾的完整代码可直接复制// Win32 窗口 —— main 入口无 MFC无第三方库// Visual Studio新建控制台空项目粘贴此文件直接编译运行#includewindows.h#includestdio.hLRESULT CALLBACKWndProc(HWND,UINT,WPARAM,LPARAM);intmain(){printf( 控制台启动创建窗口...\n);HINSTANCE hInstanceGetModuleHandle(NULL);WNDCLASS wc{0};wc.lpfnWndProcWndProc;wc.hInstancehInstance;wc.lpszClassNameLMainWindowClass;wc.hbrBackground(HBRUSH)(COLOR_WINDOW1);if(!RegisterClass(wc)){printf(注册窗口类失败\n);return-1;}HWND hwndCreateWindowEx(0,LMainWindowClass,LWin32 主窗口,WS_OVERLAPPEDWINDOW,CW_USEDEFAULT,CW_USEDEFAULT,800,600,NULL,NULL,hInstance,NULL);if(!hwnd){printf(创建窗口失败\n);return-1;}ShowWindow(hwnd,SW_SHOW);UpdateWindow(hwnd);printf(进入消息循环...\n);MSG msg;while(GetMessage(msg,NULL,0,0)0){TranslateMessage(msg);DispatchMessage(msg);}printf(程序退出\n);return0;}LRESULT CALLBACKWndProc(HWND hwnd,UINT uMsg,WPARAM wParam,LPARAM lParam){switch(uMsg){caseWM_DESTROY:PostQuitMessage(0);return0;caseWM_PAINT:{PAINTSTRUCT ps;HDC hdcBeginPaint(hwnd,ps);// 什么都不画EndPaint(hwnd,ps);return0;}default:returnDefWindowProc(hwnd,uMsg,wParam,lParam);}}总结Win32 API 创建窗口的套路非常固定总共就四个步骤注册类 → 创建窗口 → 显示窗口 → 消息循环。窗口过程只需要处理WM_DESTROY和WM_PAINT其余全部丢给DefWindowProc。不依赖任何外部库系统自带的user32.dll和kernel32.dll就够用了。