ARTICLE DETAIL

资讯详情

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

Windows GDI画刷深度解析:从创建、使用到性能优化实战

Windows GDI画刷深度解析:从创建、使用到性能优化实战 1. 从“刷子”到“像素”理解Windows画刷的本质在Windows图形编程的世界里如果你只学会了用画笔Pen画线那就像画家只有铅笔没有颜料只能勾勒轮廓无法填充色彩。画刷Brush就是那个负责填充的“颜料桶”是让窗口、控件、图形区域从单调线条变成生动画面的核心工具。很多初学者在接触GDI图形设备接口时会把大量精力放在复杂的绘图路径和坐标计算上却忽略了画刷这个看似简单、实则影响性能和视觉效果的关键角色。我见过不少程序界面绘制卡顿、颜色过渡生硬甚至出现奇怪的填充残留追根溯源问题往往出在对画刷的理解和使用上。画刷绝不仅仅是设定一个颜色那么简单。在Windows GDI中它定义了如何填充一个封闭区域的内部。这个“如何填充”就衍生出了实心画刷、阴影画刷、图案画刷甚至逻辑画刷等多种类型。选择哪种画刷不仅取决于你想要的颜色更取决于你要填充的对象是客户区背景还是一个复杂的几何图形、你期望的性能是追求最快速度还是需要复杂的视觉效果以及资源管理的考量你是否在频繁创建和销毁画刷。理解画刷是写出高效、稳定、视觉效果专业的Windows图形程序的基本功。接下来我们就抛开那些笼统的概念深入到每一种画刷的创建、使用、特性和那些官方文档里不会写的“坑”里去看看。2. 画刷家族全解析不止是RGB(255,0,0)当你调用CreateSolidBrush(RGB(255, 0, 0))时你得到了一把红色的“实心刷”。这是最直接、最常用也最容易被误解“用完即弃”的画刷。但画刷的世界远比这丰富。2.1 实心画刷速度之王与资源陷阱实心画刷Solid Brush通过CreateSolidBrush(COLORREF crColor)创建。它的行为非常直观用指定的纯色填满整个区域。在所有的画刷类型中它的绘制速度是最快的因为图形驱动不需要进行任何额外的模式计算或位图采样直接进行内存块填充即可。注意这里的“颜色”是设备相关的。RGB(255,0,0)在你的显示器上是红色但如果你在绘制到单色打印机或一个颜色深度不同的位图上系统会进行抖动Dithering或颜色匹配最终呈现可能不是纯红。这是GDI设备相关性的一个体现。很多新手会犯的一个错误是在绘图消息如WM_PAINT处理函数中频繁创建和销毁实心画刷case WM_PAINT: { PAINTSTRUCT ps; HDC hdc BeginPaint(hWnd, ps); HBRUSH hRedBrush CreateSolidBrush(RGB(255, 0, 0)); HBRUSH hOldBrush (HBRUSH)SelectObject(hdc, hRedBrush); Rectangle(hdc, 10, 10, 200, 150); SelectObject(hdc, hOldBrush); // 恢复旧画刷 DeleteObject(hRedBrush); // 删除画刷 EndPaint(hWnd, ps); } break;这段代码逻辑上完全正确但性能上却是灾难。CreateSolidBrush和DeleteObject是相对耗时的系统调用尤其是在高速绘制的场景下比如动画或频繁刷新。正确的做法是将常用的画刷作为窗口或应用程序的全局/静态资源在初始化时如WM_CREATE创建一次在程序退出时如WM_DESTROY销毁。HBRUSH g_hRedBrush NULL; HBRUSH g_hBlueBrush NULL; case WM_CREATE: g_hRedBrush CreateSolidBrush(RGB(255, 0, 0)); g_hBlueBrush CreateSolidBrush(RGB(0, 0, 255)); return 0; case WM_PAINT: { PAINTSTRUCT ps; HDC hdc BeginPaint(hWnd, ps); HBRUSH hOldBrush (HBRUSH)SelectObject(hdc, g_hRedBrush); Rectangle(hdc, 10, 10, 200, 150); SelectObject(hdc, hOldBrush); EndPaint(hWnd, ps); } return 0; case WM_DESTROY: DeleteObject(g_hRedBrush); DeleteObject(g_hBlueBrush); PostQuitMessage(0); return 0;2.2 阴影画刷系统预定义的“纹理”库阴影画刷Hatch Brush用于创建简单的图案填充如水平线、垂直线、斜线、网格等。通过CreateHatchBrush(int fnStyle, COLORREF clrref)创建。fnStyle定义了图案样式例如HS_HORIZONTAL水平线、HS_VERTICAL垂直线、HS_CROSS十字网格等。阴影画刷的“纹理”是由前景色clrref参数指定的线条和背景色默认是白色但可以通过设备上下文背景模式和相关设置影响的间隔构成的。它的绘制速度比实心画刷慢但比基于位图的图案画刷快适用于需要简单非纯色填充又不想引入位图资源的场景比如图表中区分不同的数据区域。一个常见的误解是认为阴影画刷的颜色是“线条颜色”。实际上在默认的OPAQUE背景模式下系统会用背景色填充线条之间的空隙。你可以通过SetBkColor(hdc, backgroundColor)和SetBkMode(hdc, TRANSPARENT)来控制背景色的行为。TRANSPARENT模式下线条间的区域会保持原样即不填充这在你需要将阴影图案叠加到已有图像上时非常有用。2.3 图案画刷与位图画刷自定义“皮肤”当你需要更复杂的填充图案比如木纹、布料、公司Logo平铺时实心和阴影画刷就无能为力了。这时就需要图案画刷Pattern Brush通过CreatePatternBrush(HBITMAP hbmp)创建。你需要先准备一个位图HBITMAP资源画刷会使用这个位图作为“瓷砖”平铺填充目标区域。这里有一个至关重要的细节用作图案画刷的位图理想尺寸是8x8像素。为什么这是历史原因和性能优化的结果。早期的显示设备和内存非常有限小尺寸位图占用资源少平铺计算快。虽然现代Windows支持任意尺寸的位图创建画刷但使用非8x8尤其是非2的幂次方尺寸的位图在某些旧图形驱动或特殊设备上下文上可能会导致不可预知的绘制结果或性能下降。为了最大的兼容性和性能请尽量使用8x8、16x16、32x32这类尺寸。创建和使用图案画刷的典型流程如下加载或创建位图资源LoadBitmap或CreateBitmap。用位图句柄创建画刷CreatePatternBrush。在绘图时选入设备上下文。使用完毕后需要先删除画刷再删除位图。因为画刷内部持有位图的一份引用。如果先删除位图画刷将变成无效状态可能导致程序崩溃或绘制错误。HBITMAP hBmp LoadBitmap(hInstance, MAKEINTRESOURCE(IDB_MY_PATTERN)); // 假设是8x8位图 HBRUSH hPatBrush CreatePatternBrush(hBmp); // ... 使用画刷 ... DeleteObject(hPatBrush); // 先删画刷 DeleteObject(hBmp); // 再删位图2.4 逻辑画刷高级玩家的抽象工具CreateBrushIndirect(const LOGBRUSH *lplb)这个函数允许你通过一个LOGBRUSH结构体来创建画刷。这个结构体统一了上述所有画刷类型的创建参数样式、颜色、阴影样式、位图句柄。它的主要优势在于序列化和灵活性。你可以方便地将画刷的描述LOGBRUSH保存到文件或通过网络传输然后在另一端重建画刷。它也常用于需要根据运行时数据动态决定画刷类型的场景。LOGBRUSH lb; lb.lbStyle BS_HATCHED; lb.lbColor RGB(0, 255, 0); lb.lbHatch HS_DIAGCROSS; // 斜交叉线 HBRUSH hLogBrush CreateBrushIndirect(lb);3. 画刷使用实战选入、绘制与资源管理三部曲知道了怎么创建画刷下一步就是正确地使用它。这个过程可以概括为“选入、绘制、恢复/清理”三部曲但每一步都有细节需要注意。3.1 设备上下文与画刷的“选入”机制在GDI中所有的绘制操作都是通过设备上下文Device Context, HDC来完成的。DC就像一个画家的画板上面有当前选中的画笔、画刷、字体、调色板等工具。SelectObject函数就是用来更换这些工具的。当你调用HGDIOBJ SelectObject(HDC hdc, HGDIOBJ hgdiobj)时它做两件事将新的GDI对象如我们的画刷选入指定的DC使其成为当前的活动工具。返回之前被选中的同类型对象的句柄。这是一个关键点也是很多内存泄漏问题的根源。你必须保存这个返回的旧句柄并在绘制完成后用它把旧对象选回DC。这不是为了“恢复现场”的仪式感而是为了防止DC持有你即将删除的对象的引用。如果一个GDI对象如画刷还被某个DC选中你就调用DeleteObject删除它结果是未定义的通常会导致下一次使用该DC时程序崩溃或图形异常。正确的范式永远是HBRUSH hMyBrush CreateSolidBrush(...); HBRUSH hOldBrush (HBRUSH)SelectObject(hdc, hMyBrush); // 进行你的绘制操作例如 Rectangle, Ellipse, FillRect 等 SelectObject(hdc, hOldBrush); // 恢复旧画刷 DeleteObject(hMyBrush); // 现在可以安全删除3.2 核心绘制APIFillRect、Rectangle与ExtFloodFill有了画刷选入DC就可以开始填充了。最常用的几个函数是FillRect(HDC hDC, const RECT *lprc, HBRUSH hbr): 这是填充矩形区域最直接、最高效的方式。注意它不需要你先SelectObject画刷而是直接将画刷句柄作为参数传入。函数内部会处理画刷的选入和恢复因此它不会影响DC中当前选中的画刷。这对于快速填充多个不同颜色的矩形区域非常方便也是填充窗口背景在WM_ERASEBKGND消息中的标准做法。Rectangle(HDC hdc, int left, int top, int right, int bottom): 这个函数绘制一个矩形边框并填充其内部。边框使用当前选入DC的画笔Pen填充使用当前选入DC的画刷Brush。所以使用Rectangle前你需要确保正确的画笔和画刷都已选入DC。Ellipse,RoundRect,Pie等: 与Rectangle类似它们绘制形状并填充填充行为取决于当前DC中的画刷。ExtFloodFill(HDC hdc, int x, int y, COLORREF crColor, UINT fuFillType): 这是一个更高级的填充函数用于填充一个颜色边界围成的区域类似于绘图软件中的“油漆桶”工具。它的性能开销较大且对设备颜色深度敏感使用时需谨慎。3.3 必须绕开的资源管理深坑GDI资源是系统全局资源非常有限尤其是在旧系统上。泄漏GDI对象是Windows C程序最常见的错误之一会导致程序运行一段时间后界面绘制异常甚至系统整体图形性能下降。坑一GetStockObject返回的对象不要删除系统提供了一些预定义的、常用的标准对象称为库存对象Stock Object。例如GetStockObject(BLACK_BRUSH)、GetStockObject(NULL_BRUSH)空画刷用于只画边框不填充。这些对象的句柄是常量不属于你的程序你绝对不能调用DeleteObject来删除它们。试图删除库存对象会导致错误。坑二NULL_BRUSH与HOLLOW_BRUSH的妙用NULL_BRUSH或HOLLOW_BRUSH两者等价是一个特殊的库存画刷。将它选入DC后任何填充操作如Rectangle的内部都将变成透明的不会绘制任何东西。这在你只想画一个形状的轮廓时非常有用避免了创建实心画刷再设置特殊颜色的麻烦。// 只画矩形边框不填充内部 HBRUSH hOldBrush (HBRUSH)SelectObject(hdc, GetStockObject(NULL_BRUSH)); Rectangle(hdc, 10, 10, 100, 100); SelectObject(hdc, hOldBrush);坑三MFC/ATL等框架中的自动化管理如果你使用MFCCBrush类封装了HBRUSH并在其析构函数中自动调用DeleteObject这大大降低了内存泄漏的风险。但你要注意“临时对象”问题。例如CDC::SelectObject会返回一个指向之前CBrush的指针你需要保存它并在用完后选回。CBrush myBrush(RGB(255,0,0)); CBrush* pOldBrush pDC-SelectObject(myBrush); pDC-Rectangle(10,10,100,100); pDC-SelectObject(pOldBrush); // 必须选回 // myBrush析构时自动删除GDI对象同时MFC也提供了CGdiObject::DeleteObject方法允许你显式删除但通常让析构函数处理即可。4. 性能优化与高级技巧让绘制飞起来当你的界面元素变得复杂或者需要实现平滑动画时画刷的使用方式会直接影响性能。4.1 缓存与复用减少GDI对象创建开销如前所述最直接的优化就是在程序生命周期内缓存常用的画刷。创建一个画刷管理类或简单的全局数组在程序启动时初始化所有可能用到的画刷特别是实心和阴影画刷。对于图案画刷如果图案是静态的也应该缓存。如果图案需要动态改变比如根据用户输入生成那么你需要权衡是每次重新创建画刷和位图的开销大还是维护一个动态更新位图再创建画刷的复杂度高。通常对于不频繁变化的图案后者更优。4.2 区域裁剪只画该画的地方HRGN区域句柄是GDI中另一个强大的工具。你可以创建矩形、圆形、多边形甚至组合区域然后通过SelectClipRgn将这个区域选入DC。之后所有的绘制操作都只会在这个区域内部生效区域外的部分会被“裁剪”掉不会进行任何实际的像素计算和写入。这在用画刷填充复杂背景时特别有用。例如你的窗口客户区需要填充一种颜色但中间有一块不规则形状的控件区域需要保持透明或另一种颜色。你可以先创建一个覆盖整个客户区的区域然后用CombineRgn函数“减去”控件区域得到一个镂空的区域。将这个区域选入DC再用画刷填充就可以一次性、精确地完成背景填充避免了先全填充再局部重绘的覆盖操作效率更高也没有闪烁问题。4.3 双缓冲与画刷消除闪烁的黄金组合画面闪烁是Windows图形编程的老大难问题根本原因是用户看到了绘制过程的中间状态。双缓冲Double Buffering是根治此问题的标准方案先在内存中的一个位图后台缓冲区上完成所有绘制然后一次性将这个位图“贴”到屏幕DC上。在这个过程中画刷的使用需要一点技巧。你需要在内存DC与后台位图关联上进行绘制就像在屏幕DC上一样。这意味着你为屏幕DC创建的那些画刷同样需要选入内存DC。一个常见的优化是如果你的画刷是纯色的并且双缓冲中填充操作很多可以考虑直接使用内存位图的DC进行FillRect或者更高效地在创建兼容位图CreateCompatibleBitmap后直接用PatBlt函数配合画刷一次性填充整个位图背景。// 简化的双缓冲绘制示例 case WM_PAINT: { PAINTSTRUCT ps; HDC hdc BeginPaint(hWnd, ps); RECT rcClient; GetClientRect(hWnd, rcClient); // 1. 创建内存DC和兼容位图 HDC hMemDC CreateCompatibleDC(hdc); HBITMAP hMemBmp CreateCompatibleBitmap(hdc, rcClient.right, rcClient.bottom); HBITMAP hOldBmp (HBITMAP)SelectObject(hMemDC, hMemBmp); // 2. 先用背景画刷填充整个内存位图可选消除残留 HBRUSH hBgBrush (HBRUSH)GetClassLongPtr(hWnd, GCLP_HBRBACKGROUND); FillRect(hMemDC, rcClient, hBgBrush); // 3. 在内存DC上进行所有复杂绘制使用你的各种画刷 HBRUSH hRedBrush GetCachedRedBrush(); // 假设从缓存获取 HBRUSH hOldMemBrush (HBRUSH)SelectObject(hMemDC, hRedBrush); Rectangle(hMemDC, 50, 50, 200, 200); // ... 更多绘制 ... SelectObject(hMemDC, hOldMemBrush); // 4. 一次性贴到屏幕 BitBlt(hdc, 0, 0, rcClient.right, rcClient.bottom, hMemDC, 0, 0, SRCCOPY); // 5. 清理内存DC SelectObject(hMemDC, hOldBmp); DeleteObject(hMemBmp); DeleteDC(hMemDC); EndPaint(hWnd, ps); } break;4.4 画刷与透明/半透明效果标准的GDI画刷本身不支持Alpha通道半透明。要实现半透明填充传统GDI需要一些技巧例如使用图案画刷模拟创建一个8x8的位图其中部分像素为纯色部分像素为透明色在某些混合模式下平铺后可以产生类似网格半透明的视觉效果但这并非真正的颜色混合。与AlphaBlend函数结合这是GDI的功能但可以通过Gdiplus.h在原生项目中使用。你需要先在一个独立的位图上用纯色画刷填充然后将这个位图以半透明的方式AlphaBlend到目标DC上。这超出了纯GDI画刷的范畴进入了GDI的领域。使用更新层的窗口UpdateLayeredWindow这是实现真正任意形状、任意透明度窗口的现代API它通常与GDI或Direct2D配合使用完全脱离了传统GDI的画刷填充模式。对于大多数需要简单透明效果的场景如高亮、遮罩使用SetBkMode(hdc, TRANSPARENT)配合特定颜色的阴影画刷或者使用NULL_BRUSH只画边框往往是更简单、性能更好的选择。
返回列表