
不少人用DEV-C学C写来写去都是在黑乎乎的控制台窗口里跑printf第一次看到别人做的Windows窗口程序难免会想这玩意儿到底是怎么在DEV-C里做出来的说实话这个问题的答案没那么玄乎——DEV-C自带的MinGW编译器完全能编译Windows窗口程序它内置了Windows SDK的头文件和导入库只是默认建工程时没给你开这个门。这篇文章就带着你用DEV-C从零建一个真正的Windows窗口程序窗口里放一个多行文本框和两个命令按钮点击按钮能统计文本长度、清空内容。整个过程紧扣C语言基础不需要额外安装任何东西适合刚学完控制台C、想往图形界面迈一步的读者。当你看完这篇文章得到的不仅是一份能跑的代码更重要的是理解Windows窗口程序的组织方式入口函数、窗口类、消息循环、窗口过程、控件创建这些概念都会被拆开揉碎讲明白。理解了这套机制以后不管用Visual Studio、Qt还是其他工具写Windows程序底层的逻辑都是相通的。1. 先拆问题DEV-C写窗口程序到底缺什么1.1 窗口程序和黑框程序入口的本质区别控制台程序的入口是main函数操作系统把你写的程序当成一个命令行工具给它一块黑乎乎的字符终端。窗口程序的入口则是WinMain操作系统不再给你字符终端而是把程序当作有界面的应用程序需要自己创建窗口、响应鼠标键盘事件。这不仅是函数名变了程序的整个工作方式都变了。控制台程序是我做一步输出一步流程是线性的窗口程序是我创建完窗口后就一直等着用户操作流程是事件驱动的。这个没想明白代码抄下来也很难理解。具体到DEV-C上当你新建一个项目时它默认给的是Console Application模板入口是main链接器也会把程序按控制台子系统处理。所以要在DEV-C里写窗口程序核心就两件事把入口改成WinMain把链接子系统改成 Windows子系统另外把用到的系统库链接进来。这三个关键点处理好环境就通了。1.2 Win32 API方案在DEV-C下的优势市面上有很多Windows GUI方案MFC、Qt、wxWidgets、C# WinForms等等。在DEV-C环境里MFC基本没戏因为它需要微软自家的Visual Studio环境和庞大的头文件库Qt对编译器版本和构建工具有要求刚入门配置环境就能劝退一大半人。剩下最务实的选择就是直接调用Win32 API。Win32 API是Windows系统本身提供的编程接口用C/C就能直接调用而且只要一个windows.h头文件和对应的系统库就行。在DEV-C的MinGW环境里这些头文件和库文件编译器已经带上了不需要额外安装任何东西。用Win32 API还有一个隐蔽优势它是所有Windows图形界面的地基。你以后学的MFC、Qt或者C#封装库底层最终都要调到这些API。先用Win32把消息循环和窗口过程搞明白等于先把地基打牢后面学什么框架都轻松很多。2. 窗口程序的运行机制与消息循环2.1 注册窗口类给程序一个身份证写一个Win32窗口程序第一步不是创建窗口而是注册窗口类。这个概念在编程书里讲得比较绕我用生活的比方说窗口类就像是商场的店铺设计图纸它规定了这家店的门头样式、服务员的办事风格、用什么背景色注册就相当于把这个图纸交给工商部门备案。备案以后你才能凭这个图纸去实际开一家店。代码上的对应关系是定义一个WNDCLASS结构体填上窗口过程函数、图标、光标、背景色、类名然后用RegisterClass交给系统。这里的类名相当于窗口的身份证号后面创建窗口时只要说按这个名字的图纸来系统就知道该怎么创建了。很多新人一上来就抄CreateWindow的代码却漏了前面的RegisterClass结果程序运行到创建窗口时直接报错或没有任何反应。这个顺序相当重要注册是创建的前提类名还必须在整个程序里保持一致。2.2 窗口过程函数程序的客服中心窗口过程函数是整个窗口程序最核心的部分它是一个长得很怪的回调函数LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)这个函数不需要你自己调用是操作系统在需要的时候反过来调你的函数。系统每产生一个事件比如鼠标点了一下、键盘敲了一下、窗口需要重绘、窗口要关闭了都会以消息的形式扔到你这个函数里。窗口过程函数就是一个客服中心你只需在函数里写清楚看到什么消息就处理什么处理不了的消息统一转给DefWindowProc让系统去做默认处理。CALLBACK这个修饰符也别无视它表示这个函数是给系统回调用的必须按系统约定的调用方式来编译。漏了它程序运行到某个时刻会突然崩掉而且崩得莫名其妙。2.3 CAMessage Loop程序为什么不会闪退窗口创建好了窗口过程函数也写好了接下来需要一个消息循环把系统扔过来的消息一个个喂给窗口过程函数。典型的消息循环长这样MSG msg; while (GetMessage(msg, NULL, 0, 0)) { TranslateMessage(msg); DispatchMessage(msg); }GetMessage从程序的消息队列里取一条消息没有消息就阻塞等待TranslateMessage将键盘消息转换成字符消息DispatchMessage把消息分发给正确的窗口过程函数。这个循环一直转直到收到WM_QUIT消息才返回退出。初学者可以把消息循环理解成一个前台接待员他不停地接电话GetMessage按内线转给对应的部门DispatchMessage各部门处理完回来继续等电话。没有这个循环程序创建完窗口就直接下班了表现出来的就是窗口一闪而过。3. 从零起步DEV-C里一步步建出窗口程序3.1 工程与编译器参数配置打开DEV-C我这里以大家用得最多的Orwell Dev-C 5.11为例小熊猫Dev-C菜单相差不大先别急着建项目直接点文件 → 新建 → 源代码把文件保存为main.cpp。有人会问不建Windows Application模板吗DEV-C 5.11里其实有一个Windows Application模板但比较老自动生成的代码风格不太利于学习这里直接手写反而更清爽。关键是编译器参数。Dev-C默认按控制台程序链接如果不管它你写了WinMain也会编译不过报错undefined reference to WinMain。解决办法是给链接器指定Windows子系统参数。最简单的做法是菜单工具 → 编译器选项 → 编译器在在编译时加入以下命令里填上-mwindows点击确定。这个参数告诉GCC这个程序是Windows图形界面程序不需要控制台窗口。为了保险建议同时把-luser32 -lgdi32也添上显式链接Windows界面库和图形设备接口库。这样后续编译任何窗口程序都能直接用。3.2 完整代码与逐段讲解下面是一份可以直接复制到main.cpp里运行的程序功能是窗口里有一个多行文本框、一个统计字数按钮、一个清空内容按钮。#include windows.h #define IDC_EDIT 1001 #define IDC_BTN_COUNT 1002 #define IDC_BTN_CLEAR 1003 HWND hEdit; LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) { switch (uMsg) { case WM_CREATE: { hEdit CreateWindowExW( WS_EX_CLIENTEDGE, LEDIT, L, WS_CHILD | WS_VISIBLE | WS_VSCROLL | ES_MULTILINE | ES_AUTOVSCROLL, 20, 20, 360, 220, hwnd, (HMENU)IDC_EDIT, GetModuleHandle(NULL), NULL ); CreateWindowExW( 0, LBUTTON, L统计字数, WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON, 120, 260, 100, 35, hwnd, (HMENU)IDC_BTN_COUNT, GetModuleHandle(NULL), NULL ); CreateWindowExW( 0, LBUTTON, L清空内容, WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON, 240, 260, 100, 35, hwnd, (HMENU)IDC_BTN_CLEAR, GetModuleHandle(NULL), NULL ); break; } case WM_COMMAND: { int cmdId LOWORD(wParam); if (cmdId IDC_BTN_COUNT) { wchar_t buf[1024] {0}; GetWindowTextW(hEdit, buf, 1023); int len lstrlenW(buf); wchar_t info[128]; wsprintfW(info, L当前文本长度%d 个字符, len); MessageBoxW(hwnd, info, L统计结果, MB_OK | MB_ICONINFORMATION); } else if (cmdId IDC_BTN_CLEAR) { SetWindowTextW(hEdit, L); } break; } case WM_DESTROY: PostQuitMessage(0); return 0; } return DefWindowProcW(hwnd, uMsg, wParam, lParam); } int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) { const wchar_t CLASS_NAME[] LSampleWindowClass; WNDCLASSW wc {0}; wc.lpfnWndProc WindowProc; wc.hInstance hInstance; wc.lpszClassName CLASS_NAME; wc.hCursor LoadCursorW(NULL, (LPCWSTR)IDC_ARROW); wc.hbrBackground (HBRUSH)(COLOR_WINDOW 1); RegisterClassW(wc); HWND hwnd CreateWindowExW( 0, CLASS_NAME, LDEV-C 文本框与按钮示例, WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, 420, 360, NULL, NULL, hInstance, NULL ); ShowWindow(hwnd, nCmdShow); UpdateWindow(hwnd); MSG msg; while (GetMessageW(msg, NULL, 0, 0) 0) { TranslateMessage(msg); DispatchMessageW(msg); } return 0; }代码看起来有100多行但结构非常清晰拆开讲一遍你就完全能拿捏。#include windows.h引入了Windows API的全部声明。然后是三个宏定义分别给文本框和两个按钮起了ID后续按钮的消息处理全靠这几个ID识别。HWND hEdit保存文本框的窗口句柄相当于文本框的身份证号后面操作文本框读内容、清空都要用它。WindowProc是窗口过程函数。程序收到窗口创建消息时在WM_CREATE分支里创建三个子控件。注意这里调用CreateWindowExW时把父窗口的句柄hwnd传了进去控件就会自动显示在父窗口上。控件类型由类名决定类名是LEDIT就是文本框是LBUTTON就是按钮。这一点很关键——文本框和按钮对操作系统来说也是窗口只是它们的窗口类被系统注册过了。按钮点击时系统会向父窗口发送WM_COMMAND消息函数参数里的LOWORD(wParam)就是按钮的ID用它判断是哪个按钮被点了。处理统计字数按钮时先用GetWindowTextW把文本框内容读出来再用lstrlenW获取字符长度wsprintfW格式化一段提示文字最后用MessageBoxW弹窗显示。处理清空内容时就简单了SetWindowTextW(hEdit, L)直接设置成空字符串。WinMain里做的事情按顺序看定义窗口类名、填充WNDCLASSW结构体、注册窗口类、创建主窗口、显示窗口、进入消息循环。这里的WNDCLASSW和CreateWindowExW都带一个W后缀表示使用Unicode宽字符版本好处是中文不会乱码这也是推荐新手直接用宽字符API的原因。3.3 编译、运行与实际效果代码粘贴完成后按F11编译并运行即可。如果前面的编译器参数没设置这里就会报错设好了则一切顺利。程序运行后主窗口大小为420x360像素文本框位于窗口左上角偏移20像素的位置宽360高220下面排着两个按钮。我在文本框里随手打了几个字点统计字数弹出提示当前文本长度6 个字符点清空内容文本框立即变成空白。全程不需要命令行窗口可以正常拖拽、缩放、关闭整体表现已经是一个标准的Windows程序。运行成功后建议你在WM_CREATE里改几个参数感受一下把按钮的横坐标从120改成20观察按钮贴到左边了把按钮高度从35改成50按钮变高了再把WS_OVERLAPPEDWINDOW改成WS_POPUP窗口标题栏没了。这一步的体验式学习比看十遍文档都有用。4. 高频问题排查与DEV-C实用避坑4.1 编译报错速查表我在自己机器上测试时基本把所有会踩的坑都踩了一遍下面这张表可以直接拿来当排查手册报错信息或现象原因解决办法undefined reference to WinMain链接器还在按控制台子系统找入口在编译器选项里加-mwindowsundefined reference to RegisterClassWuser32库没有链接在编译器选项里补-luser32编译通过但运行出现黑色控制台窗口链接子系统没有切换加-mwindows重新编译窗口一闪而过消息循环缺失或窗口没显示检查ShowWindow与while(GetMessage...)按钮点击没反应WM_COMMAND里ID判断错误检查LOWORD(wParam)与宏定义是否一致中文显示乱码字符集不匹配统一用W后缀API和L字符串源文件保存后再打开中文变问号DEV-C默认编码导致的字符集问题代码中统一使用宽字符字面量避免直接写中文到窄字符API4.2 按钮点击没反应优先检查这个顺序按钮事件不响应我见过最多的情况就是新手把WM_CREATE里的CreateWindowEx参数写错或者忘了把按钮ID强制转成(HMENU)。CreateWindowEx最后一个参数是菜单句柄但创建控件时这个位置被复用为控件ID。不转类型编译器虽然不报错但生成的ID是乱的WM_COMMAND里自然对不上。还有一点容易忽略的是按钮点击会产生WM_COMMAND消息但焦点在文本框时按下回车键可能触发的是默认按钮行为。如果窗口代码里没有设置默认按钮按下回车时系统找不到默认按钮消息就发不出去。初学阶段不用纠结这个知道有这回事就行。4.3 DEV-C环境特有的坑编码、路径、历史遗留有不少人卡在保存文件路径上。DEV-C 5.11是很多年前的工具对中文路径和空格路径的支持都比较差。建议项目文件一律保存在纯英文路径下比如D:\Win32Demo\main.cpp目录里别有中文、空格和特殊符号。我试过把代码放在我的文档默认的中文用户目录里编译报错都找不到根源挪到英文路径后一切正常。编码问题也要单独提一下。DEV-C老版本默认以ANSI编码保存源文件如果你用窄字符char[]硬塞中文容易出现乱码。本文的代码全部使用带W后缀的API和宽字符字面量L...从根源上规避了这个问题。如果非要使用MessageBoxA这种窄字符API一定要确保源文件保存为ANSI编码否则建议放弃窄字符方案。环境方面DEV-C有多种发行版。Orwell Dev-C 5.11是经典版资源占用低但对高分屏适配差小熊猫Dev-C是社区维护的新版本功能更全对Win10/Win11的兼容性更好。两个版本的编译器参数界面略有差异但核心思路一模一样照着文章思路操作都能搞定。4.4 一个小技巧用资源文件管理按钮文字代码里按钮文字是写死在CreateWindowEx里的如果以后按钮变多代码会变得很乱。Windows正规开发中的做法是使用资源文件.rc把窗口大小、控件位置、按钮文字都放到资源文件里再在代码里用DialogBox加载对话框模板。这种方式做出来的界面修改起来不需要动C代码更加工业化。不过这篇文章的目的是把Win32 API的消息机制讲清楚所以直接手写控件创建过程更适合初学阶段的理解。等你把这段代码吃透再去看对话框资源模板会发现很多概念是相通的那时候再跨一步就水到渠成了。5. 个人经验体会我最初在DEV-C里摸索Win32编程时最难拐过来的弯就是系统调用你的函数而不是你调用系统的函数。控制台程序的所有逻辑都从main开始按顺序执行窗口程序却是创建完窗口就没事干了全等着消息到来再去处理。后来我把窗口过程函数比作店里的服务台消息循环比作门口迎宾的接待员这一下就想通了接待员把客人领到对应柜台柜台员工处理需求处理完继续等下一个客人。想通了这一点整个Win32编程就不再神秘。如果这段代码你已经跑通了下一步我建议你试着把统计字数改成统计字母数或者统计行数再给窗口加一个输入框做成一个小计算器。折腾的过程才是真正吸收的过程光看不练看完三天必忘。另外注意这篇文章里的API全部用的是Unicode版本这在现代开发中是默认选择而老旧的教科书还经常用RegisterClassA之类的窄字符API学会辨认这两个后缀的区别你会少踩很多莫名其妙的坑。