第5章,[Win32 章节] :绘制填充区域
2026/8/29 4:31:53 网站建设 项目流程

专栏导航

上一篇:第5章,[Win32 章节] :绘图模式

回到目录

下一篇:第5章,[Win32 章节] :Polygon 函数和多边形填充模式

本节前言

对于本节所讲解的知识,有可能,你会需要时不时地参考本专栏的其它文章。真的遇到了需要参考之前的文章的知识点,请你自行查阅。

我呢,也会提到一部分的参考课节。但是呢,你不应该依赖于我的主动提及。最好呢,你自己能够多去了解和查看本专栏目录。

绘制填充区域,这是一个有趣的知识了。

在这一部分的知识里面,我们会学习到画刷。当我们学习完了画刷的知识以后,之前的某些个课节里面的图片,我们就可以用编程的方式,来将其绘制出来了。

关于 GDI 绘图,这应该算是一个相当重要的知识了。

佩措尔德原教材中,虽说也设置了不少的案例代码。不过,我还是希望,多提供一些个基础的代码案例,供大家来学习。并且呢,在这种,对于简单代码的学习之中,了解一些个基础的编程概念。

佩措尔德原教材里面,包含着许多的不易理解的代码。我希望,通过我的一番讲解,能够给大家降低一些学习难度。

当然了,为了降低学习难度,那么,免不了地,在讲解的时候,就需要多花费一点笔墨了,大家也会需要多阅读一些个文字了。

不过呢,想要做到深入浅出,想要尽量地让多一些的人,能够克服 Windows 编程学习中的许多的难点、卡点,多耗费一点笔墨,也就是必要的了。

不过呢,很多时候,我的话语,可能会比较啰嗦。有好的写作建议的,也欢迎提出来。

填充区域的部分,还是希望大家能够学好它。学好了以后,我就会带领大家一起来绘制之前的某些个课节中,用过的一些个图片了。

我们开始本节的学习。

本节内容,来自于对佩措尔德教材的大段抄录。

一. 本节内容

绘制线条的下一步就是如何填充一个封闭区域。下表列出了 Windows 用于绘制带有边框的填充区域的 7 个函数。

函数名称图 形
Rectangle直角矩形
Ellipse椭圆
RoundRect圆角矩形
Chord一个弓形,由椭圆圆周上的弧和一根弦组成
Pie椭圆上的一个扇形
Polygon多边形
PolyPolygon多个多边形

Windows 使用当前被选入设备环境的画笔来绘制图形的边框线,边框线使用当前的背景模式、背景颜色和绘图模式,这与 Windows 绘制线条一样。我们所学的关于线条的所有知识,都适用于这些图形的边框线。

Windows 使用当前选入设备环境的画刷来填充图形。在默认情况下,使用的是备用对象 WHITE_BRUSH,这就意味着使用白色来绘制图形内部。Windows 定义了 6 种备用画刷。

和选择备用画笔一样,可以将任何一种备用画刷选入设备环境。

Windows 定义的画刷的句柄为 HBRUSH 类型,所以要先定义一个画刷句柄变量。

HBRUSH hBrush;

获取 GRAY_BRUSH 句柄可以通过调用如下的 GetStockObject 函数来实现。

hBrush = (HBRUSH) GetStockObject (GRAY_BRUSH);

然后通过调用 SelectObject 函数来将它选入设备环境。

SelectObject (hdc, hBrush);

现在,当绘制上表中的任意图形时,内部都会变成灰色。

如果要绘制一个不含边框线的图形,则要把 NULL_PEN 画笔选入设备环境。

SelectObject (hdc, GetStockObject (NULL_PEN));

如果真想绘制图形的边框线,而不想填充图形的内部,可以将 NULL_BRUSH 选入设备环境。

SelectObject (hdc, GetStockObject (NULL_BRUSH));

也可以建立自定义画刷,就像建立自定义画笔一样。我们很快就会谈到创建自定义画刷的问题。

关于画刷的使用,也需要在用它填充图形的内部区域之前,将其选入设备环境。同时呢,用完了以后,需要将其选出设备环境。

二. 本节代码

对于本节知识,佩措尔德先生是没有给配置案例代码的。我想,这大概是因为,他不想给教材中配上太多的简单的代码。不过呢,我这里,倒是愿意多配置一些个简单和基础的代码。

因为,在这种看似简单和基础的代码之中,你可以熟悉许多的已有的知识。比如说设置窗口类,注册窗口类,等等。

