简介:WTL10.0是微软轻量级C++库的最终版本,专门针对Visual Studio 2019做了适配与优化,面向希望直接调用Windows API、构建高效小巧桌面程序的C++开发者。压缩包共收纳296个文件,整体约704KB,其中104个头文件与39个C++源文件构成核心库,另有sln、vcxproj等多套示例工程,bmp、ico、rc等界面资源,以及html、txt、hhc等文档索引,方便在VS2019中安装、编译和查阅。目前已有325人学习下载。包内附带了安装向导、示例代码、完整的头文件与库文件、更新日志和API参考文档,开发者可据此快速搭好WTL开发环境,借助模板类、消息映射和Unicode支持编写出结构清晰、便于维护的Windows程序;同时它保留了对ATL、MFC乃至Boost的良好兼容性,适合具备C++基础、希望在轻量框架下替代MFC的中高级开发者。
1. WTL 10.0 最终版本:为什么 VS2019 用户还在翻它的牌
2019 年 10.0 最终版本发布之后,WTL 进入了长期维护模式,但这恰恰是它适合当下的原因:API 不再大幅变动,坑基本被人踩平了。对还在 VS2019 上维护内网工具、设备上位机程序、插件面板的 C++ 工程师来说,WTL 10.0 是一个介于 MFC 和裸 Win32 之间的舒服位置——不需要分发运行库,一个 exe 几百 KB,编译快,界面编写又比手写 Win32 消息循环省一半力气。这篇文章直接以 VS2019 为环境,把 WTL 10.0 的依赖链、工程配置、消息映射、对话框和常见编译坑一次讲透。
2. 从 ATL 到 WTL 10.0:搞懂依赖链,避开 VS2019 的 ATL 版本坑
2.1 WTL 不是又一个 UI 库,它是 ATL 的“民间官方扩展”
先把这个关系说清楚,不然配环境时你会一脸茫然。ATL(Active Template Library)是微软做 COM 组件用的轻量模板库,本身不提供窗口框架。WTL 的全称是 Windows Template Library,它做的事情就是把 ATL 的模板思路延伸到窗口上:以CWindow为根,让窗口类、对话框类、控件封装全部变成模板类和宏的组合。
跟 MFC 比,WTL 最大的不同在于没有运行时依赖。MFC 程序不管动态还是静态链接,编译产物里都带着一整套类库的痕迹,升级 VS 版本时经常要跟着改。WTL 程序编译时只依赖 ATL 头文件和系统库,运行时不需要分发任何 WTL 动态库,发布一个 exe 就完事。这对我来说是最有吸引力的一点:内网机器上往往缺少各种 VC 运行库,而 WTL 程序双击就跑。
下面是三个方案在实践中的对比,你判断自己该不该转:
| 对比项 | MFC | WTL | 裸 Win32 |
|---|---|---|---|
| 运行时依赖 | MFC DLL 或静态运行库 | 仅系统 DLL | 无 |
| 界面代码量 | 少,向导强 | 较少,宏简洁 | 多,全手写 |
| 产物体积 | 大 | 小 | 最小但代码量失控 |
| 控件封装 | 全面 | 够用 | 无 |
| 消息处理 | 消息表 + 向导 | 消息映射宏 | 手工 switch |
| 学习曲线 | 中 | 中高 | 高 |
我早期做过一个监控小工具,MFC 版本发布后要带 3 个 DLL,换到 WTL 后变成一个 400 KB 的单 exe,客户拷贝直接跑。就这一条,足够让老项目翻牌子。
2.2 为什么说 10.0 是“最终版本”:VS2019 兼容性之外的事实清单
WTL 的版本线从 7.x 走到 8.x、9.x,到 10.0 时官方明确把适配重点放到了 VS2019 上。当时的核心问题不是 WTL 代码本身,而是 ATL 的版本检测。WTL 头文件里大量代码靠_ATL_VER这个宏判断当前 ATL 支持哪些特性,VS2019 的 v142 工具集带的 ATL 版本号比 WTL 9.1 预期的高,导致条件编译走错分支,出现一批莫名其妙的编译错误。
10.0 做了三件关键事:一是重写了 ATL 版本检测逻辑,让 v142 工具集能正常通过;二是修了一批 64 位编译告警,老代码里GetWindowLong转GetWindowLongPtr的隐患被处理;三是对 Windows 10 SDK 新增的头文件和控件做了适配。之后项目进入修复式维护,没有继续规划 11,所以“最终版本”这个说法是准确的。
这里有个容易被忽略的点:WTL 10.0 的代码风格还是老式的 C++98/03 那一套,编译不需要开 C++11/C++14。你在 VS2019 里用默认标准编译完全没问题,反而别随手开/std:c++latest去追新标准,开了之后某些第三方库的告警会盖住你自己的问题。
2.3 VS2019 必须先装 ATL 组件,否则头文件都打不开
WTL 头文件的第一行几乎都带着#include <atlbase.h>,而这个atlbase.h不在 Windows SDK 里,属于 VS 的 ATL 组件。VS2019 安装时,默认“使用 C++ 的桌面开发”工作负载并不会把 ATL 装进去,这是新人翻车的第一站。
正确做法:打开 Visual Studio Installer,点“修改”,切到“单个组件”页,搜索ATL,勾选“适用于最新 v142 生成工具的 C++ ATL (x86 & x64)”,然后点修改。装完再启动 VS2019,atlbase.h才存在。
如果你手头只有 vs2019 离线安装包,也同样要走 Installer 里的布局修改流程,把 ATL 组件补进本地缓存。网上很多教程让你把 WTL 的 include 目录直接拷进系统 Include 目录,我强烈不建议:WTL 9 和 10 的头文件结构差异不小,全局污染之后,换项目就会窜版本。WTL 的 include 就放到你自己指定的目录,按工程配路径是最稳的。
3. 把 WTL 10.0 配进 VS2019:include 路径、最小窗口工程和链接配置
3.1 建一个空 C++ 工程,把 WTL 的 include 目录配进去
打开 VS2019,新建一个“空项目”,语言选 C++。不用选“Windows 桌面应用程序”模板,那个模板默认带了一堆预编译头和框架代码,对 WTL 反而是干扰。
拿到 WTL 10.0 源码包后解压,记住这个路径结构:顶层有include、Samples、AppWizard等目录。我们真正需要的是include目录,里面是一堆.h头文件,没有.cpp。
然后打开项目属性:
- 配置属性 → VC++ 目录 → 包含目录,把
WTL 解压路径\include加进去 - 配置属性 → C/C++ → 预处理器 → 预处理器定义,确认有
WIN32、_WINDOWS、_UNICODE、UNICODE - 配置属性 → 链接器 → 系统 → 子系统,选“窗口 (/SUBSYSTEM:WINDOWS)”
现在先不写代码,有个预处理宏值得顺手配好:_WTL_NO_CSTRING。这个宏控制 WTL 是否自动依赖CString。如果你不想让 WTL 强制把 ATL 的CString拉进编译单元,可以定义它。但绝大多数项目直接用CString更省事,我一般不会定义它,而是保留 WTL 默认行为。
3.2 最小可运行的窗口程序:从 _tWinMain 到消息循环
在工程里新建一个main.cpp,把下面的代码完整贴进去,这是 WTL 10.0 在 VS2019 下能跑通的最小骨架:
#include <atlbase.h> #include <atlapp.h> #include <atlwin.h> #include <atlframe.h> // WTL 的全局模块对象,很多内部宏会引用它 CAppModule _Module; // 主窗口类:继承 CFrameWindowImpl,模板参数传自己的类名 class CMainFrame : public CFrameWindowImpl<CMainFrame> { public: // 注册窗口类名 MainFrameWnd,不使用菜单 DECLARE_FRAME_WND_CLASS(_T("MainFrameWnd"), 0) // 消息映射表:声明两个消息分发 BEGIN_MSG_MAP(CMainFrame) MESSAGE_HANDLER(WM_CLOSE, OnClose) MESSAGE_HANDLER(WM_DESTROY, OnDestroy) END_MSG_MAP() // 收到 WM_CLOSE 时销毁窗口 LRESULT OnClose(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled) { DestroyWindow(); return 0; } // 窗口销毁后向消息循环投递 WM_QUIT LRESULT OnDestroy(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled) { PostQuitMessage(0); return 0; } }; int WINAPI _tWinMain(HINSTANCE hInstance, HINSTANCE, LPTSTR, int nCmdShow) { // 初始化全局模块:第一个参数传 NULL 表示不关联资源模块 _Module.Init(NULL, hInstance); CMainFrame wnd; // 创建窗口:标题命名为 WTL 10.0 on VS2019 if (wnd.Create(NULL, CWindow::rcDefault, _T("WTL 10.0 on VS2019")) == NULL) { _Module.Term(); return 1; } wnd.ShowWindow(nCmdShow); // 启动消息循环,Run 返回 WM_QUIT 携带的退出码 CMessageLoop theLoop; int nRet = theLoop.Run(); _Module.Term(); return nRet; }逻辑说明:atlapp.h提供CAppModule和CMessageLoop,CFrameWindowImpl在atlframe.h中定义,它已经包含了窗口注册、创建、消息映射链到基类的默认行为。DECLARE_FRAME_WND_CLASS宏会生成本类自己的窗口类注册代码,第一个参数是类名,第二个参数是菜单资源 ID。
参数说明:_Module.Init的第一参数NULL表示资源句柄跟随实例句柄hInstance,如果你的资源 DLL 和主程序分开,第一个参数要换成资源 DLL 的HINSTANCE。wnd.Create的第二个参数CWindow::rcDefault让系统给窗口一个默认大小,第三个参数是窗口标题。CMessageLoop::Run()只有在收到WM_QUIT后才返回,返回值可以作为进程退出码。
3.3 撞上 LNK2019 / LNK2001:入口点、字符集和子系统的三个必查项
第一次编译这个工程,最常见的错误是链接器提示找不到wWinMain或WinMain。这是因为_tWinMain是一个宏,在定义了_UNICODE时被预处理器翻译成wWinMain,没定义时翻译成WinMain。VS2019 默认空工程不会替你决定入口点,所以你要检查三处:
- 链接器 → 系统 → 子系统,必须是“窗口”,不能是“控制台”
- 链接器 → 高级 → 入口点,填
wWinMainCRTStartup。如果你觉得自己没定义 Unicode,则对应WinMainCRTStartup - 常规 → 字符集,选“使用 Unicode 字符集”,保证
_UNICODE和UNICODE被定义
我个人建议不用管链接器入口点,而是直接删掉_tWinMain里的_t,写标准签名wWinMain,然后显式把字符集设成 Unicode。这样代码和配置一眼就能对上,省得宏在中间变来变去。出现LNK2001时,先看这两处,比乱加 pragma comment 靠谱。
提示:入口点不等于函数名。你写的
wWinMain是用户入口函数,链接器去找的启动函数是wWinMainCRTStartup,这两者在内部有调用关系,配置时不要填错。
4. 用 WTL 10.0 写界面:消息映射宏、对话框控件读写和属性页
4.1 消息映射宏的三种形态:MESSAGE_HANDLER、COMMAND_ID_HANDLER、NOTIFY_HANDLER
WTL 的界面核心是消息映射宏。一个窗口要处理哪条消息、哪个命令、哪个通知,全部写在同一张BEGIN_MSG_MAP/END_MSG_MAP表里。宏展开后,编译器会生成一个查找函数,按消息 ID 分发表项。写法上有三种用途,对应三类消息来源:
MESSAGE_HANDLER(WM_xxx, Func):处理窗口消息,比如WM_CLOSE、WM_SIZE、WM_PAINTCOMMAND_ID_HANDLER(控件ID, Func):处理来自菜单、按钮的WM_COMMANDNOTIFY_HANDLER(控件ID, 通知码, Func):处理WM_NOTIFY,比如列表控件LVN_ITEMCHANGED
函数签名是统一的:LRESULT Func(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled)。注意最后一个参数bHandled,如果你想让它继续传给下一张消息映射表,就设成FALSE;设成TRUE(或不改)表示本层已经处理完。
另外两个高频宏一定要认识:CHAIN_MSG_MAP(基类)会把未处理消息链到基类,比如你继承CFrameWindowImpl时,必须用CHAIN_MSG_MAP(CFrameWindowImpl<你的类>)才能让默认窗口行为生效。REFLECT_NOTIFICATIONS()用于对话框里的子控件,把控件发来的通知反射回控件自身处理,适合封装自绘控件时用。
4.2 对话框控件的创建与读写:从 .rc 资源到 CEdit/CButton 绑定
WTL 的对话框不是靠拖控件生成代码,而是靠资源编辑器画界面,再用CDialogImpl模板类配合控件 ID 做读写。先给一个能编译的.rc片段:
IDD_MAIN_DLG DIALOGEX 0, 0, 220, 140 STYLE DS_SETFONT | WS_POPUP | WS_CAPTION | WS_SYSMENU CAPTION "WTL Dialog" FONT 9, "Microsoft YaHei" BEGIN LTEXT "Name:", IDC_STATIC_NAME, 10, 10, 40, 8 EDITTEXT IDC_EDIT_NAME, 55, 10, 100, 14 DEFPUSHBUTTON "OK", IDC_BTN_OK, 130, 100, 40, 14 PUSHBUTTON "Cancel", IDC_BTN_CANCEL, 175, 100, 40, 14 CONTROL "", IDC_LIST1, "SysListView32", LVS_REPORT, 10, 30, 205, 60 END对应的对话框类:
#include <atlbase.h> #include <atlapp.h> #include <atldlgs.h> #include <atlctrls.h> #include <atlmisc.h> class CMainDlg : public CDialogImpl<CMainDlg> { public: enum { IDD = IDD_MAIN_DLG }; // 关联资源 ID BEGIN_MSG_MAP(CMainDlg) MESSAGE_HANDLER(WM_INITDIALOG, OnInitDialog) COMMAND_ID_HANDLER(IDC_BTN_OK, OnBtnOK) COMMAND_ID_HANDLER(IDC_BTN_CANCEL, OnBtnCancel) COMMAND_ID_HANDLER(IDCANCEL, OnCancel) // 处理 Esc 键默认关闭 END_MSG_MAP() // 对话框初始化 LRESULT OnInitDialog(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled) { // 给编辑框设一个初始文本 SetDlgItemText(IDC_EDIT_NAME, _T("WTL")); // 给列表控件加一列 m_list.Attach(GetDlgItem(IDC_LIST1)); m_list.InsertColumn(0, _T("Item"), LVCFMT_LEFT, 180); m_list.InsertItem(0, _T("hello")); return TRUE; // TRUE 让系统不再设置默认焦点 } // 点击 OK 按钮 LRESULT OnBtnOK(WORD wNotifyCode, WORD wID, HWND hWndCtl, BOOL& bHandled) { CString strName; GetDlgItemText(IDC_EDIT_NAME, strName); MessageBox(strName, _T("Input"), MB_OK); return 0; } // 点击 Cancel 按钮或按 Esc LRESULT OnBtnCancel(WORD wNotifyCode, WORD wID, HWND hWndCtl, BOOL& bHandled) { EndDialog(IDCANCEL); return 0; } LRESULT OnCancel(WORD wNotifyCode, WORD wID, HWND hWndCtl, BOOL& bHandled) { EndDialog(IDCANCEL); return 0; } CListViewCtrl m_list; // WTL 对 SysListView32 的封装 };逻辑说明:CDialogImpl要求你定义enum { IDD = 资源ID },这是它定位对话框模板的约定。WM_INITDIALOG是对话框创建后的初始化时机,排序必须在其它消息之前。SetDlgItemText、GetDlgItemText是 WTL 继承自CWindow的便捷方法,内部自动包装SendMessage。这里我用m_list.Attach(GetDlgItem(IDC_LIST1))把列表控件句柄“附加”到CListViewCtrl封装对象上,之后就能调InsertColumn、InsertItem这些看着像 MFC 的接口。
参数说明:COMMAND_ID_HANDLER的回调四个参数是WORD wNotifyCode, WORD wID, HWND hWndCtl, BOOL& bHandled,和裸WM_COMMAND的参数布局一致。IDCANCEL是系统保留的取消命令 ID,必须在映射表里单独处理,否则按 Esc 对话框不会退出。
4.3 多页设置界面:从 CPropertyPageImpl 到 CPropertySheetImpl
WTL 10.0 的属性页机制已经相当成熟,写多页设置界面比 MFC 还顺手。每一页继承CPropertyPageImpl,整体对话框继承CPropertySheetImpl。先看一页的写法:
class CSettingsPage1 : public CPropertyPageImpl<CSettingsPage1> { public: enum { IDD = IDD_PAGE1 }; BEGIN_MSG_MAP(CSettingsPage1) COMMAND_ID_HANDLER(IDC_BTN_APPLY, OnApplyClicked) CHAIN_MSG_MAP(CPropertyPageImpl<CSettingsPage1>) END_MSG_MAP() // 点击“应用”按钮时,把页面标记为“已修改” LRESULT OnApplyClicked(WORD wNotifyCode, WORD wID, HWND hWndCtl, BOOL& bHandled) { SetModified(); // 这句会让属性页的“应用”按钮亮起来 return 0; } // 用户点击“应用”或“确定”时,属性页框架回调这个虚函数 BOOL OnApply() { int nValue = GetDlgItemInt(IDC_EDIT_VALUE); // 这里做实际保存 SetDlgItemInt(IDC_STATIC_RESULT, nValue * 2); return TRUE; // 返回 TRUE 表示应用成功 } };属性页容器和调用:
class CMainSheet : public CPropertySheetImpl<CMainSheet> { public: CMainSheet(LPCTSTR pszTitle, HWND hWndParent = NULL) : CPropertySheetImpl<CMainSheet>(pszTitle, hWndParent) { m_page1.Create(GetActivePage() ? GetActivePage()->m_hWnd : NULL); AddPage(m_page1); } CSettingsPage1 m_page1; }; // 在代码里这样打开模态属性页: // CMainSheet sheet(_T("Settings")); // sheet.DoModal();逻辑说明:CPropertyPageImpl的OnApply是一个虚函数,不是消息处理函数,属性页框架会在用户点“应用”时自动调用它。SetModified()的作用是把当前页标记为脏,让属性页底部的“应用”按钮从禁用变成可用。你不需要自己显示这个按钮,系统属性页框架会处理。
参数说明:AddPage接受页对象的引用,页对象生命周期必须比CMainSheet长,所以把m_page1声明为CMainSheet的成员。DoModal()跑完整的模态循环,返回值是用户点的结束按钮 ID。
5. WTL 10.0 在 VS2019 下的避坑指南:从编译错误到界面模糊的五个踩坑记录
5.1 打不开 atlbase.h:ATL 组件缺失和离线安装的顺序
现象:新建的工程一编译,报fatal error C1083: 无法打开包括文件: "atlbase.h": No such file or directory。
原因:VS2019 默认不装 ATL 组件,atlbase.h压根不存在于你的 VS 安装目录里。这和 WTL 无关,是 VS 自身的组件裁剪问题。
解决:打开 Visual Studio Installer,修改安装,在“单个组件”里搜索ATL,勾选“适用于最新 v142 生成工具的 C++ ATL (x86 & x64)”,安装完成后重启。如果你用的是 vs2019 离线安装包,需要在离线布局阶段就把这个组件加进去,否则装完还得再拉一次在线流量。
5.2 旧版 WTL 在 VS2019 上报 C4430:ATL 版本检测失效
现象:工程里用的是 WTL 9.1 或更早,升级到 VS2019 后,atlapp.h编译时出现error C4430: 缺少类型说明符或error C2061: 语法错误,错误位置在版本宏附近。
原因:WTL 9.1 内部用_ATL_VER判断 ATL 是否支持某特性,VS2019 的 v142 工具集带的 ATL 版本号超出了旧 WTL 的预期值,条件编译走进了不存在的分支,后面的代码全部错位。这是 9.1 与 VS2019 兼容性差的关键。
解决:升级到 WTL 10.0 最终版本,它的版本检测逻辑是重写过的。临时应付的办法是在包含任何 WTL 头文件之前#define _WTL_NO_ATL_VER,跳过检测,但不保证所有特性都正常,我只把它当诊断手段用,不会正式发布。
5.3 链接报 LNK2001 _Module:全局模块对象没定义或漏了类型
现象:编译通过,链接时报error LNK2001: 无法解析的外部符号 "class CAppModule _Module"。
原因:WTL 的atlapp.h内部有一堆宏引用全局变量_Module,但你的工程里没有定义这个全局对象,或者定义了但拼写、类型不对。这不算 WTL 的 bug,而是它约定俗成的全局入口。
解决:在包含 WTL 头文件的某个.cpp文件顶部,加上CAppModule _Module;。注意类型必须是CAppModule,不要用CComModule。WTL 10 里CAppModule才是针对线程消息循环优化的类型,CComModule是 COM 场景用的。
5.4 中文乱码或 C2664:字符集不匹配的判定与统一
现象:运行后界面中文变成问号,或者编译时字符串相关函数报C2664: 无法将参数从“wchar_t *”转换为“const char *”。
原因:工程属性里字符集选了“多字节字符集”,但代码里用了_T()宏,资源文件里用的是宽字符格式,两者在_UNICODE未定义时拧着来。最麻烦的是,同一个工程里一部分文件用了L"",另一部分用了"",拼起来就是随机乱码。
解决:全部统一到 Unicode。工程属性 → 配置属性 → 常规 → 字符集 → 使用 Unicode 字符集。代码里字符串只用_T("..."),入口函数写成wWinMain,文件保存为 UTF-8 with BOM。这样处理之后,C2664 基本绝迹,乱码也自然消失。
5.5 界面在高 DPI 屏幕上发虚:WTL 需要自己开 DPI 感知
现象:在 150% 缩放的笔记本上运行 WTL 程序,窗口和文字都是糊的,像被强制放大了一倍。
原因:WTL 10.0 不会自动调用 DPI 感知接口,系统把整个窗口当普通位图做缩放拉伸。这是很多老旧 Win32 框架的通病,不是 WTL 特例。
解决:在_Module.Init之前调用 DPI 感知设置。Win10 上推荐SetProcessDpiAwareness(PROCESS_PER_MONITOR_DPI_AWARE),需要Shcore.lib。老系统兼容做法是动态加载,这个放到下一章的代码里直接抄。
注意:DPI 感知设置必须在创建任何窗口之前调用,放在
_Module.Init前面最保险。运行后再调用不会生效。
6. 让 WTL 10.0 项目再稳一点:迁移验证的三个检查点和 DPI 修正
6.1 从老版本迁到 10.0,按这三个顺序验证
如果你手里是 WTL 8/9 的项目,头文件路径换成 10.0 的 include 之后,先别急着跑业务测试。我习惯按三个顺序过:第一是编译告警,重点看GetWindowLongPtr、reinterpret_cast相关的行,WTL 10 修了 64 位隐患,老代码里常见的LONG到指针转换在这里会暴露;第二是消息反射,对话框里如果有自绘控件,跑一遍打开、关闭、切换页,确认REFLECT_NOTIFICATIONS()行为没变,WTL 10 对通知反射的触发时机有调整;第三是资源,把.rc里DIALOGEX和控件定义的写法跟 SDK 头文件对齐,老工程里过时的控件样式BS_ICON之类的在新头文件下可能失效。
6.2 DPI 感知的代码级修正:入口处加三行
最能立竿见影的技巧就是把这个函数贴到_Module.Init之前:
#include <shellscalingapi.h> #pragma comment(lib, "Shcore.lib") // 在 _Module.Init(NULL, hInstance) 之前调用 if (SetProcessDpiAwareness(PROCESS_PER_MONITOR_DPI_AWARE) != S_OK) { // 老系统上可能失败,回退到系统级感知 SetProcessDPIAware(); }逻辑说明:PROCESS_PER_MONITOR_DPI_AWARE让系统按每个显示器实际缩放比渲染窗口,多显示器接不同缩放时,窗口不会糊。SetProcessDPIAware是老一代接口,只在系统级生效,双屏切换时可能仍会有轻微缩放,属于保底方案。
参数说明:SetProcessDpiAwareness的枚举值是PROCESS_DPI_UNAWARE、PROCESS_SYSTEM_DPI_AWARE、PROCESS_PER_MONITOR_DPI_AWARE三个,选最后一个即可。如果程序将来要切到 VS2022,这个调用不变,WTL 10.0 在 VS2022 上同样适用。
做完这些,窗口在高 DPI 下清晰了,老项目迁移也能安全落地。我现在的习惯是每接一个老 WTL 项目,先按 6.1 的顺序做一遍体检,再改 DPI,最后才动业务代码。这个顺序帮我挡掉了不少上线后的模糊、崩溃类返工,希望帮到你。
本文还有配套的精品资源,点击获取