Windows GDI画刷深度解析:从创建、使用到性能优化实战
2026/8/23 17:15:25 网站建设 项目流程

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;

这段代码逻辑上完全正确,但性能上却是灾难。CreateSolidBrushDeleteObject是相对耗时的系统调用,尤其是在高速绘制的场景下(比如动画或频繁刷新)。正确的做法是,将常用的画刷作为窗口或应用程序的全局/静态资源,在初始化时(如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这类尺寸。

创建和使用图案画刷的典型流程如下:

  1. 加载或创建位图资源(LoadBitmapCreateBitmap)。
  2. 用位图句柄创建画刷(CreatePatternBrush)。
  3. 在绘图时选入设备上下文。
  4. 使用完毕后,需要先删除画刷,再删除位图。因为画刷内部持有位图的一份引用。如果先删除位图,画刷将变成无效状态,可能导致程序崩溃或绘制错误。
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)时,它做两件事:

  1. 将新的GDI对象(如我们的画刷)选入指定的DC,使其成为当前的活动工具。
  2. 返回之前被选中的同类型对象的句柄。这是一个关键点,也是很多内存泄漏问题的根源。

你必须保存这个返回的旧句柄,并在绘制完成后,用它把旧对象选回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 核心绘制API:FillRectRectangleExtFloodFill

有了画刷选入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_BRUSHHOLLOW_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等框架中的自动化管理如果你使用MFC,CBrush类封装了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需要一些技巧,例如:

  1. 使用图案画刷模拟:创建一个8x8的位图,其中部分像素为纯色,部分像素为透明色(在某些混合模式下),平铺后可以产生类似网格半透明的视觉效果,但这并非真正的颜色混合。
  2. AlphaBlend函数结合:这是GDI+的功能,但可以通过Gdiplus.h在原生项目中使用。你需要先在一个独立的位图上用纯色画刷填充,然后将这个位图以半透明的方式AlphaBlend到目标DC上。这超出了纯GDI画刷的范畴,进入了GDI+的领域。
  3. 使用更新层的窗口UpdateLayeredWindow):这是实现真正任意形状、任意透明度窗口的现代API,它通常与GDI+或Direct2D配合使用,完全脱离了传统GDI的画刷填充模式。

对于大多数需要简单透明效果的场景(如高亮、遮罩),使用SetBkMode(hdc, TRANSPARENT)配合特定颜色的阴影画刷,或者使用NULL_BRUSH只画边框,往往是更简单、性能更好的选择。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询