我们首先把本节代码的绘制任务展示在下面。

绘制三个填充区域。
1. BLACK_PEN,LTGRAY_BRUSH,绘制矩形
2. 自定义红色画笔,NULL_BRUSH,椭圆
3. NULL_PEN,GRAY_BRUSH,圆角矩形

然后呢,下面就是完整的代码了。

/*------------------------------------------------------------ 绘制三个填充区域。 1. BLACK_PEN,LTGRAY_BRUSH,绘制矩形 2. 自定义红色画笔,NULL_BRUSH,椭圆 3. NULL_PEN,备用灰色画刷,圆角矩形 水饺编程,2026 ------------------------------------------------------------*/ #include <windows.h> LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); void DrawRect(HDC hdc, RECT* pRect); void DrawEllipse(HDC hdc, RECT* pRect); void DrawRoundRect(HDC hdc, RECT* pRect, int nWidth, int nHeight); int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) { static TCHAR szAppName[] = TEXT("DrawPadRegion_Lx"); HWND hwnd; MSG msg; WNDCLASS wndclass; wndclass.style = CS_HREDRAW | CS_VREDRAW; wndclass.lpfnWndProc = WndProc; wndclass.cbClsExtra = 0; wndclass.cbWndExtra = 0; wndclass.hInstance = hInstance; wndclass.hIcon = LoadIcon(NULL, IDI_APPLICATION); wndclass.hCursor = LoadCursor(NULL, IDC_ARROW); wndclass.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH); wndclass.lpszMenuName = NULL; wndclass.lpszClassName = szAppName; if (!RegisterClass(&wndclass)) { MessageBox(NULL, TEXT("注册窗口类失败!"), szAppName, MB_ICONERROR); return 0; } hwnd = CreateWindow(szAppName, // window class name TEXT("The DrawPadRegion_Lx Program"), // window caption WS_OVERLAPPEDWINDOW, // window style CW_USEDEFAULT, // initial x position CW_USEDEFAULT, // initial y position CW_USEDEFAULT, // initial x size CW_USEDEFAULT, // initial y size NULL, // parent window handle NULL, // window menu handle hInstance, // program instance handle NULL); // creation parameters ShowWindow(hwnd, iCmdShow); UpdateWindow(hwnd); while (GetMessage(&msg, NULL, 0, 0)) { TranslateMessage(&msg); DispatchMessage(&msg); } return msg.wParam; } LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) { HDC hdc; PAINTSTRUCT ps; static RECT rect, rect_01, rect_02; static int nWidth, nHeight; switch (message) { case WM_CREATE: rect.left = 100; rect.right = 400; rect.top = 30; rect.bottom = 230; rect_01.left = rect.left + 400; rect_01.right = rect.right + 400; rect_01.top = rect.top; rect_01.bottom = rect.bottom; rect_02.left = rect.left; rect_02.right = rect.right; rect_02.top = rect.top + 230; rect_02.bottom = rect.bottom + 230; nWidth = nHeight = 60; return 0; case WM_PAINT: hdc = BeginPaint(hwnd, &ps); DrawRect(hdc, &rect); DrawEllipse(hdc, &rect_01); DrawRoundRect(hdc, &rect_02, nWidth, nHeight); EndPaint(hwnd, &ps); return 0; case WM_DESTROY: PostQuitMessage(0); return 0; } return DefWindowProc(hwnd, message, wParam, lParam); } void DrawRect(HDC hdc, RECT* pRect) { HPEN hPen, hOldPen; HBRUSH hBrush, hOldBrush; hPen = (HPEN)GetStockObject(BLACK_PEN); hOldPen = (HPEN)SelectObject(hdc, hPen); hBrush = (HBRUSH)GetStockObject(LTGRAY_BRUSH); hOldBrush = (HBRUSH)SelectObject(hdc, hBrush); Rectangle(hdc, pRect->left, pRect->top, pRect->right, pRect->bottom); SelectObject(hdc, hOldPen); SelectObject(hdc, hOldBrush); } void DrawEllipse(HDC hdc, RECT* pRect) { HPEN hPen, hOldPen; HBRUSH hBrush, hOldBrush; COLORREF color; color = RGB(255, 0, 0); hPen = CreatePen(PS_SOLID, 0, color); hOldPen = (HPEN)SelectObject(hdc, hPen); hBrush = (HBRUSH)GetStockObject(NULL_BRUSH); hOldBrush = (HBRUSH)SelectObject(hdc, hBrush); Ellipse(hdc, pRect->left, pRect->top, pRect->right, pRect->bottom); SelectObject(hdc, hOldPen); DeleteObject(hPen); SelectObject(hdc, hOldBrush); } void DrawRoundRect(HDC hdc, RECT* pRect, int nWidth, int nHeight) { HPEN hPen, hOldPen; HBRUSH hBrush, hOldBrush; hPen = (HPEN)GetStockObject(NULL_PEN); hOldPen = (HPEN)SelectObject(hdc, hPen); hBrush = (HBRUSH)GetStockObject(GRAY_BRUSH); hOldBrush = (HBRUSH)SelectObject(hdc, hBrush); RoundRect(hdc, pRect->left, pRect->top, pRect->right, pRect->bottom, nWidth, nHeight); SelectObject(hdc, hOldPen); SelectObject(hdc, hOldBrush); }

