“老师我C学了大半年控制台里的贪吃蛇都能写了能不能做个有窗口、有输入框、有按钮的程序”这几年我陆陆续续被问到过很多次类似的问题。每次我都会回答能而且根本不需要去啃MFC也不需要去装什么几百MB的GUI库你电脑里那个写着DEV-C的绿色小图标就够了。这篇文章我会带你用DEV-C配合Windows系统自带的Win32 API从零做一个真正带文本框和命令按钮的Windows窗口程序代码量控制在150行以内。你只要会基本的C语法——变量、函数、if判断——就能跟着做完。很多人搜“命令按钮”这个叫法其实是老教材里的翻译在Windows编程里它的真名叫BUTTON控件按钮文本框则对应EDIT控件。这两个控件都是系统预置好的不需要额外安装任何东西。我会先把窗口程序的骨架讲透再一步步把控件“种”进去最后附上我实际使用DEV-C时踩过的坑帮你少走弯路。1. 工具选择DEV-C的哪个版本更适合做Win32窗口程序1.1 老版Orwell与新版小熊猫Dev-C怎么选DEV-C这个名字有个历史遗留问题目前市面上流传最广的是Orwell Dev-C 5.11它大概是2015年停更的内置的编译器是TDM-GCC 4.9.2只支持到C11。而国内开发者维护的“小熊猫Dev-C”有时也写作小熊猫C其实一直在更新内置的GCC版本能切到9.x甚至11.x对C17、C20的支持都很好。选择标准很简单如果只是为了做本文这种Win32 API窗口程序两个版本都能跑代码没有任何区别。但老版本有一个老毛病默认新建的源文件编码和中文系统下GCC的宽字符转换配合得不好写L中文很容易乱码需要手动把文件另存为ANSI编码后才能正常显示。小熊猫Dev-C对UTF-8的支持就好很多界面也更现代Windows 10/11下高分屏缩放不乱。我的建议是直接用新版小熊猫Dev-C。下载后是个压缩包解压就能用不需要安装。老版本在Windows 11上也不是不能跑但偶尔缺DLL、显示模糊没必要为难自己。界面风格虽然和经典老版有点区别但菜单布局基本继承不会迷路。1.2 装好之后先检查这两项打开Dev-C之后不用急着写代码先确认两件事一是文件编码设置。老版本在“工具”-“编辑器选项”里把默认编码改成ANSI或者每次新建文件后用“文件-另存为”手工选择编码ANSI。新版本默认UTF-8保持默认即可。二是新建项目时不要选错类型。很多新手直接点“新建源代码文件”就开始写窗口程序最后发现入口函数对不上满屏的编译错误。正确的做法是点“文件-新建-项目”在向导里选择Windows Application有的版本叫Win32 GUI。这一步会自动帮你把链接参数配置好本文第5章你会看到选错有多坑。2. 窗口骨架WinMain、WNDCLASS与消息循环之间的关系2.1 WinMain是窗口程序的入口控制台程序的入口是main而窗口程序的入口是WinMain。这是Windows系统规定的链接器会先找WinMain。WinMain有四个参数int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)这四个参数分别代表当前程序实例的句柄、前一个程序实例句柄这个基本没用传NULL、命令行参数、窗口初始显示方式。最常用的是hInstance和nCmdShow。hInstance可以理解为这个程序在系统里的身份ID后面注册窗口类、创建窗口都要用到它。nCmdShow决定窗口一开始是正常显示、最大化还是最小化通常原样传给ShowWindow就行。注意这里入口函数的名字是固定的WinMain大小写都不能错。如果你新建源代码文件系统默认模板给的入口是main直接编译WinMain程序就会报错因为两者不是同一个函数。2.2 WNDCLASS注册窗口类就是给窗口“上户口”很多人第一次接触Win32会被WNDCLASS劝退觉得它结构体字段太多。其实它干的事特别简单先把窗口的“长相关键信息”登记下来比如窗口的消息处理函数是谁、背景色是什么、鼠标光标长什么样、窗口类名叫什么。然后调用RegisterClass告诉系统“我定义好了一个窗口类型”。你可以把WNDCLASS理解为菜谱RegisterClass就是把这个菜谱交给厨房备案CreateWindow则是照着菜谱真正端出一盘菜。一个菜谱可以端出很多盘菜也就是一个窗口类可以创建很多个外观完全相同的窗口。WNDCLASSW wc {0}; wc.lpfnWndProc WndProc; // 窗口过程函数 wc.hInstance hInstance; // 程序实例句柄 wc.hCursor LoadCursorW(NULL, IDC_ARROW); wc.hbrBackground (HBRUSH)(COLOR_WINDOW 1); // 窗口背景色 wc.lpszClassName LFirstWinApp; // 这个窗口类的名字 RegisterClassW(wc);这里我先用WNDCLASSW这个宽字符版本因为它和后面控件的中文显示密切相关。为什么不用WNDCLASS因为Dev-C默认没定义UNICODE宏用WNDCLASS的话类名就得写窄字符串L中文这种宽字符串就配不上后续麻烦不断。全程序统一用带W后缀的API和宽字符是Win32开发里最不容易出错的做法。窗口中最重要的字段是lpfnWndProc它指向一个回调函数。这个函数就是我们自己写的WndProc负责处理窗口收到的所有消息。窗口被拖动、被点击、被关闭系统都会往这个窗口“发消息”消息最终会来到WndProc函数里。2.3 CreateWindow那长长的一串参数创建窗口的代码是让新手头大的第一道坎。CreateWindowW的参数确实多但逐个拆开并不难HWND hWnd CreateWindowW( LFirstWinApp, // 窗口类名 L第一个窗口程序, // 窗口标题 WS_OVERLAPPEDWINDOW, // 窗口样式 CW_USEDEFAULT, CW_USEDEFAULT, 340, 180, // 位置和大小 NULL, NULL, // 父窗口、菜单 hInstance, // 程序实例句柄 NULL // 附加参数 );前两个参数是“用哪个类创建”和“标题显示什么”。第三个参数WS_OVERLAPPEDWINDOW是一组样式的组合包含标题栏、系统菜单、最小化/最大化按钮、可拖动边框是最常用的主窗口样式。中间四个数字是X坐标、Y坐标、宽度、高度坐标是相对于屏幕左上角CW_USEDEFAULT让系统自己选个合适位置。最后传NULL的部分是父窗口句柄和菜单主窗口没有父窗口所以是NULL。到这里窗口只是“创建”出来了它还没出现在屏幕上。还要通过ShowWindow把它显示出来然后调用UpdateWindow让窗口内容立即重绘ShowWindow(hWnd, nCmdShow); UpdateWindow(hWnd);2.4 消息循环窗口程序为什么“卡”在这里还能运行最后的循环是窗口程序的核心机制MSG msg; while (GetMessageW(msg, NULL, 0, 0)) { TranslateMessage(msg); DispatchMessageW(msg); } return (int)msg.wParam;GetMessageW从消息队列里取一条消息。如果队列为空程序就会停在这里等待不会占CPU。取到消息后TranslateMessage负责把键盘消息翻译成字符消息DispatchMessageW则把消息派发给对应窗口的WndProc函数。这就像银行的叫号系统系统不停叫号窗口过程根据号码办理不同业务。鼠标点一下按钮系统往窗口线程的消息队列里塞一条WM_COMMAND消息GetMessage取出DispatchMessage派发最后进了我们自己写的WndProc函数里处理。窗口程序的运行本质就是“取消息-分派消息-处理消息”的无限循环直到收到WM_QUIT消息GetMessage返回0循环退出。2.5 空窗口的完整代码把上面的片段串起来再加上最简单的WndProc就是一个能运行的空窗口程序#include windows.h LRESULT CALLBACK WndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam) { switch (msg) { case WM_DESTROY: PostQuitMessage(0); return 0; default: return DefWindowProcW(hWnd, msg, wParam, lParam); } } int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) { WNDCLASSW wc {0}; wc.lpfnWndProc WndProc; wc.hInstance hInstance; wc.hCursor LoadCursorW(NULL, IDC_ARROW); wc.hbrBackground (HBRUSH)(COLOR_WINDOW 1); wc.lpszClassName LFirstWinApp; RegisterClassW(wc); HWND hWnd CreateWindowW( LFirstWinApp, L第一个窗口程序, WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, 340, 180, NULL, NULL, hInstance, NULL); ShowWindow(hWnd, nCmdShow); UpdateWindow(hWnd); MSG msg; while (GetMessageW(msg, NULL, 0, 0)) { TranslateMessage(msg); DispatchMessageW(msg); } return (int)msg.wParam; }WM_DESTROY消息表示窗口正在被销毁此时调用PostQuitMessage(0)向消息队列投递一个WM_QUIT消息循环才会退出。其他消息一律交给DefWindowProcW做默认处理。这个骨架是Win32编程的地基建议先把这个程序跑起来再继续往下加控件。3. 控件的本质用CreateWindow创建文本框和按钮3.1 控件也是窗口只不过“穿着不同的衣服”Win32里有一个非常重要的概念文本框、按钮、标签这些控件本质上都是窗口。系统内部已经注册好了几个预定义的窗口类比如EDIT、BUTTON、STATIC、LISTBOX、COMBOBOX。我们用CreateWindow来创建一个编辑框和创建一个主窗口用的是同一个函数只不过窗口类名从自定义的“FirstWinApp”换成了系统预置的“EDIT”。这就能解释很多现象控件可以用SetWindowText设置文字、可以用MoveWindow移动位置因为这些本来就是窗口的基础操作。理解这一点Win32控件学习成本瞬间砍半——你只需要会“开一个窗口”就会创建所有控件区别只在于类名、样式和回调函数不同。3.2 为什么控件要放在WM_CREATE消息里创建我见过很多新手尝试在主窗口CreateWindow之后、ShowWindow之前连续调用两三次CreateWindow创建控件。代码能跑但有个问题控件创建得早主窗口还没完全准备好父窗口句柄只能直接用hWnd填如果哪天代码顺序乱了一点就容易出错。更规范的做法是在WM_CREATE消息里创建控件。WM_CREATE是主窗口创建完成后、显示之前收到的第一条消息此时主窗口句柄hWnd已经可用控件创建后就能立即绑定到父窗口上而且可以确定控件一定在窗口被用户看到之前创建完成。在WM_CREATE里怎么拿到hInstance因为hInstance并不是直接传给WndProc的但lParam参数里带着一个CREATESTRUCT结构体它的hInstance字段就是创建主窗口时传入的程序句柄。所以代码是这样case WM_CREATE: { HINSTANCE hInst ((LPCREATESTRUCT)lParam)-hInstance; // 在这里创建文本框和按钮 break; }3.3 定义控件ID门牌号必须有窗口程序里多个控件都发消息给同一个父窗口父窗口靠什么区分是谁发的答案是通过控件ID。创建控件时把ID作为CreateWindow的菜单参数传进去没错但菜单参数在子窗口里被重新解释成ID后续在WM_COMMAND消息里就能通过ID识别出哪个控件“举手”了。ID一般用#define定义方便阅读#define IDC_TEXTBOX 1001 #define IDC_BTN_SHOW 1002数值从1000开始是我个人的习惯避免和系统保留ID冲突。3.4 创建文本框和按钮的完整代码在WM_CREATE里补上创建控件的代码case WM_CREATE: { HINSTANCE hInst ((LPCREATESTRUCT)lParam)-hInstance; // 文本框 CreateWindowW( LEDIT, L, WS_CHILD | WS_VISIBLE | WS_BORDER | ES_LEFT, 20, 20, 260, 26, hWnd, (HMENU)IDC_TEXTBOX, hInst, NULL ); // 按钮 CreateWindowW( LBUTTON, L获取内容, WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON, 20, 60, 100, 30, hWnd, (HMENU)IDC_BTN_SHOW, hInst, NULL ); break; }创建任何一个控件四个要素缺一不可类名LEDIT表示编辑框LBUTTON表示按钮。样式WS_CHILD | WS_VISIBLE这两个是必须的WS_CHILD表示“我是子窗口”WS_VISIBLE表示“立刻显示”。除此之外EDIT加WS_BORDER给一个边框备选ES_LEFT左对齐按钮加BS_PUSHBUTTON表示普通按钮。父窗口所有控件都需要一个父窗口这里填的是主窗口的hWnd。控件ID用(HMENU)IDC_TEXTBOX这样的强制类型转换因为CreateWindow函数设计年代久远这个参数原本是菜单句柄。坐标尺寸那四个数20, 20, 260, 26表示控件左上角距离父窗口客户区左上角的水平距离、垂直距离、宽度、高度。注意这个坐标系是相对于父窗口客户区即窗口内部不含标题栏而言的。不少新手把控件“弄丢”了就是因为坐标写成负数或者超过窗口大小窗口被挡住看不见控件。3.5 这次一定能跑的完整带控件版本把之前的空窗口代码加上WM_CREATE控件创建部分主窗口宽度高度改成340和180就能看到一个带文本框和按钮的窗口程序。按钮“获取内容”当前还没有任何反应下一步就处理它的点击。4. 让按钮真正“干活”WM_COMMAND与文本内容读写4.1 WM_COMMAND消息的数据结构按钮被点击时系统会向它的父窗口发送WM_COMMAND消息。这个消息的参数关系是这样的wParam的低16位控件ID告诉我们点的是谁。wParam的高16位通知码告诉我们控件发生了什么按钮常见的是BN_CLICKED被点击。lParam控件的窗口句柄HWND。所以判断按钮被点击最严谨的写法是case WM_COMMAND: { if (HIWORD(wParam) BN_CLICKED LOWORD(wParam) IDC_BTN_SHOW) { // 处理按钮点击 } break; }很多老教程只判断LOWORD(wParam)忽略通知码功能上往往也能跑但不够规范。因为同一个控件可能发来多种通知比如编辑框会发EN_CHANGE内容变化、EN_SETFOCUS获得焦点如果只判断ID不判断通知码很可能在不该触发的时候执行了你不想执行的代码。4.2 用GetWindowText拿回文本框里的内容想要读取文本框中的文字需要两步。第一步根据之前定义的控件ID拿到文本框的窗口句柄HWND hEdit GetDlgItem(hWnd, IDC_TEXTBOX);GetDlgItem这个名字容易让人误会它虽然带Dlg字样但对普通窗口同样有效作用就是“在指定父窗口里按ID找子窗口的句柄”。第二步调用GetWindowTextW把文字拷贝到自己的缓冲区里wchar_t buf[256] {0}; GetWindowTextW(hEdit, buf, 256);GetWindowTextW的第二个参数是接收文字的缓冲区第三个参数是缓冲区最多接收的字符数。缓冲区先全部初始化为0这样即使读取结果不满256个字符末尾也一定是宽字符串的结束符不会出乱码。最后用MessageBoxW把文字弹出来MessageBoxW(hWnd, buf, L你输入的是, MB_OK);整理一下WM_COMMAND分支的完整代码就是case WM_COMMAND: { if (HIWORD(wParam) BN_CLICKED LOWORD(wParam) IDC_BTN_SHOW) { wchar_t buf[256] {0}; HWND hEdit GetDlgItem(hWnd, IDC_TEXTBOX); GetWindowTextW(hEdit, buf, 256); MessageBoxW(hWnd, buf, L你输入的是, MB_OK); } break; }到这里第一个“带文本框和命令按钮的Windows窗口程序”已经完整成立在文本框里输入内容点按钮弹窗显示输入内容。标题里要求的功能全部实现。4.3 更实用的示范做一个文本框加法计算器MessageBox只是弹窗演示实际项目里更多是把结果显示在窗口上的标签控件STATIC里。STATIC是静态文本控件同样可以用SetWindowTextW设置内容所以常被当作“显示区域”用。我们来做一个加法计算器两个文本框输入数字一个按钮触发计算一个静态文本显示结果。定义ID#define IDC_EDIT1 1001 #define IDC_EDIT2 1002 #define IDC_CALC 1003 #define IDC_LABEL 1004WM_CREATE里创建四个控件case WM_CREATE: { HINSTANCE hInst ((LPCREATESTRUCT)lParam)-hInstance; CreateWindowW(LEDIT, L, WS_CHILD | WS_VISIBLE | WS_BORDER, 20, 20, 100, 26, hWnd, (HMENU)IDC_EDIT1, hInst, NULL); CreateWindowW(LEDIT, L, WS_CHILD | WS_VISIBLE | WS_BORDER, 130, 20, 100, 26, hWnd, (HMENU)IDC_EDIT2, hInst, NULL); CreateWindowW(LBUTTON, L相加, WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON, 20, 60, 80, 30, hWnd, (HMENU)IDC_CALC, hInst, NULL); CreateWindowW(LSTATIC, L结果, WS_CHILD | WS_VISIBLE, 20, 100, 220, 25, hWnd, (HMENU)IDC_LABEL, hInst, NULL); break; }按钮点击后的处理逻辑是分别读取两个编辑框的文字转成整数相加后把结果拼成宽字符串再用SetWindowTextW显示到静态文本上。case WM_COMMAND: { if (HIWORD(wParam) BN_CLICKED LOWORD(wParam) IDC_CALC) { wchar_t buf1[128] {0}, buf2[128] {0}, result[128] {0}; HWND hEdit1 GetDlgItem(hWnd, IDC_EDIT1); HWND hEdit2 GetDlgItem(hWnd, IDC_EDIT2); GetWindowTextW(hEdit1, buf1, 128); GetWindowTextW(hEdit2, buf2, 128); int a _wtoi(buf1); int b _wtoi(buf2); swprintf(result, 128, L%d %d %d, a, b, a b); HWND hLabel GetDlgItem(hWnd, IDC_LABEL); SetWindowTextW(hLabel, result); } break; }_wtoi是宽字符字符串转整数的函数返回值是int。swprintf是宽字符格式化函数用法和printf一样只是第一个参数要传目标缓冲区。如果输入的文本不是数字_wtoi返回0程序不会崩溃体验还算友好。这个例子虽然简单但它涵盖了Win32界面程序最核心的交互闭环用户输入、程序处理、界面输出。你可以把它扩展成乘法、字符串拼接、字符计数等各种小工具原理完全一致。4.4 SetWindowText的“反向”应用SetWindowTextW是Win32里出场率最高的函数之一。除了给窗口标题、静态文本设置内容它还能给文本框设置内容。比如程序启动时想在文本框里显示默认文字可以在WM_CREATE创建完控件后调用HWND hEdit GetDlgItem(hWnd, IDC_EDIT1); SetWindowTextW(hEdit, L请输入第一个数);也就是说读写控件文字都靠这两个函数GetWindowTextW负责拿SetWindowTextW负责放。后面操作复选框、单选按钮、组合框时判断选中状态用的则是SendMessage配合BM_GETCHECK、CB_GETCURSEL等消息但读写文本始终是这两个函数打天下。5. DEV-C独有的编译坑从报错到运行的排查实录5.1 undefined reference to WinMain16这是我见过最多的报错几乎每个初学窗口编程的人都会栽一次。完整的报错是[Error] ld returned 1 exit status undefined reference to WinMain16WinMain16里的16意思是WinMain有4个参数__stdcall调用约定下参数总大小是16字节。链接器在全工程里找不到这个入口函数于是报错。原因很简单你新建项目时选的是Console Application控制台程序这种程序的入口函数是main不是WinMain。你写的WinMain在链接器眼里只是一个普通的自定义函数链接器自然会去找main结果找不到。解决方法是在DEV-C里打开“项目”菜单下的“项目属性”把“类型Type”改成Windows Application或Win32 GUI。老版本设置里的具体名称可能有点区别但找类型这个选项就不会错。改了之后重新编译这个报错就消失。如果你是用命令行g手动编译需要在编译命令里加-mwindowsg main.cpp -o app.exe -mwindows5.2 编译过了但运行时会弹一个黑乎乎的控制台窗口在控制台项目类型下编译窗口程序有时程序能跑但会额外弹出一个黑色控制台窗口非常影响体验。窗口程序使用的是GUI子系统不应该有控制台窗口。原因还是在项目配置项目类型没设置成Windows Application。控制台程序默认链接时带上控制台子系统标记系统启动时会先创建一个控制台窗口。设置了Windows Application后连接器会加上-mwindows参数告诉系统这是一个纯GUI程序不再创建控制台窗口。如果你想用命令行手动指定就是上面提到的-mwindows参数。在DEV-C图形界面里项目属性类型改对了之后会自动带上这个参数不用手动写。5.3 中文全部乱码用老版本DEV-C写L中文这类宽字符串编译能通过但运行起来所有中文都变成乱码甚至有些版本连编译都通不过。这个坑的根源是编码不匹配。DEV-C 5.11默认按系统locale把UTF-8无BOM的源文件误判成GBK导致宽字符串里的中文字符在转换时变成了一堆无效字节。解决方式有两个第一个也是老版本最稳妥的把源文件另存为ANSI编码。在DEV-C编辑器的“文件-另存为”对话框里有个Encoding下拉框选择ANSI后保存重新编译即可。ANSI在中文系统上就是GBK编码和系统locale一致宽字符串转换完全正常。第二个直接换成小熊猫Dev-C它默认用UTF-8宽字符串转换不需要额外处理。这也是我推荐新版本的原因之一。还有一种情况即使编码正确代码里混用了宽窄字符也会出问题。比如用L字符串写宽字符串却调用MessageBoxA版本A的窄字符APIA版本只能接收窄字符串编译器会报参数类型错误。所以编码问题最好从源头解决老版本ANSI 宽字符串API或者新版本UTF-8 宽字符串API不要混用。5.4 运行时报缺少DLL老版本DEV-COrwell 5.11在部分Windows 10/11电脑上编译出的exe双击后弹窗提示缺少libgcc_s_dw2-1.dll或libstdc-6.dll。这其实是GCC运行时库的问题不是代码的问题。默认情况下编译出的程序动态链接到GCC的运行时DLL而这些DLL在目标电脑上没有。解决办法有三个第一个在DEV-C的“工具-编译器选项”里把编译优化参数里加一条静态链接选项-static-libgcc -static-libstdc这样程序会把运行库编进exe里体积大一点但任何电脑都能直接跑。第二个把DEV-C安装目录下的libgcc_s_dw2-1.dll、libstdc-6.dll等DLL复制到exe同目录下。这是临时办法适合不修改编译设置的场景。第三个换小熊猫Dev-C。新版本默认的处理方式通常更合理少了这类问题。5.5 一个值得养成的排查习惯报错信息其实已经把线索给出得很清楚了。看到“undefined reference to”这类英文别慌它说的是“找不到某个符号”。第一步看得失的符号是什么WinMain就是入口没找对CreateWindowW就是链接库不对或是函数名拼错第二步看前面有没有源文件名和行号定位到具体哪一行。窗口程序编译链接阶段的问题90%集中在这几个点上入口函数、窗口类名拼写、项目类型、链接参数。养成从报错信息反推配置问题的习惯能省下大量网上搜索的时间。6. 窗外的视野文本框按钮之外的扩展方向6.1 控件样式速查读懂样式的组合规则后你就能快速拼出各种不同外观的控件。下面是我常用的一个速查表按场景罗列了样式组合控件类名想要的形态需要加的样式EDIT单行输入框WS_CHILD | WS_VISIBLE | WS_BORDEREDIT多行输入框WS_CHILD | WS_VISIBLE | ES_MULTILINE | ES_AUTOVSCROLL | WS_VSCROLLEDIT密码输入框WS_CHILD | WS_VISIBLE | WS_BORDER | ES_PASSWORDBUTTON普通按钮WS_CHILD | WS_VISIBLE | BS_PUSHBUTTONBUTTON复选框WS_CHILD | WS_VISIBLE | BS_CHECKBOXSTATIC普通标签WS_CHILD | WS_VISIBLESTATIC带边框标签WS_CHILD | WS_VISIBLE | WS_BORDER组合方式就是把需要的样式用“|”连接比如多行文本框需要先有ES_MULTILINE让它变成多行再有ES_AUTOVSCROLL让内容超过高度时自动滚动最后加WS_VSCROLL显示竖直滚动条缺一个体验都不完整。6.2 在这条路上继续进发的几个方向把文本框和按钮玩熟之后可以按这个路线继续第一个方向是把控件数量增加做一个“表单填写工具”。加入复选框BS_CHECKBOX和单选按钮BS_AUTORADIOBUTTON学会通过SendMessage发BM_GETCHECK来读取选中状态。这一步能把消息机制的运用范围拓宽到“不只是文本”。第二个方向是做菜单。在窗口上挂一个菜单栏需要先创建一个菜单资源或动态添加菜单项然后处理WM_COMMAND里菜单项的ID。Windows里菜单项也是通过WM_COMMAND上报的只是没有通知码一说ID直接放在LOWORD(wParam)里这套逻辑你已经学过了。第三个方向是学习DrawItem自绘控件。Win32自带控件的样式有限想做出好看的界面就要自绘。自绘需要处理WM_DRAWITEM、WM_MEASUREITEM等消息难度上一个台阶但对GUI原理的理解会有质的提升。6.3 关于学习路线的个人建议我不建议一上来就抱着某个重量级框架死磕。MFC、Qt、wxWidgets的底层都是对Win32 API的封装如果你已经亲手用CreateWindow创建过窗口和控件亲手写过消息循环再打开Qt文档时看到“信号与槽”这类抽象概念就会有一种“原来这就是把消息循环包装了一下”的踏实感。反之如果完全没碰过原生API学任何框架都容易有种“悬空”感代码跑起来了却不知道背后发生了什么。把本文这个简单的窗口程序吃透Windows编程的门就算真正推开了一半。剩下的一半无非就是控件种类的增多、消息类型的增多、界面排版的精细化管理底层机制和100行代码时并无两样。