news 2026/9/30 1:40:13

从零用DEV-C++和Win32 API创建带文本框按钮的窗口程序

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
从零用DEV-C++和Win32 API创建带文本框按钮的窗口程序

“老师,我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,只支持到C++11。而国内开发者维护的“小熊猫Dev-C++”(有时也写作小熊猫C++)其实一直在更新,内置的GCC版本能切到9.x甚至11.x,对C++17、C++20的支持都很好。

选择标准很简单:

  • 如果只是为了做本文这种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 = L"FirstWinApp"; // 这个窗口类的名字 RegisterClassW(&wc);

这里我先用WNDCLASSW这个宽字符版本,因为它和后面控件的中文显示密切相关。为什么不用WNDCLASS?因为Dev-C++默认没定义UNICODE宏,用WNDCLASS的话,类名就得写窄字符串,L"中文"这种宽字符串就配不上,后续麻烦不断。全程序统一用带W后缀的API和宽字符,是Win32开发里最不容易出错的做法。

窗口中最重要的字段是lpfnWndProc,它指向一个回调函数。这个函数就是我们自己写的WndProc,负责处理窗口收到的所有消息。窗口被拖动、被点击、被关闭,系统都会往这个窗口“发消息”,消息最终会来到WndProc函数里。

2.3 CreateWindow那长长的一串参数

创建窗口的代码是让新手头大的第一道坎。CreateWindowW的参数确实多,但逐个拆开并不难:

HWND hWnd = CreateWindowW( L"FirstWinApp", // 窗口类名 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 = L"FirstWinApp"; RegisterClassW(&wc); HWND hWnd = CreateWindowW( L"FirstWinApp", 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( L"EDIT", L"", WS_CHILD | WS_VISIBLE | WS_BORDER | ES_LEFT, 20, 20, 260, 26, hWnd, (HMENU)IDC_TEXTBOX, hInst, NULL ); // 按钮 CreateWindowW( L"BUTTON", L"获取内容", WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON, 20, 60, 100, 30, hWnd, (HMENU)IDC_BTN_SHOW, hInst, NULL ); break; }

创建任何一个控件,四个要素缺一不可:

  • 类名:L"EDIT"表示编辑框,L"BUTTON"表示按钮。
  • 样式: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 1004

WM_CREATE里创建四个控件:

case WM_CREATE: { HINSTANCE hInst = ((LPCREATESTRUCT)lParam)->hInstance; CreateWindowW(L"EDIT", L"", WS_CHILD | WS_VISIBLE | WS_BORDER, 20, 20, 100, 26, hWnd, (HMENU)IDC_EDIT1, hInst, NULL); CreateWindowW(L"EDIT", L"", WS_CHILD | WS_VISIBLE | WS_BORDER, 130, 20, 100, 26, hWnd, (HMENU)IDC_EDIT2, hInst, NULL); CreateWindowW(L"BUTTON", L"相加", WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON, 20, 60, 80, 30, hWnd, (HMENU)IDC_CALC, hInst, NULL); CreateWindowW(L"STATIC", 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 WinMain@16

这是我见过最多的报错,几乎每个初学窗口编程的人都会栽一次。完整的报错是:

[Error] ld returned 1 exit status undefined reference to `WinMain@16'

WinMain@16里的@16,意思是WinMain有4个参数,__stdcall调用约定下参数总大小是16字节。链接器在全工程里找不到这个入口函数,于是报错。

原因很简单:你新建项目时选的是Console Application(控制台程序),这种程序的入口函数是main,不是WinMain。你写的WinMain在链接器眼里只是一个普通的自定义函数,链接器自然会去找main,结果找不到。

解决方法是:在DEV-C++里打开“项目”菜单下的“项目属性”,把“类型(Type)”改成Windows Application或Win32 GUI。老版本设置里的具体名称可能有点区别,但找类型这个选项就不会错。改了之后重新编译,这个报错就消失。如果你是用命令行g++手动编译,需要在编译命令里加-mwindows:

g++ main.cpp -o app.exe -mwindows

5.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的窄字符API),A版本只能接收窄字符串,编译器会报参数类型错误。所以编码问题最好从源头解决:老版本ANSI + 宽字符串API,或者新版本UTF-8 + 宽字符串API,不要混用。

5.4 运行时报缺少DLL

老版本DEV-C++(Orwell 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_BORDER
EDIT多行输入框WS_CHILD | WS_VISIBLE | ES_MULTILINE | ES_AUTOVSCROLL | WS_VSCROLL
EDIT密码输入框WS_CHILD | WS_VISIBLE | WS_BORDER | ES_PASSWORD
BUTTON普通按钮WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON
BUTTON复选框WS_CHILD | WS_VISIBLE | BS_CHECKBOX
STATIC普通标签WS_CHILD | WS_VISIBLE
STATIC带边框标签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行代码时并无两样。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/30 1:40:02

CORS跨域问题终极指南:原理、修复与排查实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/30 1:39:27

ESP32 PlatformIO 开发实战:VSCode 配置与烧录指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/30 1:39:20

Unity显示隐藏完全指南:从SetActive到CanvasGroup的代价权衡

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/30 1:39:05

智能车摄像头循迹实战:PID串级控制与环岛十字识别复盘

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/30 1:38:56

一尊古法琉璃的脱蜡熔铸与前端渐变色相淬炼

一尊古法琉璃的脱蜡熔铸与前端渐变色相淬炼在美院玻璃艺术与工艺美术系的高温烧造工坊里&#xff0c;有一处需要佩戴护目镜与耐热石棉手套的特殊炉膛。 这里正在进行着拥有两千余年历史的中国传统手工技艺——“古法脱蜡琉璃熔铸&#xff08;Ancient Lost-wax Glass Casting&am…

作者头像 李华
网站建设 2026/9/30 1:38:38

YOLOv5训练实战避坑指南:环境配置、数据准备与超参数调优

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华