上述代码呢,多数地方呢,应该都不难。但是呢,还是有这么几个地方,我需要来指出。

(一)DrawRect 函数讲解

图1

图1 中,111 行到 114 行,分别是获取备用画笔与画刷,并且将它们选入设备环境。在这里呢,在调用 SelectObject 的时候,我都是将这个函数的返回值,给保存在某个变量里面了。我将选入画笔的 SelectObject 函数调用的返回值赋给了 hOldPen 变量,将选入画刷的 SelectObject 函数调用的返回值赋给了 hOldBrush 变量。

这种写法,是我推荐的写法。在佩措尔德的原教材中,好多时候,也是采用了这种写法。

写啥要将 SelectObject 函数调用的返回值给保存起来呢?

因为,当我们使用 GDI 绘图函数,画完了我们想要绘制的图,或者是使用完了某个 GDI 对象以后,我们需要将这个 GDI 对象的句柄,给选出设备环境。在图1 里面,当我们在 116 行调用 Rectangle 函数,画完了矩形以后,我们临时获取的备用画笔 BLACK_PEN 和 备用画刷 LTGRAY_BRUSH,就完成了它们的绘图任务了。完成了以后呢,我们需要将它们选出设备环境。

于是乎,我们就得调用 SelectObject 函数,以 hOldPen 为参数,将 BLACK_PEN 的画笔句柄给选出设备环境,而将原来的画笔句柄给选入设备环境。我们还得调用 SelectObject 函数,以 hOldBrush 为参数,将 LTGRAY_BRUSH 的画刷句柄给选出设备环境,而将原来的画刷句柄给选入设备环境。

通过将原来的画笔句柄选入设备环境,也就同时将现在正在使用的画笔句柄给选出设备环境了。

通过将原来的画刷句柄给选入设备环境,也就同时将现在正在使用的画刷句柄给选出设备环境了。

我们学习 Windows 程序,那就需要建立一个习惯,某一个 GDI 对象,使用它完成了规定的绘图任务以后,要及时地将它选出设备环境,同时将原来的那个对象句柄给选入设备环境。

也就是说,假如我们使用某一个画笔,它的句柄被保存在 hPen 之中,此画笔被选入设备环境之时,SelectObject 返回的前一个画笔句柄被保存在 hOldPen 之中,那么,当 hPen 画笔的绘图任务完成了以后,要及时地通过函数调用【SelectObject (hdc, hOldPen)】,将 hPen 给选出设备环境,同时将原来的画笔句柄 hOldPen 给选入设备环境。

同理,假如我们使用某一个画刷,它的句柄被保存在 hBrush 之中国,此画刷呗选入设备环境之时,SelectObject 返回的前一个画刷句柄被保存在 hOldBrush 之中,那么,当 hBrush 画刷的绘图任务完成了以后,要及时地通过函数调用【SelectObject (hdc, hOldBrush)】,将 hBrush 给选出设备环境,同时将原来的画刷句柄 hOldBrush 给选入设备环境。

这不难理解。好比生活中,我们想要吃饭时,才将饭菜、酱油、醋等等拿到饭桌上。吃完了饭以后呢,我们还得将其放回去,放到橱柜里或者冰箱里。

Windows 编程的这种选入设备环境,使用,选出设备环境的流程,我觉得很有意思,大家呢,也需要多加体会,多加熟悉。

一开始呢,可能会觉得不习惯,有点怪。但是呢,代码看得多了,练习得多了,慢慢地也就习惯了。习惯了呢,也就能会用了。

(二)DrawEllipse 函数讲解

在这个函数里面呢,我们重点看下图的红色框线的部分。

