拓冰建站拓冰建站
首页 / 资讯中心 / 正文

Win32 API从零创建窗口:C语言实现Windows GUI程序骨架

1. 从“黑框框”到“白窗口”为什么第一个窗口如此重要如果你是从C语言的控制台程序或者从Python、Java的命令行应用开始接触编程的那么“Windows程序设计”对你来说可能意味着一个全新的世界。长久以来你的程序运行在一个单调的命令行窗口里输入输出全靠文本界面交互基本为零。而“创建第一个窗口”就是推开这扇新世界大门的钥匙。这不仅仅是多画了一个框它代表着你从处理“数据流”转向了处理“事件流”从“顺序执行”迈入了“消息驱动”的异步世界。很多初学者卡在这一步不是因为代码多复杂而是思维没转过来——你不再是一个发号施令的指挥官而是一个在窗口里布置好一切然后静静等待用户来“触发”你的服务生。这个“第一个窗口”通常指的是一个最基础的、可移动、可缩放、带标题栏和系统菜单那个左上角的小图标的空白窗口。别看它简单它包含了Windows图形界面程序几乎所有的核心机制窗口类注册、窗口创建过程、消息循环、以及窗口过程函数。理解了这个流程你就掌握了Windows GUI程序的“骨架”后续无论是添加按钮、绘制图形还是处理文件拖放都是在这个骨架上“长肉”。网络上相关的代码和教程浩如烟海但很多要么是古老的Win32 API示例要么是依赖MFC等框架让人看不清本质。今天我们就抛开框架用最原始的C语言和Win32 API亲手从零“铸造”这个窗口并把每一步背后的“为什么”讲清楚。你会发现那些看似神秘的“句柄”、“消息”其实有着非常直观的对应关系。2. 环境准备不是只有Visual Studio一条路提到Windows开发很多人第一反应就是打开庞大的Visual Studio。这当然是最正统、最强大的选择特别是对于复杂的项目。但如果你只是想快速验证概念或者喜欢更轻量、更可控的环境完全有别的路可走。2.1 编译器的选择MSVC、MinGW与ClangMSVC (Microsoft Visual C)这是微软的亲儿子与Windows SDK集成度最高对最新Windows特性的支持也最好。如果你安装了Visual Studio即使是免费的Community版你就拥有了MSVC编译器。它的编译命令通常是cl。MinGW-w64这是一个Windows平台上的GNU编译器集合GCC端口。它非常轻量可以让你在Windows上使用熟悉的GCC命令如gcc。很多轻量级IDE如Code::Blocks或文本编辑器如VSCode的C/C插件都依赖它。它的好处是开源、免费且编译出的程序不依赖额外的MSVC运行时库。ClangLLVM的C语言前端现在在Windows上也有很好的支持可以通过MSYS2或直接安装LLVM for Windows获得。它编译速度快错误信息更友好。对于我们的第一个窗口程序三者皆可。我个人的建议是如果你追求“原汁原味”和最简单的路径用Visual Studio创建一个空项目是最快的。如果你想体验更“原生”的命令行编译过程或者你的开发环境是VSCode那么配置MinGW-w64会是一个好选择。本文的示例代码将使用纯C和标准Win32 API确保在任何上述编译器下都能通过。2.2 头文件与库文件Windows SDK的核心无论你用哪个编译器都需要Windows SDK。它包含了所有Win32 API函数的声明在.h头文件里和实现在.lib库文件里。好消息是如果你安装了Visual Studio或者单独的Windows SDK这些就已经具备了。对于MinGW其安装包通常已经包含了基本的Windows头文件和导入库。我们程序中最关键的两个头文件是windows.h这是一个“总括头文件”它内部包含了绝大多数常用的Windows头文件如windef.h,winbase.h,winuser.h,wingdi.h等。对于我们这个基础窗口包含这一个就够了。tchar.h如果你希望程序同时支持ANSI和Unicode字符集这是现代Windows程序的必备要求这个头文件定义了TCHAR,_T()等宏能让你写出一份代码两种编码通用。链接时我们需要链接user32.lib负责窗口、消息等用户界面功能和gdi32.lib负责绘图。在Visual Studio的项目属性里可以设置在命令行下MSVC用/link user32.lib gdi32.libGCC/MinGW用-luser32 -lgdi32。注意很多古老的教程会教你使用WinMain的ANSI版本即WinMain本身并直接使用char字符串。但在现代WindowsWindows NT内核以后中底层全是UnicodeUTF-16。为了兼容性和正确性强烈建议始终使用Unicode版本。这意味着我们的入口函数是wWinMain字符串字面量要用L”…”包裹或者使用_T()宏配合TCHAR。本文示例将采用显式的Unicode方式以求清晰。3. 核心四步曲解剖一个窗口的诞生创建一个窗口不像在控制台打印“Hello World”那样一蹴而就。它是一个标准的、有固定步骤的仪式。这个过程充分体现了Windows“注册-创建-事件循环”的设计哲学。3.1 第一步设计蓝图——定义与注册窗口类在创建具体的窗口实例之前我们必须先定义一种“窗口类”。你可以把它理解成面向对象编程中的“类”Class而后续创建的窗口就是这个类的“实例”。这个类定义了这类窗口的共同属性比如外观图标、光标、背景色和行为处理消息的函数。这个过程通过填充一个WNDCLASSEXW结构体EX表示扩展版本W表示宽字符/Unicode版本并调用RegisterClassExW函数来完成。#include windows.h // 声明窗口过程函数后续定义 LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam); int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, PWSTR pCmdLine, int nCmdShow) { // 1. 定义并注册窗口类 const wchar_t CLASS_NAME[] L”Sample Window Class”; WNDCLASSEXW wc {0}; wc.cbSize sizeof(WNDCLASSEXW); // 结构体大小必须设置 wc.style CS_HREDRAW | CS_VREDRAW; // 窗口水平或垂直方向大小改变时重绘整个窗口 wc.lpfnWndProc WindowProc; // 指向窗口过程函数的指针这是窗口的“大脑” wc.hInstance hInstance; // 当前应用程序实例句柄由系统传入 wc.hIcon LoadIcon(NULL, IDI_APPLICATION); // 窗口图标使用默认应用程序图标 wc.hCursor LoadCursor(NULL, IDC_ARROW); // 窗口内光标形状使用默认箭头 wc.hbrBackground (HBRUSH)(COLOR_WINDOW 1); // 窗口背景色使用系统窗口颜色 wc.lpszClassName CLASS_NAME; // 这个窗口类的唯一名称 RegisterClassExW(wc); // ... 后续步骤 }关键参数解读lpfnWndProc: 这是最重要的成员。它指定了这类窗口的“窗口过程”函数。所有发生在这个窗口上的事件鼠标点击、键盘输入、窗口移动等都会以“消息”的形式发送到这个函数来处理。你可以把它看作窗口的事件处理器。hInstance: 应用程序实例句柄。它像一个ID将我们注册的窗口类与当前运行的程序实例绑定。wWinMain的参数hInstance就是系统给我们的。lpszClassName: 给这个窗口类起个名字一个字符串常量。之后创建窗口时就要靠这个名字来告诉系统“我要创建一个属于‘Sample Window Class’这个类的窗口”。hbrBackground: 背景画刷。COLOR_WINDOW是系统定义的窗口背景颜色索引通常为浅灰色。1是因为系统颜色索引从0开始而COLOR_WINDOW的值是5但作为画刷使用时需要这个转换。你也可以用CreateSolidBrush(RGB(255, 255, 255))创建一个纯白色的画刷。3.2 第二步按图施工——创建窗口实例注册好窗口类我们就有了蓝图。现在可以用CreateWindowExW函数来真正“建造”一个窗口了。这个函数参数很多但大部分都有合理的默认值。// 2. 创建窗口 HWND hwnd CreateWindowExW( 0, // 扩展窗口样式0表示无特殊样式 CLASS_NAME, // 上一步注册的窗口类名 L”Learn to Program Windows”, // 窗口标题栏文字 WS_OVERLAPPEDWINDOW, // 窗口样式这是一个标准的、可重叠的顶级窗口 // 位置和大小CW_USEDEFAULT 让系统选择默认位置和大小 CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, // 父窗口句柄NULL表示这是顶级窗口 NULL, // 菜单句柄NULL表示使用窗口类中定义的菜单这里也没有 hInstance, // 应用程序实例句柄 NULL // 附加数据通常为NULL ); if (hwnd NULL) { return 0; // 创建失败 }关键参数解读第二个参数CLASS_NAME必须和注册时使用的类名完全一致系统靠它找到蓝图。第三个参数窗口标题。第四个参数WS_OVERLAPPEDWINDOW这是一个组合样式它等于WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_THICKFRAME | WS_MINIMIZEBOX | WS_MAXIMIZEBOX。也就是说它给了窗口标题栏、系统菜单、可调整大小的边框、最小化和最大化按钮——一个标准窗口该有的它都有了。CW_USEDEFAULT这是一个神奇的常量。当你把窗口的位置和宽高都设为它时系统会帮你计算一个合适的默认位置和大小通常是屏幕中央。这比硬编码坐标要友好得多。返回值HWND这是“窗口句柄”。句柄是Windows中用来标识各种内核对象如窗口、文件、进程的一个唯一值你可以把它想象成对象的“身份证号”或“遥控器”。后续所有对这个窗口的操作显示、隐藏、移动、销毁都需要通过这个句柄来进行。3.3 第三步让窗口亮相——显示与更新窗口创建成功后窗口在内存中已经存在但默认是隐藏的。我们需要两条命令让它出现在屏幕上。// 3. 显示并更新窗口 ShowWindow(hwnd, nCmdShow); UpdateWindow(hwnd);ShowWindow(hwnd, nCmdShow): 这个函数控制窗口的显示状态正常、最大化、最小化。参数nCmdShow是wWinMain传进来的通常由启动方式决定比如用户是双击运行还是通过快捷方式以最小化方式运行。直接传递这个参数让系统决定初始显示状态是最佳实践。UpdateWindow(hwnd): 这个函数会向窗口发送一个WM_PAINT消息强制窗口立即绘制其客户区窗口内部除了标题栏和边框的区域。如果不调用窗口可能会等到消息循环第一次处理WM_PAINT时才绘制在快速创建的窗口中可能导致一瞬间的空白。3.4 第四步心脏起搏器——消息循环窗口显示出来了但它现在是“静止”的。它不会响应用户的点击、按键也不会移动。因为它缺少一个驱动它活起来的引擎——消息循环。Windows是一个事件驱动的操作系统。用户的任何操作、系统状态的任何变化都会被封装成一个一个的“消息”投递到对应窗口的“消息队列”中。我们的程序必须主动地、不断地从这个队列里取出消息并分发给对应的窗口过程去处理。这个“取-分发”的无限循环就是消息循环。// 4. 消息循环 MSG msg {0}; while (GetMessage(msg, NULL, 0, 0)) { TranslateMessage(msg); DispatchMessage(msg); } return (int)msg.wParam;GetMessage: 从当前线程的消息队列中获取一条消息。如果队列为空这个函数会阻塞直到有消息到来。这保证了CPU不会空转。当获取到的消息是WM_QUIT退出消息时GetMessage返回0循环结束。TranslateMessage: 将键盘的击键消息进行转换生成对应的字符消息WM_CHAR。比如你按一下‘A’键系统会先产生WM_KEYDOWN和WM_KEYUP消息经过TranslateMessage后可能会多产生一个WM_CHAR消息其wParam就是字符‘A’的ASCII码。这对于处理文本输入非常关键。DispatchMessage: 这是最关键的一步。它将消息“派遣”到该消息目标窗口的窗口过程函数即我们注册窗口类时指定的WindowProc中去。系统会调用我们的WindowProc并传入消息详情。至此一个窗口程序的主框架就完成了。但还缺最关键的一块拼图那个决定窗口如何响应事件的WindowProc函数。4. 窗口的灵魂窗口过程函数详解窗口过程函数Window Procedure是每个窗口的“大脑”。它决定了这个窗口如何响应外界的一切刺激。其函数签名是固定的LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam);hwnd: 发生消息的窗口的句柄。如果一个程序有多个窗口这个参数能让你知道消息是发给谁的。uMsg: 消息标识符。这是一个无符号整数比如WM_CREATE窗口创建、WM_PAINT需要绘制、WM_DESTROY窗口被销毁、WM_LBUTTONDOWN鼠标左键按下等等。Windows定义了数百个这样的消息。wParam和lParam: 消息的附加参数。它们的含义完全取决于uMsg。例如对于WM_KEYDOWN消息wParam存储的是被按下的虚拟键码如 VK_A 代表A键。一个典型的、最简化的窗口过程函数如下LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) { switch (uMsg) { case WM_DESTROY: PostQuitMessage(0); // 发送 WM_QUIT 消息使主循环退出 return 0; case WM_PAINT: { PAINTSTRUCT ps; HDC hdc BeginPaint(hwnd, ps); // 获取设备上下文准备绘制 // 在这里进行绘制操作例如 // TextOut(hdc, 10, 10, L”Hello, Windows!”, 15); // 画一个矩形Rectangle(hdc, 50, 50, 200, 200); FillRect(hdc, ps.rcPaint, (HBRUSH)(COLOR_WINDOW1)); // 简单用背景色填充需要重绘的区域 EndPaint(hwnd, ps); // 结束绘制必须成对调用 } return 0; } // 对于我们不处理的消息必须调用 DefWindowProc 进行默认处理 return DefWindowProc(hwnd, uMsg, wParam, lParam); }这个函数的核心逻辑是一个巨大的switch-case语句根据不同的uMsg执行不同的代码。WM_DESTROY: 当窗口被销毁时比如用户点击了标题栏的关闭按钮收到此消息。在这里我们调用PostQuitMessage(0)。这个函数会向线程的消息队列中投递一个WM_QUIT消息。当消息循环中的GetMessage取到WM_QUIT时会返回0从而退出循环程序结束。这是关闭窗口并退出程序的标准方式。WM_PAINT: 当窗口的客户区需要重新绘制时收到此消息。这可能是因为窗口刚从隐藏变为显示、窗口被其他窗口遮挡后重新露出、窗口大小改变等。绘制窗口必须遵循特定步骤BeginPaint: 获取设备上下文HDC你可以把它想象成一张画布和一套画笔的集合。同时它还会填充一个PAINTSTRUCT结构其中的rcPaint成员指明了需要重绘的矩形区域。系统是智能的它只告诉你需要重画哪一块而不是整个窗口这能提升性能。使用GDI函数如TextOut,Rectangle,Ellipse在hdc上进行绘制。EndPaint: 释放设备上下文资源。BeginPaint和EndPaint必须成对调用。在我们的最简示例中只是用背景色填充了需要重绘的区域。DefWindowProc: 这是至关重要的一步。对于我们不打算显式处理的所有消息必须调用DefWindowProc默认窗口过程。这个函数提供了Windows对标准窗口的默认行为。例如如果你不处理WM_CLOSE用户点击关闭按钮时先于WM_DESTROY发送DefWindowProc会调用DestroyWindow来销毁窗口。如果你不处理WM_SIZE窗口大小改变DefWindowProc会帮你调整窗口内部布局的某些默认状态。漏掉它或错误地返回0会导致窗口行为异常比如无法拖动、无法关闭。5. 第一个完整程序将所有部分组合起来现在让我们把上述所有代码片段组合成一个完整的、可以编译运行的Win32窗口程序。将以下代码保存为first_window.c。#include windows.h // 声明窗口过程函数 LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam); // 程序入口点 int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, PWSTR pCmdLine, int nCmdShow) { // 1. 设计并注册窗口类 const wchar_t CLASS_NAME[] L”Sample Window Class”; WNDCLASSEXW wc {0}; wc.cbSize sizeof(WNDCLASSEXW); wc.style CS_HREDRAW | CS_VREDRAW; wc.lpfnWndProc WindowProc; wc.hInstance hInstance; wc.hIcon LoadIcon(NULL, IDI_APPLICATION); wc.hCursor LoadCursor(NULL, IDC_ARROW); wc.hbrBackground (HBRUSH)(COLOR_WINDOW 1); wc.lpszClassName CLASS_NAME; if (!RegisterClassExW(wc)) { // 注册失败可能是类名冲突等 return -1; } // 2. 创建窗口实例 HWND hwnd CreateWindowExW( 0, CLASS_NAME, L”我的第一个Windows窗口”, WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, hInstance, NULL ); if (hwnd NULL) { return -1; } // 3. 显示窗口 ShowWindow(hwnd, nCmdShow); UpdateWindow(hwnd); // 4. 运行消息循环 MSG msg {0}; while (GetMessage(msg, NULL, 0, 0)) { TranslateMessage(msg); DispatchMessage(msg); } return (int)msg.wParam; } // 5. 窗口过程函数实现 LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) { switch (uMsg) { case WM_DESTROY: PostQuitMessage(0); return 0; case WM_PAINT: { PAINTSTRUCT ps; HDC hdc BeginPaint(hwnd, ps); // 在窗口中心绘制一行文本 const wchar_t* greeting L”你好Windows编程世界”; RECT rect; GetClientRect(hwnd, rect); // 获取窗口客户区矩形 DrawTextW(hdc, greeting, -1, rect, DT_CENTER | DT_VCENTER | DT_SINGLELINE); EndPaint(hwnd, ps); } return 0; // 可以在这里添加更多case来处理其他消息例如 // case WM_LBUTTONDOWN: // MessageBoxW(hwnd, L”你点击了鼠标左键”, L”提示”, MB_OK); // return 0; } // 将我们不处理的消息交给系统进行默认处理 return DefWindowProc(hwnd, uMsg, wParam, lParam); }编译与运行使用MSVC (Visual Studio Developer Command Prompt):cl first_window.c user32.lib gdi32.lib然后运行生成的first_window.exe。使用MinGW-w64 (在MSYS2或已添加PATH的环境下):gcc -o first_window.exe first_window.c -luser32 -lgdi32然后运行./first_window.exe。运行后你应该能看到一个标准的Windows窗口标题为“我的第一个Windows窗口”窗口中央显示着“你好Windows编程世界”。你可以拖动、缩放、最大化、最小化、关闭它。恭喜你已经亲手创建了一个完整的Windows GUI应用程序骨架6. 从“能跑”到“好用”常见问题与进阶技巧第一个窗口跑起来只是开始。在实际开发中你会遇到各种细节问题。下面是一些初期最常见的坑和应对技巧。6.1 窗口闪烁与绘制优化你可能已经注意到当我们快速拖动窗口改变大小时客户区内容那行文本有时会闪烁。这是因为WM_PAINT消息在窗口大小变化期间会被频繁触发而每次BeginPaint默认会先用背景色擦除整个客户区然后再绘制我们的文本这个“擦除-绘制”的间隙导致了闪烁。解决方案处理WM_ERASEBKGND消息当系统准备擦除窗口背景时会发送此消息。如果我们处理此消息并直接返回TRUE非零就告诉系统“背景我自己处理了你别擦了”。这可以避免一次不必要的全屏清除。但注意你需要在WM_PAINT中自己绘制整个需要的背景。case WM_ERASEBKGND: return 1; // 告诉系统我们已经处理了背景擦除使用双缓冲绘制对于复杂的图形界面如游戏、图表更高级的做法是“双缓冲”。原理是先在内存中的一张“虚拟画布”位图上完成所有绘制然后一次性将整张画布复制到屏幕窗口上。这能彻底消除因多次绘制造成的闪烁。但这涉及更多GDI知识属于进阶内容。6.2 Unicode与ANSI的编码陷阱现代Windows内部使用UTF-16编码。我们的程序使用了wWinMain、WNDCLASSEXW、CreateWindowExW、DrawTextW等带W后缀的函数这是显式使用Unicode宽字符版本。这是推荐的做法。但如果你看到一些老旧代码或教程使用的是WinMain、WNDCLASSEX、CreateWindowEx、DrawText。注意这些是没有后缀的“通用”版本。它们实际上是一组宏编译时根据是否定义了UNICODE宏展开为W版本或AANSI版本。为了确保一致性一个项目里应该明确定义字符集在项目属性中设置“字符集”为“使用Unicode字符集”这会在编译时定义UNICODE和_UNICODE宏。或者在代码开头手动定义#define UNICODE #define _UNICODE #include windows.h然后你就可以使用无后缀的通用函数名和TCHAR类型了。我个人更倾向于直接使用显式的W版本意图更清晰。6.3 窗口创建失败与调试如果CreateWindowEx返回NULL窗口创建就失败了。如何调试使用GetLastError()函数在调用可能失败的API后立即调用DWORD err GetLastError();。这个函数返回最近一次API调用失败的错误代码。你可以通过FormatMessage函数将这个代码转换成可读的文字。if (hwnd NULL) { DWORD err GetLastError(); wchar_t errMsg[256]; FormatMessageW(FORMAT_MESSAGE_FROM_SYSTEM, NULL, err, 0, errMsg, 256, NULL); MessageBoxW(NULL, errMsg, L”创建窗口失败”, MB_ICONERROR); return -1; }常见失败原因窗口类名 (lpszClassName) 拼写错误或者没有成功注册 (RegisterClassEx失败)。窗口样式 (dwStyle) 设置不合理比如给一个子窗口设置了WS_OVERLAPPEDWINDOW。实例句柄 (hInstance) 传递错误。6.4 消息循环的变体PeekMessage与游戏循环我们使用的GetMessage循环在程序没有消息时会阻塞这非常节能适合大多数GUI程序。但有些程序需要在不处理消息时也进行一些后台工作比如游戏需要持续渲染画面。这时可以使用PeekMessage。PeekMessage会立即返回无论消息队列是否有消息。利用这个特性可以写出一个“游戏循环”MSG msg; bool done false; while (!done) { // 处理所有待处理的消息 while (PeekMessage(msg, NULL, 0, 0, PM_REMOVE)) { if (msg.message WM_QUIT) { done true; } TranslateMessage(msg); DispatchMessage(msg); } // 如果没有消息或者处理完所有消息就执行游戏逻辑如更新状态、渲染 if (!done) { // 在这里调用你的游戏更新和渲染函数 // UpdateGameLogic(); // RenderFrame(); } }这种模式保证了程序的响应性又能持续运行。但要注意它会占用大量CPU资源因为循环空转通常需要配合垂直同步或帧率限制。7. 超越第一个窗口从这里出发能做什么当你成功创建并理解了这个基础窗口后Windows编程世界的大门才真正向你敞开。这个框架是所有后续扩展的基石添加控件你可以在窗口内创建按钮 (CreateWindowW指定类名为”BUTTON”)、编辑框 (”EDIT”)、列表框 (”LISTBOX”) 等标准控件。它们本质上是特殊的子窗口有自己的窗口过程和消息。菜单与资源学习使用资源文件 (.rc) 定义菜单、图标、对话框模板通过LoadMenu,LoadIcon等函数加载它们。图形绘制深入GDI或更现代的Direct2D、GDI学习绘制线条、形状、位图、文本实现自定义的UI。处理用户输入更细致地处理WM_KEYDOWN,WM_CHAR,WM_MOUSEMOVE,WM_LBUTTONDOWN等消息实现交互逻辑。多窗口与对话框创建多个窗口处理它们之间的通信。创建模态或非模态对话框。探索现代框架理解这个底层机制后再去学习MFC、WinForms、WPF、Qt或WinUI 3你会明白这些框架是如何封装和简化这些底层API的从而更能理解它们的优势和局限。万事开头难而你已经成功地开了这个头。这个看似简单的“第一个窗口”浓缩了Windows GUI程序最核心的架构思想。反复阅读代码尝试修改参数比如改变窗口样式WS_OVERLAPPEDWINDOW为WS_POPUP看看无边框窗口或者修改背景色添加一些简单的消息处理比如响应鼠标点击弹出一个消息框是巩固理解的最佳方式。编程是门实践的手艺光看不练永远无法真正掌握。现在运行你的编译器让这个窗口在屏幕上亮起来吧那是属于你的第一个图形界面里程碑。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门