简介这份示例源码面向仍在维护 VC6.0 老项目的开发者聚焦旧环境缺少原生 PNG 支持时如何借助 GDI 完成图片加载、显示与 Alpha 透明度控制。压缩包共 70 个文件约 4.73MB以 34 个头文件、8 张 PNG 素材、3 个 cpp 源文件为主并附带工程文件、lib 库、资源脚本、可执行文件及调试产物目录中既有 GDI 头文件与静态库也有可参考的对话框工程、时钟界面素材和透明图片。已有 716 人学习下载。读者可据此了解 GDI 初始化与关闭、位图加载、颜色矩阵调节透明度、图像绘制和资源释放等关键环节也能从完整工程中看到头文件引用、库依赖与项目配置的对应关系理解 VC6.0 下透明 PNG 的接入方式对兼容旧代码、排查透明异常或学习 GDI 绘图流程有参考意义适合作为老项目图像功能改造和课堂演示的对照样例。1. VC6.0 里做 PNG 透明处理这套工程到底解决了什么VC6.0 里做 PNG 透明处理卡住的环节往往不在解码而在解码之后那层 Alpha 怎么正确混到背景上。手里这套工程就是做这件事的一个 MFC 对话框程序资源编辑器只认 BMP界面却要贴设计师给的带羽化边缘的 PNG 图标、Logo 和状态灯。它的做法是把 PNG 交给 GDI 解成 32 位带 Alpha 的 DIB预乘 alpha 之后用 AlphaBlend 往目标 DC 上混全程不换编译器、不引入额外运行时。适合两类人还在维护 2000 年代老 MFC 工程、没法升级开发环境的以及想搞明白“透明”在像素层面究竟发生了什么而不满足于抄个函数直接用的人。2. 技术选型对比GDI、libpng 加 AlphaBlend、FreeImage 三条路怎么选VC6 时代没有 CImageATL 那套东西要 VS2003 才有GDI 本身连 PNG 都不认识。所以“加载 PNG”这件事在 VC6 里没有零成本方案只有三种代价不同的方案借 GDI 的解码器、自己把 libpng 编进工程、或者挂一个第三方图像库。选哪条路取决于三件事——目标机器最低是哪个系统、能不能接受多带一个 dll、以及团队里有没有人愿意编译 libpng。下面把三条路摊开讲重点是每条路的失败模式因为在老工程里能不能跑起来往往由最弱的那一环决定。2.1 GDI 路线一个 Bitmap::FromFile 就把解码包了GDI 从 Windows 2000 起就是系统组件XP 之后基本不用操心分发。它内置了 PNG 编解码器Bitmap::FromFile一行就把调色板、灰度、16 位、隔行扫描这些分支全覆盖了返回的对象自己知道怎么按 32 位 ARGB 取像素。对 VC6 来说代价只有两个一是工程里要能拿到gdiplus.h和gdiplus.lib这两个不在 VC6 自带的老 SDK 里得挂一份 Platform SDK 的头文件目录二是运行机器上要有gdiplus.dllWindows 98/Me 需要额外装一次分发包。初始化就三行但位置很关键// 整个进程只需要一次放在 CWinApp::InitInstance 的最前面 #include algorithm // gdiplus.h 内部用了 min/max先把它引进来 using namespace std; #include gdiplus.h #pragma comment(lib, gdiplus.lib) ULONG_PTR g_nGdiplusToken 0; BOOL CPngDemoApp::InitInstance() { Gdiplus::GdiplusStartupInput input; // 结构体四个字段的常用取值 // GdiplusVersion 1固定值别改 // DebugEventCallback NULL不挂调试回调 // SuppressBackgroundThread FALSE正常情况不要抑制后台线程 // SuppressExternalCodecs FALSE必须为 FALSE否则读不到系统自带的 PNG 解码器 if (Gdiplus::GdiplusStartup(g_nGdiplusToken, input, NULL) ! Gdiplus::Ok) return FALSE; // 起不来就让程序退掉别带着半残状态跑 // ... 后面的对话框创建代码 return TRUE; }SuppressExternalCodecs这个参数写错会让Bitmap::FromFile对一个正常 PNG 返回 NULL而且没有任何错误提示这属于排查时最费时间的一类问题。gdiplus.lib别忘了加到工程设置里否则链接阶段报一堆GdiplusStartup未解析。2.2 libpng 加 AlphaBlend不依赖 gdiplus.dll 的裸解码方案如果目标机器里有 Windows 98或者项目对“单一 exe、不依赖系统组件”有硬要求那就得把 libpng 和 zlib 的源码直接编进工程。这条路的工作量明显大一截VC6 编译 libpng 需要打几个补丁补stdint.h、把snprintf换成_snprintf、处理inline关键字然后你要自己把解码结果整理成一张 32 位 BGRA 的 DIB——调色板 PNG 要png_set_palette_to_rgb灰度要png_set_gray_to_rgb16 位要png_set_strip_16带 tRNS 的还要png_set_tRNS_to_alpha。少一个转换出来的图颜色就是错的。核心解码部分大概是这样// 用 libpng 把文件解成 4 通道 RGBA再转成 DIB 需要的 BGRA 顺序 png_structp png png_create_read_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL); png_infop info png_create_info_struct(png); FILE* fp _tfopen(lpszPath, _T(rb)); png_init_io(png, fp); png_read_info(png, info); png_uint_32 w png_get_image_width(png, info); png_uint_32 h png_get_image_height(png, info); int colorType png_get_color_type(png, info); int bitDepth png_get_bit_depth(png, info); // 统一成 8 位 RGBA分支越少后面越好维护 if (bitDepth 16) png_set_strip_16(png); if (colorType PNG_COLOR_TYPE_PALETTE) png_set_palette_to_rgb(png); if (colorType PNG_COLOR_TYPE_GRAY bitDepth 8) png_set_expand_gray_1_2_4_to_8(png); if (png_get_valid(png, info, PNG_INFO_tRNS)) png_set_tRNS_to_alpha(png); if (colorType PNG_COLOR_TYPE_RGB || colorType PNG_COLOR_TYPE_GRAY) png_set_filler(png, 0xFF, PNG_FILLER_AFTER); // 没有 Alpha 的图补成不透明 png_read_update_info(png, info); // 注意DIB 是 BGRAlibpng 给的是 RGBA逐像素交换 R 和 B BYTE* pRaw new BYTE[w * h * 4]; png_bytep* rows new png_bytep[h]; for (png_uint_32 y 0; y h; y) rows[y] pRaw y * w * 4; png_read_image(png, rows);参数上唯一容易踩的是png_set_filler(png, 0xFF, PNG_FILLER_AFTER)填充值给 0xFF 表示“补成完全不透明”给 0x00 就是整图全透明。这个值写错现象和后面要讲的“24 位 PNG 消失”一模一样排查方向却完全不同。2.3 三条路线的对照与我的取舍维度GDIlibpng AlphaBlendFreeImage / CxImage额外依赖系统 gdiplus.dllzlib libpng 静态编入第三方 dll 或静态库VC6 亲和度挂 SDK 头文件后可用需打补丁工作量大需改工程文件视版本而定支持格式PNG/JPG/GIF/BMP/TIFF只有 PNG几乎全覆盖代码量最少中等最少适合场景目标系统 2000 以上极老系统、禁止外部依赖需要多格式且能带库我一般默认走 GDI解码不用自己维护Alpha 语义由系统统一出问题的面小。只有确认目标机器里有 Windows 98或者甲方明确要求单文件绿色版才会退到 libpng。FreeImage 那条路在 VC6 下的坑主要在工程配置它的 .dsp 是给高版本写的除非项目本来就在用否则不值得为这一件事引入。3. 工程骨架搭建让 gdiplus.h 在 VC6 里编译通过的目录与链接配置选型定了之后真正卡人的往往不是代码而是工程配置。VC6 自带的 Platform SDK 版本太老直接#include gdiplus.h会报找不到文件挂上新 SDK 之后又可能和 MFC 的min/max宏打架。这一章把工程骨架和配置一次性理干净后面写代码才不会被环境问题反复打断。3.1 目录与文件清单一个能编译、能跑、能改的最小工程大概长这样路径作用PngDemo.dsw/PngDemo.dspVC6 工程与工作区src/StdAfx.h/StdAfx.cpp预编译头只放稳定的系统头src/PngDemoApp.h/.cppCWinApp 派生类GDI 的 Startup/Shutdown 落在这里src/PngDemoDlg.h/.cpp主对话框负责绘制与消息处理src/PngImage.h/.cppPNG 封装类加载、预乘、绘制都在这里res/logo.png、res/btn_ok.png测试用的透明 PNG 素材res/PngDemo.rc/resource.h对话框模板与资源 ID把 PNG 封装单独放一对文件是为了让后面二次开发时能直接把这四个文件拷到别的工程里用不用再翻对话框代码。3.2 工程配置头文件路径、链接库与字符集配置分三步顺序别乱。第一步告诉 VC6 去哪找 GDI 的头文件和库。菜单Tools → Options → Directories在Include files里把 Platform SDK 的Include目录加到最上面在Library files里把对应的Lib目录加上。加完重启 IDEVC6 的目录缓存有时候不刷新改完不生效会让人怀疑人生。第二步Project → Settings → Link在Object/library modules里补上两个库gdiplus.lib msimg32.libgdiplus.lib给Bitmap、Graphics这些类用msimg32.lib给AlphaBlend用。后者忘记加报的是unresolved external symbol __imp__AlphaBlend44一眼能认出来。第三步Project → Settings → C/C → Preprocessor definitions里补上版本宏WINVER0x0500,_WIN32_WINNT0x05000x0500对应 Windows 2000。不定义的话BLENDFUNCTION、AC_SRC_ALPHA这些符号在wingdi.h里可能被条件编译掉报未定义标识符。同时检查Project → Settings → General里的字符集如果工程是 MBCSVC6 默认Bitmap::FromFile要的是宽字符路径中间得转一次选_UNICODE的话T2W直接就是空操作代码能通用。3.3 GDI 生命周期Startup 放 InitInstanceShutdown 放 ExitInstanceGDI 是进程级单例启动一次、关闭一次中间所有Bitmap、Graphics对象都依赖它活着。所以GdiplusShutdown必须发生在所有 GDI 对象析构之后否则就是析构时访问已卸载模块的崩溃堆栈还看不出来是谁。// PngDemoApp.cpp BOOL CPngDemoApp::InitInstance() { // GDI 里所有绘图对象都依赖这个 token失败就直接退出 Gdiplus::GdiplusStartupInput input; if (Gdiplus::GdiplusStartup(g_nGdiplusToken, input, NULL) ! Gdiplus::Ok) return FALSE; CPngDemoDlg dlg; m_pMainWnd dlg; dlg.DoModal(); // 注意这里不要返回要到 ExitInstance 里关 GDI // 因为 dlg 的成员CPngImage 里的 Bitmap还没析构 return FALSE; } int CPngDemoApp::ExitInstance() { if (g_nGdiplusToken ! 0) { Gdiplus::GdiplusShutdown(g_nGdiplusToken); // 此时所有 GDI 对象都已销毁 g_nGdiplusToken 0; // 置零防止重复关闭 } return CWinApp::ExitInstance(); }这里有个血泪经验的细节如果CPngImage里有Gdiplus::Bitmap*成员而它作为全局对象或者静态成员存在那么它的析构顺序在ExitInstance之后会在GdiplusShutdown之后才执行结果是退出时必崩。解决办法很简单——不要在全局作用域放 GDI 对象把图片句柄全部收进对话框或者专门的缓存类里让它们随窗口一起销毁。4. 核心链路拆解从 PNG 文件到屏幕像素的 Load、预乘、AlphaBlend 三步这一段是整套方案的核心。从文件到屏幕中间其实发生了四次数据变形文件字节流 → GDI 的 Bitmap 对象 → LockBits 拿到的原始像素指针 → 预乘后的 DIB → 屏幕。有任何一个环节的语义对不上现象都是“图不对”但原因完全不同。下面按顺序拆。4.1 解码与取像素FromFile 之后为什么还要再拷一张 32 位画布Bitmap::FromFile返回的对象像素格式是“跟文件里一致”的——可能 24bpp可能 8bpp 调色板也可能 32bpp。直接对它LockBits拿到的缓冲区布局不固定Stride是正还是负也不一定取决于源图是自顶向下还是自底向上。所以在取像素之前先统一“搬运”到一张 32bppARGB 的画布上这一步还顺手解决了“没有 Alpha 通道的 PNG 怎么办”往一张清成完全透明的画布上做 SourceOver 绘制不透明像素画完之后 Alpha 自然变成 255。BOOL CPngImage::Load(LPCTSTR lpszPath) { Release(); // 先释放上一次的句柄支持重复调用 USES_CONVERSION; WCHAR* pwszPath T2W((LPTSTR)lpszPath); // MBCS 工程下这步不能省 Gdiplus::Bitmap* pSrc Gdiplus::Bitmap::FromFile(pwszPath, FALSE); if (pSrc NULL || pSrc-GetLastStatus() ! Gdiplus::Ok) { delete pSrc; return FALSE; } m_nWidth pSrc-GetWidth(); m_nHeight pSrc-GetHeight(); // 统一画布无论源是 24bpp 还是调色板这里都变成 32bppARGB Gdiplus::Bitmap* p32 new Gdiplus::Bitmap(m_nWidth, m_nHeight, PixelFormat32bppARGB); { Gdiplus::Graphics g(p32); g.Clear(Gdiplus::Color(0, 0, 0, 0)); // 清成全透明别留未初始化内存 g.SetInterpolationMode(InterpolationModeNearestNeighbor); g.DrawImage(pSrc, Gdiplus::Rect(0, 0, m_nWidth, m_nHeight), 0, 0, m_nWidth, m_nHeight, Gdiplus::UnitPixel); } // 取出原始像素 Gdiplus::Rect rc(0, 0, m_nWidth, m_nHeight); Gdiplus::BitmapData bd; if (p32-LockBits(rc, Gdiplus::ImageLockModeRead, PixelFormat32bppARGB, bd) ! Gdiplus::Ok) { delete p32; delete pSrc; return FALSE; } // ... 见 4.2此时 bd.Scan0 指向首行bd.Stride 是行字节数 }几个参数值得单独说ImageLockModeRead只管读改像素不会回写PixelFormat32bppARGB是请求格式GDI 会做一次转换所以拿到的bd.Stride一定是 4 的倍数InterpolationModeNearestNeighbor在这里是为了“不插值”——尺寸没变的情况下 GDI 本来也不会插值但显式设一下可以避免某些版本上的边缘抖动因为后面我们自己还要乘一次 Alpha被插值污染过的颜色会算不准。4.2 预乘 AlphaAC_SRC_ALPHA 之前必须做的那次乘法这是整个流程里最容易漏、也最难从现象反推的一步。AlphaBlend的AC_SRC_ALPHA语义是源像素的颜色分量已经预先乘过它自己的 Alpha。公式是R R × A / 255 G G × A / 255 B B × A / 255 A A为什么必须是预乘因为混合公式是目标 源 目标 × (1 - A)。如果源没预乘实际算出来就变成目标 源 × A 目标 × (1 - A)之外的另一种结果——半透明区域会偏亮边缘出现一圈泛白的“毛边”完全透明的像素A0如果 RGB 还是原色更会直接把整片区域点亮成色块。预乘之后A0 的像素 RGB 也是 0加过去等于没加这才是正确行为。// 建一张自顶向下的 32 位 DIB负高度就是告诉系统“第一行是顶行” BITMAPINFO bi; ZeroMemory(bi, sizeof(bi)); bi.bmiHeader.biSize sizeof(BITMAPINFOHEADER); bi.bmiHeader.biWidth m_nWidth; bi.bmiHeader.biHeight -m_nHeight; bi.bmiHeader.biPlanes 1; bi.bmiHeader.biBitCount 32; bi.bmiHeader.biCompression BI_RGB; // 32 位不用写掩码 void* pBits NULL; m_hBmp CreateDIBSection(NULL, bi, DIB_RGB_COLORS, pBits, NULL, 0); if (m_hBmp NULL || pBits NULL) { p32-UnlockBits(bd); delete p32; delete pSrc; return FALSE; } // 逐像素拷贝并预乘。DIB 是 BGRAGDI 的 32bppARGB 内存布局同样是 BGRA BYTE* pSrcBase (BYTE*)bd.Scan0; int nStride bd.Stride 0 ? bd.Stride : -bd.Stride; // 防御性处理负 Stride BYTE* pDst (BYTE*)pBits; for (int y 0; y m_nHeight; y) { BYTE* s pSrcBase y * nStride; BYTE* d pDst y * m_nWidth * 4; for (int x 0; x m_nWidth; x) { BYTE a s[3]; // 127 是四舍五入整数除法也能贴到最近的整数 d[0] (BYTE)((s[0] * a 127) / 255); // B d[1] (BYTE)((s[1] * a 127) / 255); // G d[2] (BYTE)((s[2] * a 127) / 255); // R d[3] a; // A 原样保留 s 4; d 4; } } p32-UnlockBits(bd); delete p32; delete pSrc; // 把 DIB 挂到内存 DC 上后面 AlphaBlend 直接从这取 HDC hScreen ::GetDC(NULL); m_hMemDC ::CreateCompatibleDC(hScreen); ::ReleaseDC(NULL, hScreen); m_hOldBmp (HBITMAP)::SelectObject(m_hMemDC, m_hBmp); return TRUE;biHeight用负数这个技巧值得记住GDI 的 DIB 默认是自底向上存的用负高度可以让内存里的第一行就是图像的第一行跟 GDI 的 Scan0 顺序一致省掉一次整图翻转。翻转代码写错了图会上下颠倒这个现象比颜色错误好认得多。4.3 绘制内存 DC 加 AlphaBlend 的参数到底怎么填有了内存 DC绘制本身只有一个函数调用但BLENDFUNCTION四个字段里有两个是必须盯死的。void CPngImage::Draw(HDC hdc, int x, int y, int cx, int cy, BYTE nAlpha) { if (m_hMemDC NULL || m_hBmp NULL) return; BLENDFUNCTION bf; bf.BlendOp AC_SRC_OVER; // 只支持这一个值 bf.BlendFlags 0; // 必须为 0 bf.SourceConstantAlpha nAlpha; // 整体透明度255 表示保持原样 bf.AlphaFormat AC_SRC_ALPHA; // 关键告诉系统源是“预乘过的逐像素 Alpha” ::AlphaBlend(hdc, x, y, cx, cy, m_hMemDC, 0, 0, m_nWidth, m_nHeight, bf); }SourceConstantAlpha和AC_SRC_ALPHA是可以叠加的系统会先按逐像素 Alpha 混再乘整体透明度。所以做淡入淡出动画时直接改第四个参数从 0 到 255 就行不用重新预乘一遍。但要注意如果传了AC_SRC_ALPHA但像素其实是没预乘的SourceConstantAlpha调得越小边缘的白边越明显——这是判断预乘有没有做对的快捷方法。另外AlphaBlend的目标区域和源区域尺寸可以不一样它内部会做拉伸但拉伸质量一般不适合做高清缩放。如果项目里有“图标按 DPI 缩放”的需求最好在加载阶段就用Graphics::DrawImage缩好再预乘而不是在绘制时让AlphaBlend硬拉。4.4 对话框上的透明化WM_ERASEBKGND、双缓冲与 OnCtlColor图能画出来只是第一步贴到对话框上不闪、不白、不盖住文字才算完。MFC 对话框默认会在OnPaint之前用背景刷把整个客户区刷一遍这一刷就是闪烁的根源同时静态文本控件又会自己刷一块底色把刚画好的 PNG 遮住。// 关掉系统擦背景避免同一次重绘刷两遍 BOOL CPngDemoDlg::OnEraseBkgnd(CDC* pDC) { return TRUE; } void CPngDemoDlg::OnPaint() { CPaintDC dc(this); CRect rcClient; GetClientRect(rcClient); // 双缓冲背景和图片全部画进内存 DC最后一次性贴到屏幕 CDC memDC; memDC.CreateCompatibleDC(dc); CBitmap bmp; bmp.CreateCompatibleBitmap(dc, rcClient.Width(), rcClient.Height()); CBitmap* pOld memDC.SelectObject(bmp); memDC.FillSolidRect(rcClient, ::GetSysColor(COLOR_3DFACE)); // 贴 Logo尺寸取原图第三个参数 200 表示整体 78% 不透明 m_imgLogo.Draw(memDC.GetSafeHdc(), 20, 20, m_imgLogo.GetWidth(), m_imgLogo.GetHeight(), 200); dc.BitBlt(0, 0, rcClient.Width(), rcClient.Height(), memDC, 0, 0, SRCCOPY); memDC.SelectObject(pOld); // 必须还原否则 bmp 析构时选不出去 } // 让静态控件不刷底色PNG 才不会被盖住 HBRUSH CPngDemoDlg::OnCtlColor(CDC* pDC, CWnd* pWnd, UINT nCtlColor) { HBRUSH hbr CDialog::OnCtlColor(pDC, pWnd, nCtlColor); if (nCtlColor CTLCOLOR_STATIC) { pDC-SetBkMode(TRANSPARENT); return (HBRUSH)::GetStockObject(HOLLOW_BRUSH); // 空刷子 不刷 } return hbr; }HOLLOW_BRUSH和NULL_BRUSH是同一个东西返回它表示“不要填充”但控件的文字仍然会在WM_CTLCOLORSTATIC之后绘制所以文字是可见的。这一步做完静态文本就能压在 PNG 上面而不是用一个灰方块把图切掉一块。5. 避坑与排查透明 PNG 在 VC6 下最常翻车的 5 个现场这套流程每一步都有对应的“图不对”现象但现象之间的差别很细微。下面按我的排查顺序整理每条都给出从现象反推原因的判断依据。5.1 编译与加载阶段头文件报错、图片读不进来现象一编译gdiplus.h时报error C2065: min : undeclared identifier位置在 SDK 头文件里。原因是这版 SDK 的gdiplus.h内部直接用了min/max既没有std::前缀也没包含algorithm而工程里恰好定义了NOMINMAX或者 MFC 的宏顺序把windows.h里的min/max宏给屏蔽了。 解决办法是在#include gdiplus.h之前插入#include algorithm和using namespace std;让非限定调用的min/max能解析到标准库版本。顺序不能反放到后面无效。现象二Bitmap::FromFile一直返回 NULL但同一个文件用画图能正常打开。三个原因按概率排一是 MBCS 工程下把CString或char*直接传给了要求WCHAR*的参数编译器在你强制转换的地方默许了运行时路径就是乱码二是用了相对路径而当前工作目录被快捷方式或调试器改到了别的盘三是GdiplusStartup根本没调用或者SuppressExternalCodecs传了 TRUE系统解码器没加载。 排查方式很土但有效先用GetFullPathName把绝对路径打印出来再在FromFile后面跟一句GetLastStatus()返回值不是Ok就说明卡在解码这步跟后面的绘制代码完全无关。5.2 绘制阶段黑方块、白边、整图消失现象三PNG 贴上去是一个规规矩矩的黑方块或者纯色方块边缘完全没有过渡。原因是用的BitBlt而不是AlphaBlend或者用了AlphaBlend但AlphaFormat忘了设AC_SRC_ALPHA。AC_SRC_ALPHA不设系统就把源当成不透明位图逐像素 Alpha 被忽略透明区域按 RGB 值原样输出——如果预乘做过了透明区域 RGB 是 0出来就是黑块如果没预乘出来就是原色的色块。这个现象反而能反推出预乘到底做没做。现象四图能正常显示但羽化边缘有一圈发亮的白边贴在深色背景上特别明显。原因几乎可以确定是没有预乘。AC_SRC_ALPHA要求源颜色已乘 Alpha直接拿 GDI 的原始像素去混半透明像素的 RGB 还是原色混过去就偏亮。 验证方法很简单把SourceConstantAlpha临时改成 128如果白边明显变宽变亮就是没预乘预乘正确的话整体变淡但边缘过渡依然平滑。现象五24 位无 Alpha 的 PNG 加载后整张图看不见但换成带 Alpha 的图就正常。原因是直接把源 Bitmap 的像素当 32bpp 用了24bpp 数据里根本没有 Alpha 字节读出来的s[3]是相邻像素的蓝色分量或者干脆是 0整图 Alpha 全零自然就全透明。 解决办法就是 4.1 里那一步先建一张PixelFormat32bppARGB的画布Clear成透明再把源图DrawImage上去。不透明像素经过 SourceOver 之后 Alpha 会变成 255这个“补齐”动作是免费的但漏掉就整张图消失。5.3 资源与句柄跑一会儿就不刷新了现象六反复打开、关闭对话框十几次之后图片突然不显示了重启程序又正常。这是典型的 GDI 句柄泄漏。SelectObject换进去的位图没有换出来、CreateCompatibleDC建的 DC 没有DeleteDC、CreateDIBSection建的位图没有DeleteObject本文还有配套的精品资源点击获取