图2

在图2 里面呢,我只讲解红色框线所示的 DeleteObject 函数的使用。在图1 里面,我们在用完了画笔 BLACK_PEN 和画刷 LTGRAY_BRUSH 之后,都用 SelectObject 函数将它们给选出设备环境了。而在图2 里面,在红色框线之中,对于画笔,用完了以后,不但是用 SelectObject 函数将其选出了设备环境,而且还将 hPen 画笔句柄通过 DeleteObject 函数给删除了它的绑定的 GDI 对象。

请问,为啥在图2 中,对于画笔,要用 DelectObject 将其删除呢?为啥图1 中的画笔,没有删除它绑定的画笔对象呢?

那是因为,在图1 中,我们用的画笔 BLACK_PEN,它是备用画笔,对于备用画笔,用完了以后,我们需要用 SelectObject 函数,来将其选出设备环境,但不能删除它。因为备用对象不能删除。而 图2 中的画笔,它不是备用画笔,而是我们用 CreatePen 函数,自定义的画笔。对于自定义的画笔,用完了以后,不但要使用 SelectObject 函数,来将其选出设备环境,还要在选出了设备环境以后,用 DeleteObject 函数,来将其绑定的 GDI 对象给删除掉。

为啥要删除掉呢?对于自定义画笔,以及以后我们要去学习的自定义画刷,用完了以后,如果不将其删除掉,它们就会白白地占用内存,直到程序关闭以后,它们占用的内存才会被释放掉。对于这种练习型程序,我们所创建的自定义 GDI 对象,比如画笔,它的数量还不是很多。

可是,以后在实际项目中,你可能需要创建好多好多的自定义画笔、画刷啥的。如果用完了以后,不能够及时删除掉它们,那么它们就会白白地占用着内存。白占的内存多了,容易造成内存的白白地浪费,还可能造成内存碎片啥的。

内存是一种资源,也需要程序员来合理地分配、使用,及时清理。不可以白白地浪费啊。

那么,在图2 所示的函数 DrawEllipse 中,为啥在 140 行,将画刷用 SelectObject 函数选出了设备环境以后,没有删除它呢?还是因为,我们在 DrawEllipse 中,我们所用的画刷 NULL_BRUSH,它是一个备用画刷。对于备用画刷,用完了以后,需要将其选出设备环境,但不能将它删除。

注意呢,在这里,对于自定义画笔,用完了以后,一定得是先将其选出设备环境,然后才是将其删除。如果没有将其选出设备环境,而直接将其删除,那是不行的。

另外呢,在图1 和图2 所示的程序中,在获取备用对象和将备用对象选入设备环境中的时候,我们对 GetStockObject 函数和 SelectObject 函数的返回值,均进行了强制类型转换,然后才是将其赋给句柄变量。这种强制类型转换,也是需要大家注意的。

学习编程,还是要应该本着专业、负责的精神,来书写代码。

Windows 程序,自有其使用的专业流程与秩序,请大家遵守这种秩序。即使程序不报错,我们也要养成良好的編程思维与习惯。

三. 本节代码的编译和运行

我将这个解决方案与项目的名称,设置成了 DrawPadRegion_Lx,将原文件依然设置为 WinMain.cpp 。

然后呢,关于编译方法,如果你忘了,可以参考下面的两篇文章。

参考课节:使用 VS2010 编写 C语言程序

参考课节:用 VS2019 编写C语言程序

本节代码的运行结果如下图所示。

图1

结束语

与佩措尔德先生的教材相比,我这里,多给了一点代码案例。

我觉得,很多时候,我给的代码案例,应该都会比佩措尔德的代码,要简单许多。

因为,我这会儿,也写不出来太难,水平太高的代码。不过是些基础的代码而已。

强调一下,对于备用画笔、画刷等等的 GDI 对象,用完了以后,要将它们选出设备环境,但不能删除它们。

对于自定义画笔、画刷等等的 GDI 对象,用完了以后,要将它们选出设备环境,然后还要将其删除掉。选出在前,删除在后,顺序不要乱掉。

希望你能够学好本节知识。

本节代码呢,我会将其放在代码仓库里面。

以下课节提供了代码仓库的位置。

参考课节:MFC笔记:本专栏课件

好了,本节结束。

专栏导航

上一篇:第5章,[Win32 章节] :绘图模式

回到目录

下一篇:第5章,[Win32 章节] :Polygon 函数和多边形填充模式

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

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

立即咨询