尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

C++可视化窗口编程入门:Win32 API与GUI开发实战

C++可视化窗口编程入门:Win32 API与GUI开发实战 1. 项目概述为什么用C写可视化窗口如果你学C已经有一段时间整天对着黑底白字的控制台窗口输出“Hello World”、算排序、写链表总有一天会觉得不过瘾。我当初也是从控制台程序起步的直到第一次用C写出了带按钮、能拖动、能响应用鼠标点击的窗口程序那种“我写的代码真的能在屏幕上动起来”的成就感跟单纯在终端里打印几行文本完全是两回事。所谓“可视化窗口”本质上是让程序拥有一个图形用户界面GUI用户通过鼠标、键盘和屏幕上的控件与程序交互而不是靠命令行输入输出。C本身是一门通用编程语言标准库里并没有直接提供GUI能力所以我们要借助图形库、系统接口或者跨平台框架来实现。这也正是“使用C编写可视化窗口”这件事最有意思的地方——它逼着你理解C程序是如何跟操作系统打交道的同时也让你看到C在现代软件开发中真正的用武之地。这篇内容适合谁已经掌握C基础语法变量、函数、类、指针想更进一步做图形界面练习的人被课程设计逼着要交一个窗口程序的大学生以及想搞清楚“C到底怎么弹出一个窗口”的纯好奇人士。我会从方案选型讲起再逐步带你写出一个完整的窗口程序最后把我实际踩过的坑和排查思路一并整理出来尽量让你少走弯路。2. 技术方案选型GUI框架怎么挑2.1 主流的C图形界面方案对比C做可视化窗口路子可不止一条。我在实际操作中反复比较过几种主流方案这里把它们的优缺点列出来方便你做选择。Win32 API这是Windows操作系统提供的原生接口不需要安装任何第三方库直接用编译器就能调用。它性能高、可控性强但写起来非常啰嗦——一个最小的窗口程序也要好几屏幕代码。适合用来理解Windows窗口机制的本质也适合那些只面向Windows平台的工具类小软件。Qt这是目前C领域最主流的跨平台GUI框架支持Windows、macOS、Linux自带信号槽机制、丰富的控件库、所见即所得的设计器。它在工业界的地位几乎无可撼动很多桌面软件都是用Qt开发的。代价是框架本身比较重上手难度相对高。wxWidgets同样是跨平台C GUI库强调“原生外观”用它在Windows上写的程序长得就像Windows原生的在Linux上长得就像Linux原生的。它比Qt更轻量但生态和文档不如Qt丰富。SFML / SDL这两个更适合做游戏或者多媒体应用它们提供窗口创建、渲染、输入处理、音频播放等能力比GUI框架更偏底层。想用C写小游戏的话这两个是很好的选择。如果你是纯粹为了学习“C如何写窗口”、跑通一个能用的可视化程序我的建议是Windows平台上第一优先试Win32 API然后再学Qt。原因很简单Win32能帮你把窗口、消息、事件这些底层概念吃透而这些概念在Qt里被封装得干干净净——你不搞清楚底层机制一旦遇到奇怪的问题会完全找不到方向。2.2 本项目的选型思路与理由基于“给入门者一条清晰可行的路径”这个目标本篇采用Win32 API作为主要实现方案。这样做的理由有三点第一零依赖。Windows自带的user32.dll、gdi32.dll就能完成全部工作不需要额外安装开发包环境配置复杂度最低。第二最贴近C本质。窗口程序的绘制过程就是C代码与操作系统图形子系统的交互过程理解了这个流程以后学任何GUI框架都是降维打击。第三与热搜词中“vscode配置c/c环境”、“visual c redistributable”这些高频需求完全吻合——你不需要特殊处理依赖只需要一个能编译C的编译器即可。当然Win32的代码风格跟现代C差异很大它到处是宏定义和全局句柄看起来很不“C”。但这也正是它的教学价值所在——你会亲眼看到一门语言要真正落地做事必须跟底层的运行环境协作。真实世界里很多所谓“纯C项目”本质上都是“C语法 平台API调用”的结合体。3. 环境准备编译器与开发工具链3.1 编译器选择与安装写窗口程序第一步是让C代码能编译成可执行文件。你的机器上必须有编译器这是最基础的开发工具。常见的选择有MSVCMicrosoft Visual CWindows下最正统的C编译器从Visual Studio自带的编译工具链演变而来很多第三方库在Windows上编译时都会检查MSVC版本。安装Visual Studio Build Tools即可获得不一定要装完整的Visual Studio IDE。热搜词里出现的“error: microsoft visual c 14.0 or greater is required”就是某些Python包或C库安装时要求系统里存在VS2015或更高版本的MSVC编译环境。MinGW-w64Windows下的GCC移植版开源、免费、灵活配合VSCode使用非常顺手。它是很多轻量级C开发者的首选不占太多磁盘空间命令行操作方便。Clang跨平台编译器语法检查严格、报错信息友好但Windows下的完整工具链配置略麻烦一般面向进阶用户。如果你问我个人建议纯初学者用MinGW-w64配合VSCode是体验最好的组合。Visual Studio功能强大但界面和项目配置对新人来说容易劝退MinGW只做编译这件事简单直接。装好之后在命令行里输入g --version能看到版本信息就算配好了。3.2 开发环境配置与链接选项有了编译器之后还需要准备一个编辑器来写代码。VSCode是目前最流行的选择安装上C/C扩展插件后写代码有语法高亮和智能提示体验不错。不过窗口程序跟控制台程序有个关键区别链接方式不同这地方坑过很多人。编译控制台程序链接器默认找main函数作为程序入口然后生成一个控制台子系统console subsystem的可执行文件——这就是为什么双击运行会弹出一个黑色窗口。而窗口程序走的是WinMain入口属于Windows子系统windows subsystem生成的可执行文件双击运行不会出现控制台黑窗。这段比重直接影响了很多初学者的疑惑“为什么我编译窗口程序时老是报错说找不到main”答案就是你用Win32框架写了WinMain入口函数但编译器以为你要编译的是控制台程序自然去找main了。解决方法是给编译命令加上链接选项指定窗口子系统。用g时是-mwindows用MSVC时是/SUBSYSTEM:WINDOWS。具体到VSCode里省事的做法是创建一个tasks.json文件把编译命令写在里面。比如{ version: 2.0.0, tasks: [ { label: build window app, type: shell, command: g, args: [-mwindows, main.cpp, -o, app.exe], group: { kind: build, isDefault: true } } ] }这样每次按快捷键就能一键编译并生成窗口程序。4. 核心原理拆解窗口程序背后的运行机制4.1 消息驱动机制窗口程序的生命线窗口程序跟控制台程序最本质的区别是什么控制台程序的执行流程是线性的从上往下一条条语句跑完就结束了。窗口程序恰恰相反它启动之后不会自己“活蹦乱跳”而是静静等待系统的指令——你的程序就是一个等待着被调用的大仓库。具体来说Windows为每个窗口程序维护一个消息队列。用户点击鼠标、按下键盘、拖动窗口、点击关闭按钮……操作系统会把这一切行为翻译成一条条“消息”排列进队列里。你的程序要做的事情就是不断地从队列里取消息、处理消息处理完了再取下一条。这个过程叫作消息循环整个窗口程序的生命都围绕这条循环展开。生活化类比窗口程序就像一个前台接待员。登记处把访客的诉求消息一张张递过来接待员每接到一张就处理一张有人问路就指路有人要复印就帮忙复印一直工作到下班时间。消息循环就是那个持续不断递纸条的状态。4.2 WinMain入口所有窗口的第一站WinMain是窗口程序的入口函数它长这样int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)刚开始看到这堆参数会头晕但理解它们其实不难。hInstance表示当前程序模块的句柄可以理解成程序自身的身份证号操作系统需要靠它来区分不同的程序实例。lpCmdLine是命令行参数。nCmdShow表示窗口初次显示时的状态最大化、最小化还是正常显示。hPrevInstance在32位时代已经废弃了忽略即可。这个函数负责整个窗口程序的生命周期管理注册窗口类、创建窗口、进入消息循环、处理完退出消息之后归还系统资源。它相当于一个总调度员。4.3 窗口类与消息处理两个不可绕过的概念注册窗口类是一个容易搞混的地方。这里的“窗口类”跟C的class不是一回事。它是一张配置清单告诉系统你这个窗口有哪些特征背景色什么颜色、鼠标光标长什么样、窗口消息该由哪个函数来处理。系统根据这张清单创建出实际的窗口实体。在Win32里用WNDCLASS结构体来填这张表其中最重要的一个成员是lpfnWndProc——它指定了窗口过程函数也就是真正负责处理消息的“大管家”。这个函数接收消息编号例如WM_PAINT表示需要重绘窗口WM_LBUTTONDOWN表示鼠标左键被按下WM_CLOSE表示用户点了关闭按钮。我们编写这个函数时可以用switch语句逐个处理感兴趣的消息剩下不关心的消息统统交给DefWindowProc这个默认处理函数去兜底。理解了消息驱动机制、WinMain入口、窗口类注册和消息处理函数整个窗口程序就不再是一串看不懂的代码了——它其实是一套有章可循的框架。5. 完整实操从零到一编写一个可视化窗口5.1 初始化系统注册窗口类下面进入实战环节。我带你写一个简单的窗口程序实际效果是弹出一个空白窗口窗口标题栏显示“我的第一个C窗口”点击关闭按钮能正常退出窗口中央还能显示一段文字。所有代码都基于Win32 API不影响你理解核心概念。第一步是在WinMain函数里做初始化。最要紧的是填充WNDCLASS结构体并调用RegisterClass函数注册窗口类#include windows.h LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam); int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) { const TCHAR CLASS_NAME[] TEXT(MyFirstWindowClass); WNDCLASS wc { 0 }; wc.lpfnWndProc WndProc; wc.hInstance hInstance; wc.lpszClassName CLASS_NAME; wc.hbrBackground (HBRUSH)(COLOR_WINDOW 1); wc.hCursor LoadCursor(NULL, IDC_ARROW); if (!RegisterClass(wc)) { MessageBox(NULL, TEXT(窗口类注册失败), TEXT(错误), MB_OK | MB_ICONERROR); return 0; } ... }注意几个细节。TEXT()宏是为了保证字符编码在不同编译选项下都正确它把字符串字面量转换成宽字符或窄字符。hbrBackground指定了窗口客户区的背景颜色这里用的是系统默认窗口背景色。LoadCursor(NULL, IDC_ARROW)加载系统标准箭头光标这是最省事的光标初始化方式。注册失败时用MessageBox弹出错误提示这是调试窗口程序最常用的方式——因为窗口程序的编译结果不附带控制台printf的输出你看不到MessageBox至少能让错误可见。5.2 创建窗口并显示让窗口真正出现注册完窗口类接着就是用CreateWindowEx创建具体的窗口实例然后调用ShowWindow和UpdateWindow让窗口显示出来HWND hwnd CreateWindowEx( 0, CLASS_NAME, TEXT(我的第一个C窗口), WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, 800, 600, NULL, NULL, hInstance, NULL ); if (hwnd NULL) { MessageBox(NULL, TEXT(窗口创建失败), TEXT(错误), MB_OK | MB_ICONERROR); return 0; } ShowWindow(hwnd, nCmdShow); UpdateWindow(hwnd);CreateWindowEx的参数非常多但真正核心的就几项class name指定要用哪个已注册的窗口类window name是显示在标题栏的文字style决定窗口样式WS_OVERLAPPEDWINDOW是一个组合样式代表标准的可缩放、带标题栏和系统菜单的窗口外形紧接着是位置和尺寸参数CW_USEDEFAULT让系统帮我们挑选默认位置。创建失败返回NULL这时务必处理错误并退出否则后续所有操作都会因为句柄为空而崩溃。5.3 消息循环让程序活起来窗口显示出来之后程序还不能退出必须进入消息循环持续接收并分发系统发来的消息MSG msg; while (GetMessage(msg, NULL, 0, 0)) { TranslateMessage(msg); DispatchMessage(msg); } return (int)msg.wParam;GetMessage函数在队列里没有消息时会阻塞等待这让程序不会白白占用CPU资源。它返回值有三种情况0表示收到WM_QUIT消息程序该退出了-1表示出错其他情况表示取到一条消息并需要处理。TranslateMessage负责把键盘消息翻译成更易用的字符消息——简单来说你按下键盘上的A键时系统先发一条按下消息TranslateMessage再生成一条“我按的是字符A”的消息。DispatchMessage则调用窗口过程函数WndProc把消息真正交给你的代码去处理。消息循环的退出靠WM_QUIT消息触发而WM_QUIT通常在用户点击关闭按钮、我们在WndProc里调用PostQuitMessage后产生。逻辑闭环在此时接通点击关闭按钮 → 系统发WM_DESTROY → 窗口过程函数调用PostQuitMessage → GetMessage返回0 → 循环结束 → WinMain返回 → 程序退出。5.4 窗口过程函数消息处理的核心落地窗口过程函数是处理一切消息的最终目的地。下面这段代码处理了两个关键消息LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) { PAINTSTRUCT ps; HDC hdc; RECT rect; switch (msg) { case WM_PAINT: hdc BeginPaint(hwnd, ps); GetClientRect(hwnd, rect); DrawText(hdc, TEXT(看这是我用C画的窗口), -1, rect, DT_CENTER | DT_VCENTER | DT_SINGLELINE); EndPaint(hwnd, ps); return 0; case WM_DESTROY: PostQuitMessage(0); return 0; } return DefWindowProc(hwnd, msg, wParam, lParam); }WM_PAINT消息的发生时机很讲究窗口首次显示、窗口从被遮挡变为可见、窗口大小改变时系统都会向窗口发送WM_PAINT让程序重新绘制客户区。绘制工作必须在BeginPaint和EndPaint之间完成这是设备上下文DC的正确使用规则。这里的DrawText是GDI提供的文本绘制函数它在指定的矩形区域里居中显示字符串。那段DT_CENTER | DT_VCENTER | DT_SINGLELINE是格式标志分别表示水平居中、垂直居中、单行显示。WM_DESTROY是窗口即将被销毁时收到的消息对应的处理是调用PostQuitMessage(0)。0是要传递给消息循环的退出码当GetMessage读到WM_QUIT时这个值会成为msg.wParam的值也就是WinMain的返回值。其他没处理的消息全部交给DefWindowProc去默认处理。比如拖动窗口、调整大小、最小化最大化等操作都靠这个默认处理函数来保证窗口正常响应系统要求。这也是一个常见的易错点新手自己写WndProc时容易只处理自己关心的消息但没有在default分支里调用DefWindowProc结果窗口程序表現得极其诡异——拖动不了、点关闭没反应。记住DefWindowProc必须永远存在它是最底层的兜底机制。编译运行的结果应该是一个居中显示着一行文字的800×600窗口标题是“我的第一个C窗口”点击关闭按钮能正常结束程序。6. 进阶扩展往窗口里添加按钮与事件响应6.1 添加控件的两种思路演示窗口有了但一个没有按钮、不能点击交互的窗口离“可视化应用”还有距离。往窗口里添加按钮控件通常有两条路第一条路是直接在窗口过程函数里用CreateWindow创建按钮。Win32里按钮本身也是一个窗口——没错一切可见的东西在Windows里本质上都是窗口。给父窗口指定子窗口控件句柄再通过WM_COMMAND消息来接收按钮点击事件就能实现交互。第二条路是在资源文件.rc文件里定义对话框模板再用DialogBox或CreateDialog加载对话框资源。这种方式适合做复杂界面但需要对资源脚本语法有了解入门阶段不推荐。我们先用第一种方式理解交互原理。6.2 完整示例一个带点击计数按钮的窗口下面改造窗口过程函数在窗口创建时创建一个按钮并统计用户点击按钮的次数。先定义一个全局变量保存按钮句柄和点击次数HWND g_hButton NULL; int g_clickCount 0;然后在WM_CREATE消息里创建按钮。WM_CREATE在窗口创建完成、正式显示之前交给窗口过程函数处理LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) { switch (msg) { case WM_CREATE: g_hButton CreateWindow( TEXT(BUTTON), TEXT(点我一下), WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON, 350, 280, 100, 40, hwnd, (HMENU)1, ((LPCREATESTRUCT)lParam)-hInstance, NULL ); return 0; case WM_COMMAND: if (LOWORD(wParam) 1) { g_clickCount; TCHAR buffer[64]; wsprintf(buffer, TEXT(点击次数%d), g_clickCount); SetWindowText(g_hButton, buffer); } return 0; ... } }这里几个细节值得说清楚。按钮的类名是TEXT(BUTTON)这是系统预定义的控件类直接拿过来用就行。样式里WS_CHILD表示这是一个子窗口WS_VISIBLE确保它默认可见BS_PUSHBUTTON是普通按钮样式。CreateWindow的第8个参数是父窗口句柄我们把主窗口传进去这样按钮就“挂”在了主窗口上。第9个参数是菜单或控件ID(HMENU)1是强转这个ID在之后接收WM_COMMAND消息时用来识别是哪个控件发来的消息。WM_COMMAND是控件通知消息。当用户点击按钮系统向父窗口发送这条消息wParam的低16位存放控件ID高16位存放通知码。判断LOWORD(wParam)等于1就知道是ID为1的那个按钮被点了。这是父子窗口间最经典的通信方式。wsprintf把格式化字符串写入缓冲区SetWindowText则动态修改按钮上显示的文字。这样每点击一次按钮按钮上的文字就会更新为“点击次数N”一个肉眼可见的交互效果就完整跑通了。6.3 Windows绘图基础在窗口上画线和形状除了控件交互很多可视化窗口还需要自绘图形。Win32里最核心的绘图概念是设备上下文DC——它就像一个虚拟画布所有绘制操作都是在这块画布上执行的。获取DC可以用BeginPaint也可以直接用GetDC然后把画刷、画笔选入DC再调用各种绘制函数。举个例子在窗口客户区画一条直线和一个填充矩形case WM_PAINT: hdc BeginPaint(hwnd, ps); // 画直线 MoveToEx(hdc, 50, 50, NULL); LineTo(hdc, 200, 50); // 画填充矩形 HBRUSH hBrush CreateSolidBrush(RGB(255, 0, 0)); HBRUSH hOldBrush (HBRUSH)SelectObject(hdc, hBrush); Rectangle(hdc, 50, 80, 200, 130); SelectObject(hdc, hOldBrush); DeleteObject(hBrush); EndPaint(hwnd, ps); break;注意CreateSolidBrush创建的画刷用完要DeleteObject删除否则会造成GDI资源泄漏。这是Win32绘图最常见的资源管理陷阱——GDI对象不像内存一样能自动回收忘了删除程序跑久了就会出现绘图异常、窗口显示错乱只能重启程序解决。7. 常见问题与排查技巧实录7.1 编译链接报错快速定位问题根源窗口程序最常见的报错集中在链接阶段。我把实践中最容易遇到的几种情况整理成速查表错误信息主要原因解决方案undefined reference toWinMain16编译器在找入口函数但你的代码没有提供正确的WinMain或编译命令少了必要库确认入口函数签名是否完整检查代码里是否写了main而不是WinMainundefined reference to__gxx_personality_v0或__gxx_personality_seh使用了C标准库特性但编译器没完整链接C运行库用g而非gcc编译或检查是否链接了libstdccannot find -luser32 / -lgdi32缺少Windows系统库的链接编译命令里手动加-luser32 -lgdi32 -lcomctl32等系统链接库compiler not found / g 不是内部或外部命令环境变量未配置或MinGW没有安装重新配置环境变量把MinGW的bin目录加到PATH里error: microsoft visual c 14.0 or greater is required系统缺少MSVC编译工具链一般是安装第三方库时触发安装Visual Studio Build Tools或配置好VS 2015以上版本环境我在VSCode里遇到过最典型的问题是代码明明写对了WinMain编译还是报undefined reference。排查到最后发现tasks.json里的编译命令没带-mwindows选项导致链接器仍然按控制台程序的标准去寻找main入口。链接器的子系统设置直接决定了它找哪个入口函数这个机制要理解透以后不管是用IDE还是命令行都不会再困惑。7.2 运行时窗口显示异常编译通过不代表程序就能正常运行。点击exe后窗口没弹出来或者弹出来就崩这种运行时问题有几个高发原因。第一种是多字节字符集和Unicode字符集设置不一致。代码里写TEXT(我的第一个C窗口)当工程的字符集设置是“多字节字符集”时没有问题但如果你用了窄字符串字面量窗口而没有用TEXT宏包裹在Unicode字符集下编译会直接报错。更隐蔽的情况是代码内部混用了char和wchar_t导致字符串显示乱码。我的建议是新项目一律用Unicode字符集字符串统一包TEXT宏省得为编码问题反复折腾。第二种是窗口过程函数忘记调用DefWindowProc。新手很容易只处理WM_DESTROY就完事结果程序窗口不能正常关闭、拖动、最小化。原因是这些窗口行为都靠系统默认处理来响应你不调用DefWindowProc就等于把系统服务掐断了。这个坑极其隐蔽——程序能编译、能显示窗口但行为处处透着一股“残废”感。第三种是GDI资源泄漏。上面提到过CreateSolidBrush、CreatePen等创建的资源必须DeleteObject释放。写绘图层循环代码时尤其要注意每次循环都创建不释放程序运行几分钟后整个窗口的绘图就全部错乱。这种问题用调试器很难发现只能在代码里养成成对创建的释放习惯。7.3 与系统环境相关的高频问题热搜词里出现大量“visual c redistributable”相关的词条实际上这反映了一个很普遍的痛点用C编译出来的程序在某些不装运行库的机器上会提示“缺少VCRUNTIME140.dll”之类的错误无法启动。Visual C Redistributable是MSVC编译器的运行库里面装着程序运行时需要的动态链接库。用MinGW编译的程序一般不依赖VC运行库但用MSVC编译的程序几乎都依赖。解决方式很简单到官网下载对应版本的Microsoft Visual C Redistributable安装包装上重启即可。这个依赖在很多老电脑上特别常见——为什么你的程序在自己电脑上运行得好好的发给别人打开就报错多半就是对方的电脑缺少这套运行库。另外VSCode里配置C/C环境也是很多初学者反复出问题的点。配置文件tasks.json launch.json之间的关系经常让人头疼。我个人的配置习惯是tasks.json管编译launch.json管调试两者通过preLaunchTask关联起来。调试C程序时VSCode没反应90%是launch.json里的program路径和tasks.json生成的exe文件名不一致记得检查这点。8. 后续扩展方向窗口程序的基础框架搭起来之后能扩展的方向非常多。如果你对这条路线有持续兴趣我建议从三个角度继续深入。第一是往小游戏方向扩展。窗口加消息循环这套机制本身就适合写游戏——把所有绘制放在WM_PAINT里用定时器驱动动画帧监听键盘消息控制角色移动。配合C做贪吃蛇、俄罗斯方块这类经典小游戏正好把窗口、控件、GDI绘图、消息处理这些基础能力综合用上。第二是往Qt框架迁移。当你懂了Win32的窗口和消息机制后学Qt会轻松很多。Qt把窗口创建、事件分发、布局管理全部封装成了更好用的类开发效率高得多还能一套代码编译到多个平台。从Win32到Qt属于“先苦后甜”的路径底层的苦已经吃过了上层自然顺滑。第三是数据可视化。用C做图表、二维曲线绘制本质上就是GDI绘图的延伸。把数据映射成坐标点用Polyline连线画出折线用Rectangle画出柱状图再加上坐标轴和网格线。这类实战练习能让你牢牢掌握Windows绘图API的使用技巧也是C在工业控制、数据分析软件中常见的职责之一。我个人在实际操作中的体会是窗口编程的学习一定要循序渐进。别一上来就想着做一个功能复杂的完整应用先在窗口里画一个点再画一条线再放一个按钮再让它响应点击。每前进一步都看得到成果这种正反馈是最能维持学习动力的。还有一个小技巧把系统发来的消息编号通过OutputDebugString或者MessageBox打印出来你就能看到程序运行时后台到底在跑什么——这个窗口没做事时也在默默处理各种系统消息理解到这一点你对消息驱动模型的理解就又深了一层。
返回列表