十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

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

Windows GDI画刷完全指南:从纯色到纹理,掌握UI绘制的核心技术

Windows GDI画刷完全指南:从纯色到纹理,掌握UI绘制的核心技术 1. 项目概述从像素到画布理解Windows画刷的本质在Windows图形编程的世界里我们常常把注意力放在窗口、控件和消息循环这些宏观架构上。但真正让一个界面从“能用”变得“好看”从“功能正确”跃升到“体验舒适”的往往是那些最基础的图形元素。今天我们就来深挖其中一个基石——画刷Brush。如果你写过WM_PAINT消息处理在BeginPaint和EndPaint之间调用过Rectangle或FillRect那么你已经和画刷打过交道了。它就像画家手中的颜料和笔触决定了图形填充区域的“质地”和“色彩”。简单来说一个画刷对象HBRUSH在Windows GDI图形设备接口中定义了当你要填充一个封闭区域比如矩形、椭圆、多边形时这个区域内部应该呈现为什么样子。它绝不仅仅是“颜色”那么简单。一个纯色画刷是最直接的但Windows的画刷体系还包括了图案画刷用位图平铺、阴影画刷各种斜线、网格甚至逻辑画刷供更高级的图形操作使用。理解并熟练运用画刷是摆脱“默认灰色对话框”、实现自定义UI风格、绘制复杂图表和数据可视化的第一步。无论你是想给按钮加个渐变背景还是为游戏界面绘制一个纹理面板画刷都是你绕不开的核心工具。2. 画刷类型深度解析与选型逻辑在动手创建画刷之前我们必须清楚工具箱里有哪些“刷子”以及各自最适合的“绘画场景”。Windows提供了几种创建画刷的API每一种都对应着不同的内存开销、性能表现和视觉效果。2.1 库存画刷系统预制的快速选择这是最简单、最快捷的方式。Windows系统内部维护了一系列预定义的画刷称为“库存对象”Stock Objects。你可以通过GetStockObject函数并传入一个常量来获取它们的句柄。HBRUSH hBrushGray (HBRUSH)GetStockObject(GRAY_BRUSH); // 灰色画刷 HBRUSH hBrushWhite (HBRUSH)GetStockObject(WHITE_BRUSH); // 白色画刷 HBRUSH hBrushBlack (HBRUSH)GetStockObject(BLACK_BRUSH); // 黑色画刷 HBRUSH hBrushNull (HBRUSH)GetStockObject(NULL_BRUSH); // 空画刷不填充核心要点与选型理由零成本管理库存画刷的句柄不需要你用DeleteObject来删除。系统负责其生命周期这避免了资源泄漏的风险对于初学者或快速原型开发非常友好。性能最佳由于是系统全局共享的单例使用它们几乎没有额外内存开销。局限性颜色和样式是固定的只有有限的几种白、黑、灰、空等。GRAY_BRUSH的颜色实际上是浅灰色RGB(192,192,192)是对话框和窗口的默认背景色而不是中灰色。如果你需要任何非标准颜色库存画刷就无法满足需求了。注意NULL_BRUSH是一个特殊且极其有用的库存对象。当你创建一个画笔Pen来画一个形状的边框但不想填充内部时就可以选择NULL_BRUSH。例如画一个空心矩形或空心圆。2.2 纯色画刷自定义色彩的基石这是最常用、最灵活的画刷类型。通过CreateSolidBrush函数你可以创建任何RGB颜色的画刷。COLORREF crCustom RGB(255, 128, 0); // 创建一个橙色 HBRUSH hBrushOrange CreateSolidBrush(crCustom); // ... 使用画刷 DeleteObject(hBrushOrange); // 必须手动删除核心要点与选型理由无限色彩你可以使用RGB(r, g, b)宏或GetSysColor获取系统颜色来定义任何颜色。资源管理责任这是关键所有由CreateSolidBrush、CreateHatchBrush、CreatePatternBrush等函数创建的画刷都是GDI对象占用系统资源。你必须在使用完毕后且确保没有设备上下文DC正在使用它时调用DeleteObject来释放资源。忘记删除是导致GDI泄漏的常见原因长时间运行的程序会因此耗尽GDI资源而崩溃。性能考量纯色画刷开销很小创建和销毁速度快。在需要频繁切换颜色的动态绘制中如实时图表创建临时纯色画刷是常规操作。2.3 阴影画刷经典的图案化填充阴影画刷用预定义的线条图案来填充区域常用于表示背景、区分区域或实现一些经典的UI效果如灰掉的控件背景。使用CreateHatchBrush创建。// 常见的阴影样式 HBRUSH hBrushHS_HORIZONTAL CreateHatchBrush(HS_HORIZONTAL, RGB(0,0,0)); // 水平线 HBRUSH hBrushHS_VERTICAL CreateHatchBrush(HS_VERTICAL, RGB(255,0,0)); // 垂直线红色 HBRUSH hBrushHS_CROSS CreateHatchBrush(HS_CROSS, RGB(0,0,255)); // 十字线蓝色阴影样式常量包括HS_HORIZONTAL水平线HS_VERTICAL垂直线HS_FDIAGONAL正斜线HS_BDIAGONAL反斜线HS_CROSS十字线HS_DIAGCROSS斜十字线。核心要点与选型理由视觉区分在数据密集的界面如表格、配置面板中用不同的阴影画刷交替填充行可以极大提高可读性。颜色组合CreateHatchBrush的第二个参数是线条的颜色而背景色通常是当前DC的背景色可通过SetBkColor设置。这允许你创建双色图案。经典风格这种填充风格在早期的Windows应用程序和某些专业软件如CAD中很常见具有特定的功能暗示例如阴影区域可能表示不可编辑。2.4 位图画刷用图像填充的艺术这是功能最强大的画刷类型之一。你可以使用一个位图HBITMAP作为图案画刷会用这个位图像贴瓷砖一样平铺填充整个区域。使用CreatePatternBrush创建。// 假设 hBmp 是一个已经加载好的 8x8 像素位图句柄 HBRUSH hBrushPattern CreatePatternBrush(hBmp); // ... 使用画刷 DeleteObject(hBrushPattern); // 删除画刷 // 注意画刷会复制位图数据删除画刷后原始位图 hBmp 依然可以独立使用或删除。核心要点与选型理由无限创意你可以使用任何小尺寸的位图来创建独特的纹理如木纹、布料、网格、渐变等。性能陷阱这是重点位图画刷的性能开销远大于纯色画刷。图案越复杂、平铺次数越多填充区域大绘制速度越慢。绝对要避免在WM_PAINT或频繁调用的绘制函数中动态创建和销毁位图画刷。正确的做法是在程序初始化时如WM_CREATE创建好需要的位图画刷并缓存其句柄在整个程序生命周期中重复使用。位图管理用于创建画刷的位图最好是8x8, 16x16这样的小尺寸。虽然GDI支持任意尺寸但大位图会显著增加内存占用和绘制计算量。画刷会复制位图数据因此删除画刷不影响原位图。2.5 逻辑画刷高级图形操作的桥梁CreateBrushIndirect函数允许你通过一个LOGBRUSH结构体来创建画刷。这个结构体可以定义上述所有类型的画刷纯色、阴影、图案甚至包括设备无关位图DIB图案。它更常用于复杂图形操作和与ExtCreatePen创建几何画笔等高级API配合。LOGBRUSH lb; lb.lbStyle BS_SOLID; // 样式纯色 lb.lbColor RGB(0, 255, 0); // 颜色绿色 lb.lbHatch 0; // 阴影样式纯色时忽略 HBRUSH hBrushLog CreateBrushIndirect(lb);选型理由当你需要以统一、结构化的方式动态配置画刷属性或者你的画刷配置信息存储在数据结构中时CreateBrushIndirect比调用多个不同的CreateXXXBrush函数更清晰。但在日常开发中直接使用CreateSolidBrush等函数更为常见和直观。3. 画刷的实战应用与核心绘制流程理解了画刷的类型我们来看如何将它们应用到实际的窗口绘制中。核心流程围绕着设备上下文Device Context, DC和GDI绘图函数展开。3.1 绘制的基本范式选入DC与绘制函数在Windows GDI中所有的绘制都必须通过一个设备上下文HDC进行。你可以把DC想象成画家的画板上面有当前选择的画笔画边框、画刷填内部、字体等工具。绘制一个填充图形的典型代码如下case WM_PAINT: { PAINTSTRUCT ps; HDC hdc BeginPaint(hWnd, ps); // 获取窗口的绘图DC // 1. 创建或获取一个画刷 HBRUSH hBrushOld; // 用于保存旧的画刷以便最后恢复 HBRUSH hBrushNew CreateSolidBrush(RGB(100, 150, 200)); // 2. 将新画刷“选入”DC并保存旧画刷句柄 hBrushOld (HBRUSH)SelectObject(hdc, hBrushNew); // 3. 调用绘图函数。此时图形内部将用我们新选的蓝色画刷填充 Rectangle(hdc, 50, 50, 200, 150); // 绘制一个填充矩形 // 4. 恢复DC中原有的画刷非常重要 SelectObject(hdc, hBrushOld); // 5. 删除我们创建的GDI对象 DeleteObject(hBrushNew); EndPaint(hWnd, ps); } break;关键步骤解析获取DCBeginPaint用于在WM_PAINT消息中获取无效区域的DC。对于其他绘制如内存DC绘图可能需要GetDC或CreateCompatibleDC。创建画刷根据需求创建画刷对象。选入DCSelectObject将新画刷应用到DC上并返回之前DC中的画刷句柄。你必须保存这个返回的旧句柄。执行绘制调用Rectangle,Ellipse,RoundRect,Polygon,FillRect等函数。这些函数会使用DC中当前选中的画刷来填充图形内部。恢复与清理绘制完成后必须再次调用SelectObject将旧画刷选回DC。这是一种良好的编程习惯可以避免DC状态混乱。最后删除你自己创建的画刷对象。重要注意事项SelectObject的返回值必须保存并用于恢复。在WM_PAINT处理中系统提供的DC初始时可能带有一个默认画刷如WHITE_BRUSH。如果你不恢复而直接删除自己的画刷DC中将持有一个已被删除的无效句柄可能导致后续绘制出错或程序不稳定。3.2 使用FillRect更简单的填充方式如果你只是想填充一个矩形区域而不需要画边框FillRect函数是更直接的选择。它接受一个画刷句柄并且不需要你将画刷选入DC也无需关心恢复问题。case WM_PAINT: { PAINTSTRUCT ps; HDC hdc BeginPaint(hWnd, ps); HBRUSH hBrush CreateSolidBrush(RGB(255, 220, 180)); // 浅橙色画刷 RECT rcFill {20, 20, 150, 100}; // 直接使用画刷句柄填充矩形 FillRect(hdc, rcFill, hBrush); // 只需删除画刷无需SelectObject/恢复操作 DeleteObject(hBrush); EndPaint(hWnd, ps); } break;使用场景对比RectangleSelectObject需要同时绘制边框由当前画笔决定和填充内部。流程稍复杂但功能完整。FillRect仅填充矩形内部无边框。API更简洁资源管理更清晰。在自定义控件背景绘制、高亮选区等场景下非常高效。3.3 在对话框和控件中使用画刷除了主动绘制画刷还广泛应用于窗口的背景控制。通过处理WM_CTLCOLOR*系列消息你可以自定义对话框、静态文本、按钮、编辑框等控件的背景和文字颜色。// 在对话框过程函数或窗口过程中 case WM_CTLCOLORDLG: // 对话框背景色 case WM_CTLCOLORSTATIC: // 静态文本背景色 case WM_CTLCOLORBTN: // 按钮背景色 case WM_CTLCOLOREDIT: // 编辑框背景色 { HDC hdcStatic (HDC)wParam; HWND hwndCtl (HWND)lParam; // 创建一个浅黄色的背景画刷通常缓存起来避免重复创建 static HBRUSH hBrushBkgnd NULL; if (!hBrushBkgnd) hBrushBkgnd CreateSolidBrush(RGB(255, 255, 200)); // 设置文本背景为透明避免文字后面有难看的色块 SetBkMode(hdcStatic, TRANSPARENT); // 返回画刷句柄。系统会用这个画刷来擦除控件背景。 return (INT_PTR)hBrushBkgnd; }核心机制当系统需要绘制一个控件背景时会发送相应的WM_CTLCOLOR消息给其父窗口。在这个消息处理中你可以获取控件的设备上下文wParam。通过SetTextColor设置控件中文字的颜色。通过SetBkMode设置背景模式TRANSPARENT或OPAQUE。返回一个画刷句柄。系统将使用这个画刷来填充控件的背景区域。实操心得对于WM_CTLCOLOR返回的画刷其生命周期管理需要小心。通常的做法是在窗口创建时如WM_INITDIALOG创建并缓存这个画刷在窗口销毁时WM_DESTROY删除它。切勿在每次收到消息时都创建新画刷这会导致严重的GDI泄漏。同时将背景模式设为TRANSPARENT是让UI看起来现代、干净的关键一步否则文字背景会是一个不透明的矩形色块。4. 高级技巧与性能优化实战掌握了基础用法后一些高级技巧和性能考量能让你写出更专业、更高效的图形代码。4.1 画刷缓存杜绝资源泄漏与性能瓶颈这是最重要的优化实践。不要在绘制循环中频繁创建和删除画刷。错误示范性能灾难void OnPaint(HWND hWnd) { HDC hdc GetDC(hWnd); for (int i 0; i 1000; i) { HBRUSH hBrush CreateSolidBrush(GetRandomColor()); // 每次循环都创建 HBRUSH hOld SelectObject(hdc, hBrush); Rectangle(hdc, i*10, 0, i*108, 100); SelectObject(hdc, hOld); DeleteObject(hBrush); // 每次循环都删除 } ReleaseDC(hWnd, hdc); }正确做法缓存与复用// 在窗口类或全局层面定义画刷缓存 HBRUSH g_hBrushRed NULL; HBRUSH g_hBrushGreen NULL; HBRUSH g_hBrushBlue NULL; HBITMAP g_hBmpPattern NULL; HBRUSH g_hBrushPattern NULL; // 初始化时创建 void InitBrushes() { g_hBrushRed CreateSolidBrush(RGB(255,0,0)); g_hBrushGreen CreateSolidBrush(RGB(0,255,0)); g_hBrushBlue CreateSolidBrush(RGB(0,0,255)); // 加载一个小位图 g_hBmpPattern LoadBitmap(hInst, MAKEINTRESOURCE(IDB_PATTERN)); g_hBrushPattern CreatePatternBrush(g_hBmpPattern); } // 绘制时直接使用缓存的句柄 void OnPaint(HWND hWnd) { HDC hdc GetDC(hWnd); HBRUSH hOld SelectObject(hdc, g_hBrushRed); Rectangle(hdc, 10, 10, 100, 100); SelectObject(hdc, g_hBrushGreen); Ellipse(hdc, 120, 10, 220, 100); SelectObject(hdc, hOld); // 恢复原始画刷 ReleaseDC(hWnd, hdc); } // 程序退出时清理 void CleanupBrushes() { if (g_hBrushPattern) DeleteObject(g_hBrushPattern); if (g_hBmpPattern) DeleteObject(g_hBmpPattern); if (g_hBrushBlue) DeleteObject(g_hBrushBlue); if (gBrushGreen) DeleteObject(g_hBrushGreen); if (g_hBrushRed) DeleteObject(g_hBrushRed); }4.2 使用内存DC实现双缓冲与复杂绘制当绘制操作复杂如包含多个图形、位图画刷、频繁擦除重绘时直接在窗口DC上绘制会引起严重的闪烁。双缓冲技术是标准解决方案其核心是先在内存中的一个“离屏DC”上完成所有绘制然后一次性将结果“贴”到窗口上。void DrawComplexScene(HDC hdcWindow, RECT* rcClient) { // 1. 创建与窗口DC兼容的内存DC HDC hdcMem CreateCompatibleDC(hdcWindow); // 2. 创建与窗口DC兼容的位图大小等于客户区 HBITMAP hbmMem CreateCompatibleBitmap(hdcWindow, rcClient-right, rcClient-bottom); // 3. 将位图选入内存DC并保存旧位图 HBITMAP hbmOld (HBITMAP)SelectObject(hdcMem, hbmMem); // 4. 可选用背景色清空内存位图相当于准备一块干净画布 HBRUSH hBrushBkg GetStockObject(WHITE_BRUSH); FillRect(hdcMem, rcClient, hBrushBkg); // 5. 在内存DC上进行所有复杂的、可能引起闪烁的绘制操作 HBRUSH hBrush1 (HBRUSH)GetStockObject(LTGRAY_BRUSH); HBRUSH hBrush2 CreateSolidBrush(RGB(0, 100, 200)); SelectObject(hdcMem, hBrush1); Rectangle(hdcMem, 30, 30, 200, 200); SelectObject(hdcMem, hBrush2); Ellipse(hdcMem, 50, 50, 180, 180); // ... 更多绘制 DeleteObject(hBrush2); // 删除自定义画刷 // 6. 将内存DC中的最终图像一次性传输到窗口DC BitBlt(hdcWindow, 0, 0, rcClient-right, rcClient-bottom, hdcMem, 0, 0, SRCCOPY); // 7. 清理恢复内存DC的旧位图然后删除资源 SelectObject(hdcMem, hbmOld); DeleteObject(hbmMem); DeleteDC(hdcMem); }为什么能消除闪烁因为所有中间绘制步骤都在用户看不见的内存中进行窗口上只发生了一次快速的位图复制操作BitBlt视觉上就是瞬间完成没有中间状态的闪烁。4.3 区域裁剪与画刷的局部应用有时你只想用画刷填充一个不规则区域而不是整个矩形。这时需要结合HRGN区域对象和FillRgn、PaintRgn函数。// 创建一个圆角矩形区域 HRGN hRgn CreateRoundRectRgn(50, 50, 250, 150, 20, 20); HBRUSH hBrush CreateSolidBrush(RGB(180, 230, 180)); // 方法1用指定画刷填充区域 FillRgn(hdc, hRgn, hBrush); // 方法2将区域选入DC然后用DC当前画刷填充需先SelectObject画刷 // SelectObject(hdc, hBrush); // PaintRgn(hdc, hRgn); // 清理 DeleteObject(hRgn); DeleteObject(hBrush);这在制作非矩形窗口、自定义形状控件或实现特殊高亮效果时非常有用。5. 常见问题、调试技巧与资源管理实录即使理解了原理在实际编码中仍会踩坑。下面是一些典型问题及解决方法。5.1 GDI资源泄漏的检测与排查GDI对象泄漏是Windows GUI程序最常见的顽疾之一。症状是程序运行一段时间后界面绘制变慢、残缺最终弹出“资源不足”错误甚至崩溃。排查方法任务管理器运行你的程序打开任务管理器切换到“详细信息”选项卡找到你的进程右键点击列标题选择“选择列”勾选“GDI对象”。观察这个数字是否在程序运行期间特别是进行某些操作后持续增长而不下降。稳定或周期性回收是正常的持续增长就是泄漏。代码审查确保每个CreateSolidBrush,CreateHatchBrush,CreatePatternBrush,CreateCompatibleDC,CreateCompatibleBitmap等调用都有配对的DeleteObject或DeleteDC。特别注意在分支、循环和异常处理路径中也要确保删除。配对操作对于SelectObject养成“保存旧句柄 - 使用新句柄 - 恢复旧句柄”的固定模式。常见泄漏场景忘记DeleteObject最直接的原因。在WM_CTLCOLOR中动态创建画刷这个消息可能被频繁调用必须在外部缓存画刷。位图与画刷混淆用CreatePatternBrush创建画刷后只删除了画刷却忘记了删除用于创建画刷的源位图HBITMAP。两者都是需要独立删除的GDI对象。5.2 绘制效果不符合预期的调试问题画刷颜色不对或没生效。检查RGB值是否正确是否在绘制前成功将画刷选入了DC是否在绘制函数调用前意外恢复了旧画刷调试在SelectObject后可以调用GetCurrentObject(hdc, OBJ_BRUSH)来获取DC中当前画刷的句柄与你的预期句柄对比。问题阴影画刷背景色不对。检查阴影画刷的线条颜色由创建函数指定但背景色由DC的当前背景色SetBkColor设置和背景模式SetBkMode设置决定。如果你想要纯色背景可能需要先FillRect一个纯色矩形再绘制阴影图形。问题位图画刷不显示或显示错乱。检查用于创建画刷的位图HBITMAP是否成功加载句柄是否有效位图颜色深度是否与当前DC兼容尝试先用BitBlt绘制一下这个位图本身看是否正确。注意CreatePatternBrush对单色位图1位和彩色位图的处理方式不同。单色位图中颜色0通常为黑色的像素使用DC的当前文本颜色绘制颜色1白色的像素使用DC的当前背景色绘制。5.3 在多线程环境中使用画刷GDI对象句柄默认是进程相关的但GDI调用本身不是线程安全的。这意味着你可以在一个线程中创建画刷然后将其句柄传递给另一个线程使用。但是绝对不要多个线程同时调用GDI绘图函数如SelectObject,Rectangle操作同一个设备上下文HDC或竞争使用全局GDI对象。这会导致不可预知的崩溃或图形损坏。最佳实践是每个线程使用自己独立的DC和GDI对象或者通过线程同步机制如临界区、互斥量来确保对共享DC的访问是串行化的。5.4 从GDI到GDI更现代的填充方式对于新项目尤其是需要复杂渐变、透明度、反锯齿等效果时可以考虑使用GDI。GDI是GDI的增强版提供了更简洁的API和更强大的功能。#include Gdiplus.h using namespace Gdiplus; // 初始化GDI (通常在程序启动时) GdiplusStartupInput gdiplusStartupInput; ULONG_PTR gdiplusToken; GdiplusStartup(gdiplusToken, gdiplusStartupInput, NULL); // 使用GDI画刷进行填充 Graphics graphics(hdc); // 包装HDC SolidBrush solidBrush(Color(255, 100, 150, 200)); // 纯色画刷支持Alpha通道 LinearGradientBrush linGrBrush(Point(0,0), Point(100,0), Color(255,255,0,0), Color(255,0,0,255)); // 线性渐变 HatchBrush hatchBrush(HatchStyleCross, Color(255,0,0,0), Color(255,255,255)); // 阴影画刷 graphics.FillRectangle(solidBrush, 10, 10, 200, 100); graphics.FillEllipse(linGrBrush, 50, 50, 150, 150); // 关闭GDI (程序退出时) GdiplusShutdown(gdiplusToken);GDI的优点面向对象接口自动资源管理通过析构函数内置抗锯齿支持Alpha混合、渐变、路径等高级特性。GDI的优点更轻量系统兼容性极好所有Windows版本性能在简单操作上可能略有优势是理解Windows图形系统的基础。我个人在维护旧项目或进行底层UI组件开发时依然主要使用GDI画刷因为它直接、高效且无额外依赖。但在开发需要丰富视觉效果的新应用时会优先选择GDI或更现代的图形框架。理解GDI画刷是掌握Windows图形编程脉络的关键一步它能让你在面对任何图形需求时都拥有从底层解决问题的能力。
返回列表