1. 项目概述:为什么从实心圆开始?
在VC++(Visual C++)的图形编程世界里,绘制一个实心圆,听起来像是“Hello World”级别的入门练习。很多新手教程会一笔带过,直接甩给你几行调用Ellipse函数的代码。但如果你真这么想,可能就错过了理解Windows图形设备接口(GDI)核心机制的最佳切入点。我干了十几年Windows桌面开发,带过不少新人,发现能把一个实心圆画明白、画高效、画得在不同场景下都稳定可靠的开发者,其实并不多。这个看似简单的任务,背后串联起了设备上下文(DC)、画笔(Pen)、画刷(Brush)、坐标映射、重绘机制等一系列关键概念。
最近我在一些技术社区和热搜里,看到很多人在搜“VC++基础教程”、“电脑vc++库自检”,甚至“微软 vc++ 2015-2022 x64 运行库”,这说明依然有大量开发者,特别是学生和转行进入Windows桌面开发的朋友,正在这个生态里摸索。而“实心圆绘制”恰好是一个绝佳的沙箱,它不涉及复杂的网络拓扑图、PCB布线或者三维饼图,却能让你把GDI的“内功”练扎实。当你理解了如何在VC++里优雅地画出一个实心圆,再去应对“Android View绘制流程”或者“WebGL绘制粗线”这些不同平台、不同API的图形挑战时,你会发现自己有了一个稳固的参照系。
所以,这篇内容我们不搞花架子,就深挖“VC++实现实心圆的绘制”这一件事。我会带你从零开始,不仅写出能运行的代码,更要弄懂每一个参数的意义、每一种方法的选择背后的考量,以及在实际项目中可能踩到的坑。无论你是刚安装好Visual Studio的新手,还是想重温GDI基础的老兵,相信都能从中获得一些实实在在的收获。
2. 核心原理与GDI基础探秘
2.1 图形绘制的基石:设备上下文(DC)
在Windows里,你不能直接拿支“笔”就在屏幕上乱画。所有的绘制操作,都必须通过一个叫做“设备上下文”(Device Context, DC)的抽象层来进行。你可以把DC理解为一个画家的“绘画许可证”和“工具箱打包清单”。它不仅仅关联着一块具体的“画布”(可能是屏幕、内存、打印机),还管理着当前绘画所用的所有工具属性,比如用什么颜色的笔(画笔)、用什么图案的刷子(画刷)、用什么字体写字等等。
当我们说“在窗口上画图”,本质上是先获取这个窗口的DC,然后使用这个DC进行一系列GDI函数调用。对于实心圆绘制,DC里最关键的两个工具属性就是画刷(HBRUSH)和画笔(HPEN)。画刷决定了封闭图形(如矩形、椭圆、多边形)内部的填充颜色和样式;画笔决定了图形轮廓线的颜色、粗细和样式。我们要画一个实心圆,就意味着需要关注画刷,而轮廓线画笔有时可以设置为空(NULL_PEN)来忽略。
注意:很多初学者会混淆
Rectangle和Ellipse函数对画笔和画刷的使用。记住一个原则:画刷管“肚子”,画笔管“边儿”。对于实心圆,我们主要操作画刷。
2.2 关键API:Ellipse函数详解
GDI提供了Ellipse函数来绘制椭圆和圆。它的原型如下:
BOOL Ellipse( HDC hdc, // 设备上下文句柄 int left, // 外接矩形左上角x坐标 int top, // 外接矩形左上角y坐标 int right, // 外接矩形右下角x坐标 int bottom // 外接矩形右下角y坐标 );这个函数的原理非常直观:它绘制一个由参数指定的矩形(left, top, right, bottom)内切产生的椭圆。当这个矩形是正方形时,画出来的自然就是一个正圆。所以,画圆的关键变成了如何计算出一个正方形的外接矩形坐标。
函数执行时,会使用DC中当前选中的画笔来绘制椭圆的轮廓,并使用当前选中的画刷来填充椭圆的内部。这就是为什么在调用Ellipse之前,我们通常需要创建并选入(SelectObject)特定的画刷和画笔。
2.3 颜色与坐标:RGB与RECT
在GDI中,颜色通常用RGB宏来定义,它接受三个0-255的整数参数,分别代表红、绿、蓝的强度。例如,RGB(255, 0, 0)是红色,RGB(0, 255, 0)是绿色,RGB(0, 0, 255)是蓝色,RGB(255, 255, 255)是白色,RGB(0, 0, 0)是黑色。
坐标系统默认以客户区的左上角为原点(0, 0),X轴向右递增,Y轴向下递增。这与我们常见的数学坐标系Y轴向上不同,需要特别注意。我们绘制的矩形区域用RECT结构体表示,包含left,top,right,bottom四个成员。Ellipse函数画出的图形会填充这个矩形定义的空间,但并不会包括右下边界(即填充区域为[left, top) 到 (right, bottom))。这是一个容易忽略的细节,在精确计算像素位置时很重要。
3. 从零开始:一个完整的实心圆绘制实现
3.1 环境准备与项目创建
首先,确保你有一个可用的VC++开发环境。我推荐使用Visual Studio 2019或2022社区版,它们免费且对C++的支持非常完善。安装时,记得勾选“使用C++的桌面开发”工作负载,这会自动安装必要的VC++运行库和头文件。
创建一个新的Windows桌面应用程序项目:
- 打开Visual Studio,选择“创建新项目”。
- 搜索并选择“Windows桌面向导”(C++),点击下一步。
- 输入项目名称,例如“SolidCircleDemo”,选择位置,点击创建。
- 在应用程序类型中,选择“桌面应用程序(.exe)”,在附加选项中,勾选“空项目”,然后点击确定。
这样我们就得到了一个干净的Win32项目骨架。接下来,我们需要添加一个主源文件(如main.cpp),并实现窗口过程和绘制代码。
3.2 基础窗口框架搭建
任何GDI绘制都需要一个载体窗口。我们先搭建一个最简化的Win32窗口程序。这段代码是标准模板,它创建了一个窗口,并处理了基本的消息循环。
#include <windows.h> // 声明窗口过程函数 LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam); int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) { // 1. 注册窗口类 const wchar_t CLASS_NAME[] = L"SolidCircleWindowClass"; WNDCLASS wc = {}; wc.lpfnWndProc = WindowProc; wc.hInstance = hInstance; wc.lpszClassName = CLASS_NAME; wc.hCursor = LoadCursor(NULL, IDC_ARROW); wc.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1); // 默认背景色 RegisterClass(&wc); // 2. 创建窗口 HWND hwnd = CreateWindowEx( 0, CLASS_NAME, L"VC++ 实心圆绘制示例", WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, 800, 600, // 窗口初始位置和大小 NULL, NULL, hInstance, NULL ); if (hwnd == NULL) { return 0; } // 3. 显示和更新窗口 ShowWindow(hwnd, nCmdShow); UpdateWindow(hwnd); // 4. 消息循环 MSG msg = {}; while (GetMessage(&msg, NULL, 0, 0)) { TranslateMessage(&msg); DispatchMessage(&msg); } return 0; }这个框架创建了一个带标题栏、可缩放的标准窗口。核心在于WindowProc函数,我们将在这里处理绘制消息。
3.3 在WM_PAINT消息中绘制实心圆
窗口的绘制发生在响应WM_PAINT消息时。这是GDI编程的核心模式:系统告诉你“窗口的某个区域需要重画了”,你就在此时进行绘制操作。直接向窗口DC绘图是无效的,必须遵循“BeginPaint/EndPaint”的流程。
下面是完整的WindowProc函数实现,其中包含了实心圆的绘制逻辑:
LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) { switch (uMsg) { case WM_PAINT: { PAINTSTRUCT ps; HDC hdc = BeginPaint(hwnd, &ps); // 获取窗口的绘制DC // --- 核心绘制代码开始 --- // 1. 创建一个红色的实心画刷 HBRUSH hRedBrush = CreateSolidBrush(RGB(255, 0, 0)); // 2. 创建一个空画笔(不绘制轮廓),也可以创建特定画笔,如 CreatePen(PS_SOLID, 1, RGB(0,0,0)) HPEN hNullPen = (HPEN)GetStockObject(NULL_PEN); // 3. 将我们创建的工具选入DC,并保存旧的工具句柄 HBRUSH hOldBrush = (HBRUSH)SelectObject(hdc, hRedBrush); HPEN hOldPen = (HPEN)SelectObject(hdc, hNullPen); // 4. 计算圆形的位置和大小 RECT rcClient; GetClientRect(hwnd, &rcClient); // 获取窗口客户区大小 int clientWidth = rcClient.right - rcClient.left; int clientHeight = rcClient.bottom - rcClient.top; // 定义圆的半径(取窗口短边的一半的80%,确保圆在窗口内) int circleRadius = (min(clientWidth, clientHeight) / 2) * 0.8; // 计算外接正方形矩形的左上角和右下角坐标,使圆居中 int left = (clientWidth / 2) - circleRadius; int top = (clientHeight / 2) - circleRadius; int right = (clientWidth / 2) + circleRadius; int bottom = (clientHeight / 2) + circleRadius; // 5. 调用Ellipse函数绘制实心圆 Ellipse(hdc, left, top, right, bottom); // 6. 恢复DC原来的工具(重要!避免资源泄漏和状态混乱) SelectObject(hdc, hOldBrush); SelectObject(hdc, hOldPen); // 7. 删除我们创建的GDI对象,释放资源 DeleteObject(hRedBrush); DeleteObject(hNullPen); // 注意:对于GetStockObject获取的对象,不要Delete! // --- 核心绘制代码结束 --- EndPaint(hwnd, &ps); // 结束绘制 return 0; } case WM_DESTROY: { PostQuitMessage(0); return 0; } default: return DefWindowProc(hwnd, uMsg, wParam, lParam); } }将这段WindowProc函数代码添加到main.cpp中WinMain函数之前,编译并运行,你应该能看到一个居中的红色实心圆。当你调整窗口大小时,圆会随着窗口中心动态变化(因为每次WM_PAINT都会重新计算位置)。
3.4 代码逐行解析与关键点
CreateSolidBrush: 这是创建实心画刷的函数。参数是一个COLORREF(通常由RGB宏生成)。画刷决定了填充色。GetStockObject(NULL_PEN): 获取一个库存GDI对象——空画笔。库存对象是系统预定义的,无需删除。使用空画笔意味着绘制的图形没有轮廓线。如果你想有轮廓,可以用CreatePen(PS_SOLID, 宽度, 颜色)。SelectObject: 这是GDI编程中极其重要的函数。它将一个GDI对象(如画刷、画笔、字体)选入指定的DC,并返回DC中之前同类型的对象句柄。你必须保存这个返回的旧句柄,在绘制完成后用它恢复DC原状。这是良好的编程习惯,能避免状态污染和资源问题。GetClientRect: 获取窗口客户区(即去除了标题栏、边框后的可绘制区域)的大小。这是我们计算绘制坐标的基础。- 坐标计算: 我们通过使圆的中心点
(centerX, centerY)等于客户区中心(clientWidth/2, clientHeight/2)来实现居中。外接正方形的计算就是中心点坐标加减半径。 Ellipse: 执行绘制。此时DC中当前选中的是红色画刷和空画笔,所以结果是一个无边框的红色实心圆。- 资源管理:
SelectObject恢复旧对象后,调用DeleteObject删除我们创建的画刷和画笔。切记:不要删除通过GetStockObject获取的对象,只删除自己通过CreateXXX创建的对象。资源泄漏是GDI编程常见的错误,会导致程序运行一段时间后界面异常或崩溃。
4. 进阶技巧与性能优化实战
4.1 避免闪烁:双缓冲绘图技术
如果你运行上面的代码,在拖动窗口改变大小时,可能会观察到圆在重绘时有轻微的闪烁。这是因为WM_PAINT直接向屏幕DC绘制,在复杂或频繁重绘的场景下,屏幕的更新过程会被用户看到,产生闪烁。解决方案是双缓冲。
双缓冲的原理是:先在内存中创建一个“离屏”的位图DC,把所有绘制操作都先在这个内存DC上完成,然后一次性将这个内存位图“贴”到屏幕DC上。因为贴图操作很快,用户就看不到中间的绘制过程了。
以下是修改后的WM_PAINT处理代码,加入了双缓冲:
case WM_PAINT: { PAINTSTRUCT ps; HDC hdc = BeginPaint(hwnd, &ps); RECT rcClient; GetClientRect(hwnd, &rcClient); // 1. 创建兼容的内存DC HDC hMemDC = CreateCompatibleDC(hdc); // 2. 创建兼容的位图,大小与客户区相同 HBITMAP hMemBitmap = CreateCompatibleBitmap(hdc, rcClient.right, rcClient.bottom); // 3. 将位图选入内存DC,并保存旧位图 HBITMAP hOldBitmap = (HBITMAP)SelectObject(hMemDC, hMemBitmap); // 4. 先用窗口背景色填充内存位图(模拟背景) HBRUSH hBgBrush = (HBRUSH)GetStockObject(WHITE_BRUSH); // 假设背景为白色 FillRect(hMemDC, &rcClient, hBgBrush); // --- 在内存DC上绘制实心圆(代码与之前类似)--- HBRUSH hRedBrush = CreateSolidBrush(RGB(255, 0, 0)); HPEN hNullPen = (HPEN)GetStockObject(NULL_PEN); HBRUSH hOldBrush = (HBRUSH)SelectObject(hMemDC, hRedBrush); HPEN hOldPen = (HPEN)SelectObject(hMemDC, hNullPen); int circleRadius = (min(rcClient.right, rcClient.bottom) / 2) * 0.8; int left = (rcClient.right / 2) - circleRadius; int top = (rcClient.bottom / 2) - circleRadius; int right = (rcClient.right / 2) + circleRadius; int bottom = (rcClient.bottom / 2) + circleRadius; Ellipse(hMemDC, left, top, right, bottom); SelectObject(hMemDC, hOldBrush); SelectObject(hMemDC, hOldPen); DeleteObject(hRedBrush); // --- 绘制结束 --- // 5. 将内存DC的内容一次性拷贝到屏幕DC BitBlt(hdc, 0, 0, rcClient.right, rcClient.bottom, hMemDC, 0, 0, SRCCOPY); // 6. 清理资源:恢复内存DC的旧位图,并删除位图和内存DC SelectObject(hMemDC, hOldBitmap); DeleteObject(hMemBitmap); DeleteDC(hMemDC); EndPaint(hwnd, &ps); return 0; }实操心得:双缓冲是改善GDI绘图体验的利器,尤其对于动态图形、游戏或频繁更新的UI。它的代价是额外的内存开销和一次位图拷贝操作。对于静态或简单的界面,直接绘制可能更高效。需要根据实际情况权衡。
4.2 绘制多个与动态变化的圆
单一静态圆只是开始。实际项目中,我们可能需要绘制多个圆,或者让圆能够响应鼠标、键盘事件动态变化。这涉及到状态管理和无效区域的概念。
状态管理:我们需要在窗口过程之外(例如全局变量、窗口类额外数据、或者更好的,C++类成员变量中)保存圆的信息,如圆心坐标、半径、颜色等。
无效区域:当你改变了需要绘制的内容(比如移动了圆的位置),你需要告诉Windows:“窗口的某某区域现在内容过时了,需要重画”。这是通过InvalidateRect函数实现的。它会在消息队列中产生一个WM_PAINT消息,但系统会智能地合并多个无效区域请求,并在合适的时机(通常是消息队列空闲时)进行重绘,而不是立即绘制。
下面是一个简单示例,演示如何通过鼠标点击改变圆的颜色:
首先,定义存储圆信息的结构体和全局(或静态)变量。
struct CircleInfo { int centerX, centerY; int radius; COLORREF color; }; static CircleInfo g_circle = {400, 300, 100, RGB(255, 0, 0)}; // 初始状态修改
WM_PAINT中的绘制代码,使用g_circle的数据。// 在WM_PAINT中... HBRUSH hCircleBrush = CreateSolidBrush(g_circle.color); // ... 计算left, top, right, bottom 基于 g_circle.centerX, centerY, radius Ellipse(hMemDC, left, top, right, bottom);在
WindowProc中处理WM_LBUTTONDOWN(鼠标左键按下)消息,改变颜色并触发重绘。case WM_LBUTTONDOWN: { // 随机生成一个新颜色 g_circle.color = RGB(rand() % 256, rand() % 256, rand() % 256); // 标记整个客户区为无效,需要重绘 InvalidateRect(hwnd, NULL, TRUE); // TRUE表示擦除背景 return 0; }
InvalidateRect的第二个参数可以指定一个具体的矩形区域,只重绘该区域以提升性能。第三个参数为TRUE会先发送WM_ERASEBKGND消息擦除背景,为FALSE则保留原有背景,直接在上面绘制。在我们的双缓冲例子中,因为我们在内存DC中自己填充了背景,所以这个参数影响不大。
4.3 抗锯齿与更高质量的绘制
标准的GDIEllipse函数绘制的是锯齿边缘的图形。如果你需要平滑的边缘(抗锯齿),GDI本身支持有限。一个方法是使用GDI+,它是随Windows XP引入的更先进的图形API。
使用GDI+绘制抗锯齿实心圆的步骤:
- 在项目中包含GDI+头文件和链接库。
- 初始化GDI+环境(通常在程序启动和退出时)。
- 在绘制代码中使用
Graphics、SolidBrush、Pen等类。
以下是使用GDI+修改绘制部分的示例片段:
#include <gdiplus.h> #pragma comment(lib, "gdiplus.lib") using namespace Gdiplus; // ... 在WinMain中初始化GDI+ ... ULONG_PTR gdiplusToken; GdiplusStartupInput gdiplusStartupInput; GdiplusStartup(&gdiplusToken, &gdiplusStartupInput, NULL); // ... 在WindowProc的WM_PAINT中 ... // 假设我们已经有了内存DC hMemDC 和其对应的Graphics对象 graphics(需从hMemDC创建) Graphics graphics(hMemDC); graphics.SetSmoothingMode(SmoothingModeAntiAlias); // 开启抗锯齿 SolidBrush brush(Color(255, 255, 0, 0)); // ARGB颜色,255表示不透明 // GDI+的坐标和大小可以使用浮点数,更精确 graphics.FillEllipse(&brush, (REAL)left, (REAL)top, (REAL)(right-left), (REAL)(bottom-top)); // ... 程序退出前 ... GdiplusShutdown(gdiplusToken);GDI+提供了更丰富的功能,如渐变画刷、路径、图像处理等,但也会引入额外的复杂性和轻微的性能开销。对于大多数传统的桌面UI,标准GDI已经足够。是否需要GDI+取决于你对图形质量的具体要求。
5. 常见问题排查与深度避坑指南
即使是一个简单的实心圆,在实际编码和调试中也会遇到各种问题。下面我整理了一些典型问题及其解决方案,很多都是我在早期项目中踩过的坑。
5.1 问题一:圆没有显示出来
- 症状:窗口一片空白,或者只有背景色,看不到圆。
- 排查步骤:
- 检查
WM_PAINT处理:确保你的绘制代码确实在WM_PAINT消息分支里,并且正确调用了BeginPaint和EndPaint。 - 检查坐标计算:这是最常见的原因。打印或调试输出你计算出的
left, top, right, bottom值。确保它们都在客户区范围内,并且left < right且top < bottom。一个常见的错误是坐标计算出现负数或零,导致矩形无效。 - 检查画刷和画笔:确认你创建并选入了有效的画刷。尝试使用一个库存画刷
GetStockObject(LTGRAY_BRUSH)来测试,排除画刷创建失败的问题。同时,检查你是否不小心选入了一个NULL_BRUSH(库存的空画刷),那会导致无法填充。 - 检查DC有效性:确保
BeginPaint返回的hdc不是NULL。 - 背景覆盖:如果你在
WM_ERASEBKGND消息中返回了TRUE,或者窗口类背景画刷擦除了客户区,它可能会在WM_PAINT之后执行,覆盖你的绘制。可以在WM_PAINT中使用双缓冲并自己填充背景,或者在窗口类注册时设置wc.hbrBackground = NULL并处理WM_ERASEBKGND返回TRUE(表示已处理背景擦除)。
- 检查
5.2 问题二:圆的位置或大小不对,不居中
- 症状:圆偏离中心,或者不是正圆。
- 排查步骤:
- 验证客户区坐标:使用
GetClientRect获取的right和bottom是宽度和高度值,而不是右下角坐标的绝对像素值?不对!这里是个关键点:RECT的right和bottom是排他性的边界。即客户区有效像素的X坐标范围是[0, right),Y坐标范围是[0, bottom)。所以中心点应该是(right/2, bottom/2)。计算半径和矩形边界时要用整数运算,注意避免因整数除法导致的中心点偏移。 - 确保是正方形:圆的半径
radius在计算left, top, right, bottom时,必须使用相同的值。right-left应该等于bottom-top。如果不等,画出的就是椭圆。 - 考虑窗口边框和菜单:
GetClientRect获取的是客户区,不包括非客户区。如果你的窗口有菜单、工具栏,客户区的原点(0,0)就在这些区域的下方。这通常是我们期望的。但如果你需要基于整个窗口计算,要用GetWindowRect,然后转换坐标,这通常更复杂。
- 验证客户区坐标:使用
5.3 问题三:程序运行一段时间后界面异常或崩溃
- 症状:程序运行几分钟或操作后,绘图出现乱码,或者直接崩溃。
- 排查步骤:
- GDI对象泄漏:这是Win32 GDI编程的“头号杀手”。务必检查每一个
CreateSolidBrush,CreatePen,CreateCompatibleDC,CreateCompatibleBitmap等创建的句柄,是否都有对应的DeleteObject或DeleteDC。特别注意:SelectObject返回的旧句柄,在删除自己创建的新对象之前,应该先被选回DC(或者确保DC即将被释放)。一个良好的模式是:HGDIOBJ hOld = SelectObject(hdc, hNew); // ... 绘制操作 ... SelectObject(hdc, hOld); // 恢复旧对象 DeleteObject(hNew); // 删除新对象 - 库存对象:通过
GetStockObject获取的对象(如NULL_PEN,WHITE_BRUSH)绝对不要调用DeleteObject。 - 内存DC和位图泄漏:双缓冲中创建的
hMemDC和hMemBitmap必须成对删除。顺序一般是:先SelectObject恢复旧的位图到内存DC,然后DeleteObject删除内存位图,最后DeleteDC删除内存DC。 - 多线程访问:GDI对象和DC不是线程安全的。如果你在多个线程中操作同一个DC或GDI对象,会导致未定义行为。所有GUI绘制操作都应在主线程(窗口过程所在线程)中进行。
- GDI对象泄漏:这是Win32 GDI编程的“头号杀手”。务必检查每一个
5.4 问题四:绘制性能差,拖动窗口卡顿
- 症状:拖动窗口改变大小时,圆的重绘跟不上,感觉卡顿。
- 解决方案:
- 使用双缓冲:如前所述,双缓冲能有效减少闪烁,但本身不能提升绘制速度。它只是把多次绘制合并成一次位图拷贝。
- 减小无效区域:不要总是
InvalidateRect(hwnd, NULL, TRUE)(使整个客户区无效)。如果你只改变了一个小圆的位置,可以只使圆所在的旧区域和新区域无效。计算这两个区域的并集矩形,传递给InvalidateRect。RECT rcOld, rcNew; // 计算旧圆和新圆的外接矩形 rcOld = CalculateCircleRect(oldCenter, oldRadius); rcNew = CalculateCircleRect(newCenter, newRadius); // 使两个矩形区域无效 InvalidateRect(hwnd, &rcOld, FALSE); InvalidateRect(hwnd, &rcNew, FALSE); - 避免在WM_PAINT中做复杂计算:
WM_PAINT可能被频繁调用。将可以缓存的计算结果(如圆的坐标、画刷句柄)移到外部变量中,只在数据改变时重新计算。 - 考虑更现代的图形API:如果对性能要求极高,且项目允许,可以考虑使用Direct2D或OpenGL。但对于传统的业务UI和简单的图形,优化后的GDI通常足够。
5.5 一个实用的调试技巧:验证GDI对象创建
在调试时,可以在创建GDI对象后立即检查句柄是否有效。虽然CreateSolidBrush在失败时返回NULL,但一些更复杂对象的创建失败可能不那么明显。一个简单的验证方法是使用GetObjectType。
HBRUSH hBrush = CreateSolidBrush(someColor); if (hBrush == NULL || GetObjectType(hBrush) == 0) { // 对象创建失败或无效 DWORD err = GetLastError(); // 可以输出错误信息或使用默认画刷 hBrush = (HBRUSH)GetStockObject(BLACK_BRUSH); }通过系统地理解原理、谨慎地管理资源、并善用调试技巧,你就能在VC++的图形编程路上走得更稳更远。这个实心圆,就是你图形世界的第一块坚实基石。