ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

MDI客户区位图与调色板实战:从GDI选型到WM_QUERYNEWPALETTE全解析

MDI客户区位图与调色板实战:从GDI选型到WM_QUERYNEWPALETTE全解析 简介面向 Windows C 开发者的商用位图与调色板处理源码工程围绕 BMP 图像格式展开涵盖文件头、信息头解析、像素数据读取、调色板索引映射和 24 位真彩解码等核心环节工程在 MDI 多文档客户端中演示了如何通过 CreateDIBSection、CompatibleDC 等 API 将 DIB 绘制到窗口并同步处理字节序、内存管理与颜色深度转换适合作为图形界面编程和商业源码研读的参考。资源共 29 个文件压缩包约 61KB包含 8 个 h 头文件、7 个 cpp 源文件以及 Visual C 工程文件、RC 资源脚本、图标和两幅 BMP 示例图片目录结构紧凑方便对照代码理解文件组织。目前已有 120 人浏览学习对小体量学习型源码而言具备一定关注度。通过研读这份源码可掌握 BMP 文件内部机制与 Windows 位图显示 API 的配合方式并学习商业编程中图像处理的常见优化思路对桌面应用开发有直接借鉴意义。1. bmp_in_mdiclient.zip一份老库存代码教你吃透MDI客户区与调色板一段程序要在MDI客户区放一张位图听起来就是贴图但贴图前有一堆GDI的规矩要守。bmp_in_mdiclient.zip是商业编程源码里常见的那类小例子位图与调色板源代码它把CreateDIBSection、CreatePalette、SelectPalette、RealizePalette和MDIClient窗口过程串成一条线专门解决“图贴上去颜色不对、背景闪、子窗口一开就遮住背景”的问题。如果你是刚学完Win32消息循环、正在找位图与调色板完整落地示例的人或者是在MFC项目里用CStatic显示BMP但觉得控制不够细的程序员这份代码的思路值得拆开重写一遍老项目的维护者也能在这里找回WM_QUERYNEWPALETTE的语境。下面直接按工程落地的顺序重做把参数和坑位都标出来。2. 为什么MDI客户区画位图绕不开调色板从GDI选型讲起2.1 DDB、DIB 与 DC调色板存在的前提先把手上的名词理清。DDB是设备相关位图由当前显示驱动直接分配画得快但位图的像素格式和屏幕显示模式绑定换个显卡或调低颜色深度就翻车。DIB是设备无关位图自带位图头、颜色表和像素数据BMP文件就是DIB的内存形态。bmp_in_mdiclient.zip里的“位图与调色板”之所以放在同一个包里是因为要给DIB上色必须处理它的颜色表。GDI绘制目标统一抽象成DC屏幕上任何窗口客户区都对应一个设备DC。你的WM_PAINT里BeginPaint拿到的HDC就是这个窗口的“画板”。问题在于画板上显示的最终颜色要经过系统调色板映射。系统调色板在8位模式下只有256个槽位其中前20个是静态颜色由系统保留剩余236个槽位由当前实现调色板的前景窗口来决定。如果你的程序不主动创建和实现逻辑调色板GDI就用默认的20色静态调色板去近似所有颜色一张原本256色的位图会被映射成一片色斑。Windows通过逻辑调色板管理这个翻译过程程序用CreatePalette建一个逻辑调色板里面是位图的颜色表然后在绘图前SelectPalette再调用RealizePalette把逻辑调色板映射到系统调色板。这里有一个容易误会的点RealizePalette并不是“把颜色表复制进系统调色板”而是“把逻辑调色板中的颜色按优先级依次填入系统调色板的空槽”。如果一个逻辑调色板实现了100种颜色另有一个窗口也实现了100种系统会尽量合并相同的颜色合并不了的后到者失效。所以一个程序的颜色能不能正确显示取决于它是不是当前前景窗口、是否在绘制前重新实现过调色板。今天普遍32位色系统调色板机制看似退场但GDI调用仍然保留这些API老代码删掉调色板流程在真彩屏上也能跑可一旦需要兼容远程桌面或老显卡的16位、8位模式就会明白这套代码为什么要单独存在。2.2 MDIClient 窗口位图要画的舞台MDI程序有两个核心窗口主框架窗口Frame和它内部的客户区窗口MDIClient。MDIClient是WC_MDICLIENT类的一个实例负责管理子窗口。名字叫“客户区”但它是一个独立窗口有自己的窗口过程、背景画刷和消息处理。要往MDI程序的背景上放位图本质上是往MDIClient窗口上放图而不是往Frame上放图。区别在于Frame的背景只出现在菜单栏下方、客户区周围一小圈MDIClient的背景才是子窗口铺开的地方。MDIClient默认的窗口过程会在WM_PAINT时用系统背景画刷擦成浅灰色。你如果直接给它贴位图需要先子类化它拦截WM_PAINT和WM_ERASEBKGND否则每次子窗口重绘都会触发客户区重画位图会被默认背景覆盖或者造成闪烁。子类化MDIClient的具体做法是用SetWindowLongPtr换掉它的窗口过程把原来的过程指针存下来没处理的消息交给CallWindowProc。这一步是bmp_in_mdiclient.zip的关键也是容易被质疑的一步为什么不用现成的CStatic控件或者直接在代码里创建一个纯位图的子窗口CStatic只能给一个子控件贴图子窗口平铺、拖动时位图不会跟着客户区的滚动走主窗口的WM_ERASEBKGND只能画边框附近覆盖不到中间的子窗口区域。所以正确舞台是MDIClient自己。还有一个细节MDIClient创建时通常带上WS_CLIPCHILDREN样式避免子窗口区域被后台重绘画脏但你子类化后自己绘制时这个样式会让你的绘图在子窗口覆盖的部分被裁掉——这是MDI的正常行为背景永远在子窗口后面不要试图绕过。2.3 为什么创建位图要指定DIB_RGB_COLORS而不是DIB_PAL_COLORS源码里创建逻辑调色板之前往往会先载入一幅DIB。加载BMP文件时颜色表里的RGBQUAD是绝对的RGB三色值用CreatePalette可以直接把RGBQUAD数组填进LOGPALETTE。但有一种写法会在创建DIB段时传入DIB_PAL_COLORS表示颜色表里存的是“当前系统调色板的下标”而不是RGB值。这种写法极少用在真实文件加载场景因为BMP文件里的颜色表永远是RGBQUAD不是索引。如果你在处理非BMP格式的内部缓存才可能碰到DIB_PAL_COLORS。这个参数一旦选错位图颜色会整体偏移画面偏蓝、偏绿或者出现完全看不懂的配色。我的习惯是加载BMP时一律用DIB_RGB_COLORS配合GetDIBColorTable取颜色表只有在内存里构建特殊格式的DIB时才碰另一个。补充一点一个DIB段本身可以保存8位像素数据和它的颜色表但GDI在绘制时并不自动处理调色板。你在WM_PAINT里画一个8位DIB如果目标DC的调色板还是系统初始状态BitBlt会用目标DC的调色板去解释DIB的像素索引结果就是对不上。所以即便有了DIB段依然需要单独创建逻辑调色板并在绘制前SelectPalette。3. 把源码包落地成可运行项目文件拆分与关键代码3.1 文件组织与工程配置拿到bmp_in_mdiclient.zip第一件事不是急着编译而是先看它的工程结构。这类商业源码包一般是VC 6.0时代的Win32项目在新版Visual Studio里打开常常会遇到lib不匹配、入口函数签名不对等问题。我的习惯是新建一个空C项目把它按模块拆成三个文件mdi_frame负责创建MDI框架和子类化MDIClientbitmap_util负责加载BMP、建调色板client_paint负责在客户区上画背景。头文件里放共享的位图句柄、调色板句柄和位图尺寸。工程配置有两块容易踩。第一字符集选“使用多字节字符集”如果选Unicode所有GDI的A系列函数和文件路径都要跟着改。第二链接器里确保gdi32.lib、user32.lib、comctl32.lib都在MDI程序用到的DefMDIChildProc在user32里CreateDIBSection在gdi32里别只看C编译器不报错就忽略链接问题。下面这个函数是位图加载的核心它同时完成“加载DIB段”和“提取颜色表建调色板”// 从文件加载DIB段并提取颜色表创建逻辑调色板 HBITMAP LoadBitmapWithPalette(LPCTSTR szPath, HPALETTE *pPalOut) { HBITMAP hBmp (HBITMAP)LoadImage(NULL, szPath, IMAGE_BITMAP, 0, 0, LR_LOADFROMFILE | LR_CREATEDIBSECTION); if (!hBmp) return NULL; BITMAP bm; GetObject(hBmp, sizeof(bm), bm); // bmBitsPixel、bmWidth、bmHeight都在这 *pPalOut NULL; if (bm.bmBitsPixel 8) { // 8位及以下带颜色表用GetDIBColorTable拿回来 HDC hdcMem CreateCompatibleDC(NULL); HBITMAP hOld (HBITMAP)SelectObject(hdcMem, hBmp); RGBQUAD rgb[256]; UINT nCount GetDIBColorTable(hdcMem, 0, 256, rgb); if (nCount 0) { LOGPALETTE *pLog (LOGPALETTE*)malloc(sizeof(LOGPALETTE) nCount * sizeof(PALETTEENTRY)); pLog-palVersion 0x300; pLog-palNumEntries nCount; for (UINT i 0; i nCount; i) { pLog-palPalEntry[i].peRed rgb[i].rgbRed; pLog-palPalEntry[i].peGreen rgb[i].rgbGreen; pLog-palPalEntry[i].peBlue rgb[i].rgbBlue; pLog-palPalEntry[i].peFlags PC_NOCOLLAPSE; } *pPalOut CreatePalette(pLog); free(pLog); } SelectObject(hdcMem, hOld); DeleteDC(hdcMem); } return hBmp; }LoadImage的第四、五个参数设为0、0表示按文件原始尺寸加载不拉伸。LR_CREATEDIBSECTION是关键它保证返回的是DIB段而不是屏幕兼容的DDBDIB段才能用GetDIBColorTable拿到颜色表。GetDIBColorTable必须在位图被选入DC之后调用所以代码里开了兼容DC做中转用完立刻还原并删除。PALETTEENTRY的peFlags我习惯写PC_NOCOLLAPSE表示这个颜色不能被系统在实现调色板时合并到相邻颜色避免两张相近肤色被合并成一种。如果你担心调色板实现后颜色数量不够可以把每个颜色的peFlags写成0让系统做最激进的颜色合并这在位图本身颜色少时没有区别颜色多时会影响画面细腻度。3.2 关键代码拦截MDIClient的WM_ERASEBKGND与WM_PAINT位图和调色板都准备好之后要让它显示在MDIClient上。我一般在创建主窗口、拿到MDIClient句柄后立刻子类化它。代码分两步第一步替换窗口过程static WNDPROC g_oldMdiclientProc; LRESULT CALLBACK MdiclientProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam) { switch (uMsg) { case WM_ERASEBKGND: // 背景完全交给WM_PAINT绘制这里直接说“已擦除” return 1; case WM_PAINT: PaintMdiclientBackground(hWnd); return 0; } return CallWindowProc(g_oldMdiclientProc, hWnd, uMsg, wParam, lParam); } void HookMdiclient(HWND hMdiclient) { g_oldMdiclientProc (WNDPROC)GetWindowLongPtr(hMdiclient, GWLP_WNDPROC); SetWindowLongPtr(hMdiclient, GWLP_WNDPROC, (LONG_PTR)MdiclientProc); }为什么在WM_ERASEBKGND返回1默认的MDIClient过程会用系统背景画刷擦除客户区WM_PAINT再来画位图等于每次重绘先擦成灰白色再贴图人眼看到的就是闪烁。把擦除和绘制合并到WM_PAINT是SDK下解决闪烁的稳妥方案。注意CallWindowProc的第一个参数是原窗口过程后面四个参数与WM_*消息标准参数完全一致不能少。第二步是WM_PAINT里的实际绘制。下面这段代码做了“平铺”因为MDI客户区通常比一张位图大void PaintMdiclientBackground(HWND hMdiclient) { PAINTSTRUCT ps; HDC hdc BeginPaint(hMdiclient, ps); // 先给目标DC挂上调色板再取客户区尺寸 if (g_hPalette) { SelectPalette(hdc, g_hPalette, FALSE); RealizePalette(hdc); } RECT rc; GetClientRect(hMdiclient, rc); HDC hdcMem CreateCompatibleDC(hdc); HBITMAP hOld (HBITMAP)SelectObject(hdcMem, g_hBitmap); BITMAP bm; GetObject(g_hBitmap, sizeof(bm), bm); for (int y 0; y rc.bottom; y bm.bmHeight) { for (int x 0; x rc.right; x bm.bmWidth) { BitBlt(hdc, x, y, bm.bmWidth, bm.bmHeight, hdcMem, 0, 0, SRCCOPY); } } SelectObject(hdcMem, hOld); DeleteDC(hdcMem); EndPaint(hMdiclient, ps); }有三个参数细节值得说。第一SelectPalette的第三个参数bForceBackground为FALSE表示“我是前景窗口允许替换系统调色板”如果为TRUE则只能把颜色映射到现有系统调色板颜色质量会打折扣。第二RealizePalette的返回值表示“有多少颜色被精确放进系统调色板”如果为0说明没有一个空槽或颜色已经被占用。第三BitBlt的SRCCOPY是直接覆盖如果你想做透明叠加就得换成其他的光栅操作码。平铺循环的步长是位图尺寸因此当客户区尺寸不是位图尺寸的整数倍时右边和下边会露出没有覆盖的背景这是正常的。如果要求铺满不留缝最后一行和最后一列改为StretchBlt拉伸到位图最后一段但那会造成边缘变形老代码一般不这么做。3.3 处理WM_QUERYNEWPALETTE与WM_PALETTECHANGED的时机如果说WM_PAINT是贴图的“画布”这两个消息就是贴图颜色能不能长期成立的“守卫”。Windows规定当一个窗口要使用自己的逻辑调色板时必须在收到WM_QUERYNEWPALETTE时把它实现进系统调色板。系统在把某个窗口设为前台时会广播这个消息其他窗口实现调色板后系统再向所有窗口发WM_PALETTECHANGED。MDIClient默认不处理这两个消息所以要在自己的窗口过程里补上。我建议挂在主窗口因为MDI中焦点切换涉及子窗口消息最终都要回到框架窗口来协调。实现如下case WM_QUERYNEWPALETTE: if (g_hPalette) { HDC hdc GetDC(hWnd); SelectPalette(hdc, g_hPalette, FALSE); int nChanged RealizePalette(hdc); ReleaseDC(hWnd, hdc); if (nChanged 0) { InvalidateRect(hWnd, NULL, TRUE); return TRUE; } } return FALSE; case WM_PALETTECHANGED: if ((HWND)wParam ! hWnd g_hPalette) { HDC hdc GetDC(hWnd); SelectPalette(hdc, g_hPalette, FALSE); RealizePalette(hdc); ReleaseDC(hWnd, hdc); InvalidateRect(hWnd, NULL, TRUE); } break;这里有几个细节。WM_PALETTECHANGED的wParam是当时正在实现自己调色板的窗口句柄如果它等于自己说明这次变动是当前窗口引发的再Realize一次会造成重复映射所以必须排除。InvalidateRect的第三个参数TRUE表示擦除背景但我们已经让WM_ERASEBKGND返回1这个布尔值实际不影响闪烁保留TRUE也行。另外MDI子窗口之间切换时如果主窗口收到了WM_QUERYNEWPALETTE而MDIClient的背景没有重画颜色依然会花。我的做法是在主窗口处理这个消息后向MDIClient发一条WM_PAINT重绘if (hMdiclient) RedrawWindow(hMdiclient, NULL, NULL, RDW_INVALIDATE | RDW_UPDATENOW);这样就能把调色板的更新和背景重绘绑定在一起。很多老示例只处理了WM_QUERYNEWPALETTE却忘了让MDIClient重绘结果第一次启动正常切换回来就残留之前的颜色。4. 调色板与位图显示避坑五条实测踩坑记录4.1 现象背景闪烁、白底一闪而过原因MDIClient默认的WM_ERASEBKGND会先擦除背景然后WM_PAINT才贴图两次操作之间暴露底色。解决在子类化过程里把WM_ERASEBKGND返回1把擦除和绘制全部交给WM_PAINT。如果仍有闪检查WM_PAINT里是不是先调用了FillRect填充背景色那等于把擦除又带回WM_PAINT。把FillRect删掉直接BitBlt位图即可。还要注意BeginPaint的ps.rcPaint只代表无效区域。平铺循环不能只画这个矩形否则滚动子窗口后背景会留下大片空区。我习惯按整个客户区尺寸画GDI会自己裁剪到无效区域。这样虽然多做几次BitBlt但代码逻辑简单重绘时也不会漏背景。4.2 现象8位位图在32位屏上显示成“马赛克色块”原因32位屏上8位BMP的像素数据仍然是索引值必须有一张颜色表翻译成RGB。如果加载时用了LoadImage的LR_LOADFROMFILE而没有LR_CREATEDIBSECTIONGDI会生成一个与屏幕深度匹配的DDB像素颜色通常仍然正确只是调色板句柄拿不到。但如果用CreateDIBSection创建了DIB段却没有把颜色表读出来创建逻辑调色板绘制时8位索引会直接按目标DC的默认调色板解释于是出现马赛克。解决用上面给的LoadBitmapWithPalette函数8位及以下必须调GetDIBColorTable并把颜色表变成逻辑调色板。还要注意CreatePalette的LOGPALETTE头里palVersion一定要写0x300写别的值会让CreatePalette返回空句柄。这个版本号不是随便写的Windows把所有调色板都按0x300的palette object格式处理。4.3 现象程序刚打开正常切到其他窗口再切回来颜色全乱原因另一个程序的调色板在切换时抢占了系统调色板槽位你的程序切回前台时没有重新Realize自己的调色板。这是典型的缺少WM_QUERYNEWPALETTE处理。解决把3.3节的代码加到主窗口过程并且确认主窗口收到消息后触发了MDIClient重绘。如果只处理了消息但忘了InvalidateRect画面不会自动更新。切回后颜色全乱还有一个隐蔽原因你在启动时只调用了一次RealizePalette之后每次WM_PAINT都直接用上次的调色板。绘制期间其他窗口可能已经改过系统调色板所以每次WM_PAINT都必须重新SelectPalette和RealizePalette。把这条记在代码注释里能少踩很多次这个坑。4.4 现象位图只显示了一部分另一端被MDIClient裁掉原因如果你用的是平铺循环按位图原始尺寸复制位图小于客户区会铺满不存在“只显示一部分”。真正的问题往往出在MDIClient的WS_CLIPCHILDREN样式子窗口覆盖的区域不属于背景背景只画在子窗口周围的缝隙里看起来就像“只显示一部分”。解决这是MDI的正常行为不用强行解决。如果非要让位图完整露出在子窗口底下只能改MDI布局不让子窗口铺满客户区而不是去画背景。另一个可能的元凶是BMP的负高度。BMP头里biHeight为负数表示像素从上到下存储GetObject返回的bmHeight是正数但坐标方向有差异。此时用BitBlt按(0,0)起点画会把下半部分截掉。解决办法是改用SetDIBitsToDevice来画它对负高度兼容更好或者读取biHeight时取绝对值同时根据正负决定起点。4.5 现象每次重绘都慢且最后耗尽GDI句柄原因WM_PAINT里频繁创建DC、位图、调色板用完之后没有DeleteDC和DeleteObject。特别是平铺循环里如果每贴一张就CreateCompatibleDC一次位图一多GDI句柄很快被吃光。解决DC和调色板在进入循环前创建一次循环外删除位图句柄和调色板句柄在窗口销毁时释放一次。窗口销毁时统一清理case WM_DESTROY: DeleteObject(g_hBitmap); if (g_hPalette) DeleteObject(g_hPalette); PostQuitMessage(0); break;DeleteObject可以删除逻辑调色板但要保证没有DC还选着它。所以在删除前先确认所有兼容DC都已被DeleteDC或还原了选区。GDI句柄泄漏不像内存泄漏那么明显但进程句柄数涨到一定量就会报“无法创建窗口”。Windows任务管理器默认看不到GDI对象数量需要打开“性能监视器”或代码里用GetGuiResources(hProcess, GR_GDIOBJECTS)检查如果反复切换窗口后这个数值持续上涨就是泄漏了。5. 验证与调参用Spy、GetDeviceCaps和颜色深度测试确认方案没问题5.1 用Spy确认消息序列WM_QUERYNEWPALETTE有没有到客户区写完代码怎么知道它真的在按预期工作我一般用Visual Studio自带的Spy监视MDIClient窗口。先打开Spy的窗口查找器把靶心图标拖到程序的MDIClient区域记下窗口句柄。然后在“消息”选项里勾选WM_PAINT、WM_ERASEBKGND、WM_QUERYNEWPALETTE、WM_PALETTECHANGED。切换到别的程序再切回来观察MDIClient是否收到WM_QUERYNEWPALETTE。正常情况下切回时应该收到该消息并且随后收到一次WM_PAINT。如果只看到WM_PAINT而没有WM_QUERYNEWPALETTE说明消息没有从主窗口转发到MDIClient。Windows把WM_QUERYNEWPALETTE发给前台窗口前台是主框架时MDIClient不会自动收到所以你需要在主框架处理完消息后调用RedrawWindow让MDIClient重绘。Spy里可以同时开两个消息监视器一个看主框架一个看MDIClient确认消息最终转化成了客户区的WM_PAINT。同时看WM_ERASEBKGND如果每次重绘都有WM_ERASEBKGND说明子类化过程没有返回1或者消息发给了子窗口而不是MDIClient。MDI子窗口切换时如果客户区被遮挡再恢复背景重绘时应该只有WM_PAINT而没有擦除消息。这套消息序列是我验证调色板方案是否完整的最直接手段。5.2 用GetDeviceCaps读取当前屏的调色板能力调色板代码是否生效取决于目标DC的颜色深度。用下面这段代码把关键参数打出来是排查“为什么我这里不需要调色板”的基础HDC hdc GetDC(NULL); int planes GetDeviceCaps(hdc, PLANES); int bits GetDeviceCaps(hdc, BITSPIXEL); int colors GetDeviceCaps(hdc, NUMCOLORS); // 8位屏上NUMCOLORS是256真彩屏上是-1 ReleaseDC(NULL, hdc);在8位屏上bits * planes 8NUMCOLORS是256系统调色板真正参与显示。在32位屏上NUMCOLORS为-1表示没有调色板。这时调用SelectPalette也有效果但RealizePalette基本是空操作。所以“调色板代码是必须的吗”按环境分如果只跑32位屏删掉调色板流程也能显示正确如果要兼容256色模式或者想在程序里体验老式修复这套代码一个都不能少。我建议把这段检查代码留在这个程序里甚至放到启动日志里。以前我维护过一个进销存程序客户反馈“界面花屏”我远程连过去GetDeviceCaps显示是16位色但程序里的调色板代码被上一个工程师注释掉了恢复注释后再也没出过问题。5.3 回归测试切换256色与32位两种显示模式真正的验证不只是看代码还要切分辨率。Windows显示设置里把颜色改为256色今天Win10/11隐藏了这个选项可以在“显示适配器属性→列出所有模式”里找8位彩色模式或者用旧版DirectX工具强制切换。切换后启动程序看三件事背景位图的肤色、渐变是否还能认出有没有大面积色块。把程序最小化再还原中间打开“画图”抢一下调色板再切回来看背景有没有变色。启动多个MDI子窗口拖动重叠看背景平铺有没有留下黑色残影。如果1失败检查调色板颜色表顺序和BMP文件是否以8位索引格式存储。如果2失败基本是WM_QUERYNEWPALETTE没处理好。如果3失败检查WM_PAINT是否画了整个客户区。这三条过了bmp_in_mdiclient.zip的核心价值你就已经吃到了。另外可以在代码里临时加一个菜单项切换位图文件。每次加载新位图时先删除旧调色板再按3.1的函数重新加载验证运行时不重启能不能热切换。这能暴露句柄泄漏——切换一百次任务管理器的GDI对象数量应该基本恒定。如果涨了就检查是否在切换函数里漏了DeleteObject。5.4 颜色表边界biClrUsed、biClrImportant 与格式判断还有一个容易被源码包忽略的参数BITMAPINFOHEADER里的biClrUsed表示实际使用的颜色数biClrImportant表示重要颜色数。如果biClrUsed0意味着颜色表有2^biBitCount项如果写了小于256的数颜色表只包含前N项后面的像素索引可能没有定义。在这种位图里GetDIBColorTable返回的是实际存在的项数创建逻辑调色板时不要固化成256。我见过有人把palNumEntries写成256而实际颜色表只有32项后面访问PALETTEENTRY就会越界。所以函数里要先读nCount再分配这是最稳的。另外BMP可能是RLE压缩过的8位位图LoadImage可以解压GetDIBColorTable仍能拿到颜色表与压缩无关。32位带Alpha的BMP没有颜色表调色板代码会自动跳过但透明通道要配合AlphaBlend而不是BitBlt那是另一个话题。6. 进阶技巧把MDI客户区背景从平铺改成居中拉伸并加透明Logo最后这个技巧把这份源码的价值再往前推一步。平铺循环简单但产品经理往往要“背景就一张图铺平显得很Low”。我习惯做三种模式切换居中、拉伸、平铺。居中只需替换掉PaintMdiclientBackground里的双层循环int x (rc.right - bm.bmWidth) / 2; int y (rc.bottom - bm.bmHeight) / 2; BitBlt(hdc, x, y, bm.bmWidth, bm.bmHeight, hdcMem, 0, 0, SRCCOPY);x、y可能为负客户区小于位图时BitBlt会把超出左边和上边的部分剪掉这是标准结果不用特判。拉伸则用StretchBltStretchBlt(hdc, 0, 0, rc.right, rc.bottom, hdcMem, 0, 0, bm.bmWidth, bm.bmHeight, SRCCOPY);拉伸时位图比例会失真图片里有字或圆时一眼就能看出来。我一般会先取客户区宽高比做“适应”而不是“拉伸”等比缩放后居中剩下部分用纯色或平铺底色填。另一个实用技巧是在背景右上角叠一个小Logo先按居中画主图再在右上角用TransparentBlt画一个带透明色的图标。TransparentBlt需要msimg32.lib参数里倒数第二个是透明色通常取Logo左上角像素颜色。这样背景图既能自动适应屏幕又能保留品牌标识。我在调色板主题上花费的时间最后都转化为对GDI消息流的敏感。这套流程最初是维护一个20年前的进销存程序时重新摸出来的当时被“窗口一切换界面就变花”折磨了两天最后才发现是调色板消息没转发。后来每写一个MDI程序我都会保留WM_QUERYNEWPALETTE这段哪怕在真彩屏上只是空操作——因为谁也说不准明天这台机器会不会被接到一台老投影仪上。希望这份拆解能让你少走一次弯路。本文还有配套的精品资源点击获取
返回列表