C++图形编程入门:使用EasyX绘制同心圆,掌握事件驱动与坐标系统
2026/7/30 4:09:47 网站建设 项目流程

1. 项目概述:从“画个圆”到图形编程的思维跃迁

“用C++画个同心圆”,这个标题乍一看简单得甚至有些幼稚,仿佛回到了初学编程时在控制台打印星星的年代。但当你真正动手,尤其是引入EasyX这样的图形库时,你会发现,这远不止是调用几个绘图函数那么简单。它是一次从“命令行逻辑”到“图形界面思维”的完整跨越,是理解事件驱动、坐标系统、图形刷新等核心概念的绝佳切入点。我见过不少学了半年C++语法、能熟练操作链表和排序算法的朋友,在面对一个空白窗口,想要画点东西时却瞬间茫然。这个项目,正是为了填补这道理论与实践之间的鸿沟。

简单来说,我们将使用Visual Studio(或任何支持EasyX的IDE)结合C++,创建一个窗口程序,在窗口中绘制一组圆心相同、半径等差递增的同心圆。最终效果是一个静态的、整洁的圆环图案。但别小看它,在这个过程中,你会亲手搭建一个图形编程环境,理解设备上下文(Device Context)的概念,掌握基本的绘图API,并学会如何组织一个简单的图形程序结构。无论你是想为后续的游戏开发、数据可视化打基础,还是单纯想给枯燥的算法学习增添一些可视化的乐趣,这个项目都再合适不过。

2. 环境搭建与工具选型:为什么是EasyX和VS?

工欲善其事,必先利其器。选择正确的工具链,能让你避开无数初学者的坑。

2.1 开发环境:Visual Studio 2022社区版

首选无疑是Visual Studio。对于Windows平台的C++开发,尤其是涉及图形库,VS的兼容性和调试体验是最好的。社区版完全免费,功能强大。

注意:网络上很多教程还在用VC++6.0或VS2010,强烈建议使用VS2019或VS2022。旧版本对C++新标准支持差,且安装EasyX可能遇到兼容性问题。

安装时,在“工作负载”中勾选“使用C++的桌面开发”即可。安装完成后,务必检查是否安装了对应的“Microsoft Visual C++ Redistributable”运行库。这是运行C++程序的基础,通常VS安装时会一并装上,但如果后续运行程序报错“找不到VCRUNTIME140.dll”,就需要去微软官网单独下载安装对应版本。

2.2 图形库:为什么选择EasyX?

图形库的选择很多,比如OpenGL、DirectX、SDL2等,但它们对于初学者来说门槛太高,涉及三维变换、着色器、硬件加速等复杂概念。EasyX(全称Easy Graphics Library)的定位非常明确:为C/C++语言初学者提供一个简单、直观的图形窗口和绘图接口。

它的核心优势在于:

  1. 极简APIinitgraph创建窗口,circle画圆,line画线,函数名即功能,直观易懂。
  2. 无缝集成:下载一个.h头文件和一个.lib库文件,放入VS目录,即可像使用标准库一样使用。
  3. 即时反馈:代码修改后编译运行,图形结果立刻呈现,学习成就感强。
  4. 足够深入:虽然简单,但它也提供了鼠标键盘消息处理、图像处理、批量绘图等进阶功能,足以支撑小型游戏和图形演示程序的开发。

去EasyX官网下载最新版本。安装过程其实就是将graphics.hgraphics.lib等文件复制到VS的包含目录和库目录。官网提供了详细的安装教程,通常只需运行一个安装程序或手动复制几个文件即可。

2.3 第一个图形窗口:Hello, EasyX!

环境配置好后,我们来写一个最简单的程序验证一下。打开VS,新建一个“空项目”,添加一个.cpp源文件。

#include <graphics.h> // 包含EasyX图形库头文件 #include <conio.h> // 用于_getch()函数,等待按键 int main() { // 1. 初始化图形窗口:宽度640像素,高度480像素 initgraph(640, 480); // 2. 设置当前绘图颜色为红色 setcolor(RED); // 3. 在窗口中心(320, 240)画一个半径为100的圆 circle(320, 240, 100); // 4. 等待用户按任意键 _getch(); // 5. 关闭图形窗口 closegraph(); return 0; }

编译并运行这段代码。如果一切顺利,你应该会看到一个红色的窗口,中间画着一个红色的圆。恭喜,你的图形编程之路正式开始了!这个简单的流程——初始化、设置、绘图、等待、关闭——是所有EasyX程序的基本骨架。

3. 核心原理拆解:坐标、颜色与绘图函数

在动手画同心圆之前,我们必须把几个核心概念吃透。很多初学者画出来的图形位置不对、颜色奇怪,问题都出在这里。

3.1 坐标系:从数学到屏幕的映射

这是第一个思维转换点。数学中的笛卡尔坐标系,原点(0,0)在中心,y轴向上为正。但在计算机图形学(以及EasyX)的默认视口坐标系中,原点(0,0)在窗口的左上角,x轴向右为正,y轴向下为正

这意味着,窗口中心点的坐标是(width/2, height/2)。在我们640x480的窗口中,中心点就是(320, 240)。理解这一点至关重要,否则你计算出来的圆心坐标会完全错位。

3.2 颜色系统:RGB与预定义颜色

EasyX使用24位真彩色(RGB)模型。每个颜色由红(Red)、绿(Green)、蓝(Blue)三个分量组成,每个分量取值范围0-255。

  • RGB(255, 0, 0)表示纯红色。
  • RGB(0, 255, 0)表示纯绿色。
  • RGB(0, 0, 255)表示纯蓝色。
  • RGB(255, 255, 255)表示白色。
  • RGB(0, 0, 0)表示黑色。

你也可以通过调色板获得更柔和的颜色,例如RGB(135, 206, 235)是天蓝色。为了方便,EasyX预定义了一批常用颜色常量,如RED,GREEN,BLUE,YELLOW,BLACK,WHITE等。使用setcolor()函数设置当前绘图颜色,之后所有的线条、图形边框(非填充)都会使用这个颜色。

3.3 关键绘图函数解析

同心圆项目主要用到以下几个函数:

  • initgraph(int width, int height, int flag = NULL): 初始化图形窗口。flag参数可以控制窗口样式,如是否显示标题栏、是否置顶等,初学者用默认值即可。
  • circle(int x, int y, int radius):画圆。这是核心函数。(x, y)是圆心坐标,radius是半径,单位都是像素。注意,它画的是空心圆,只有边框。
  • setcolor(COLORREF color): 设置当前绘图颜色。
  • setfillcolor(COLORREF color): 设置当前填充颜色。这个要和填充函数配合使用。
  • fillcircle(int x, int y, int radius):画填充圆。用当前填充颜色画一个实心圆。
  • cleardevice(): 清空整个绘图设备(窗口),用当前背景色填充。常用于动画中的每一帧刷新。
  • closegraph(): 关闭图形窗口。

理解circlefillcircle的区别是关键。我们的同心圆,如果只想画轮廓,就循环调用circle;如果想画彩色圆环,就需要交替使用setcolor画边框和setfillcolorfillcircle画填充。

4. 同心圆实现方案:从静态绘制到动态交互

现在,我们进入核心实现环节。我将提供两种逐步递进的实现方案:基础静态版和进阶交互版。

4.1 方案一:基础静态同心圆

这个方案的目标是绘制一组固定数量、固定半径间隔的同心圆。我们将圆心放在窗口中央。

#include <graphics.h> #include <conio.h> int main() { const int CENTER_X = 320; // 圆心X坐标 const int CENTER_Y = 240; // 圆心Y坐标 const int MAX_RADIUS = 200; // 最大半径 const int RADIUS_STEP = 20; // 半径递增步长 const int CIRCLE_COUNT = MAX_RADIUS / RADIUS_STEP; // 计算圆的数量 initgraph(640, 480); // 设置绘图颜色为蓝色 setcolor(BLUE); // 使用循环绘制多个同心圆 for (int r = RADIUS_STEP; r <= MAX_RADIUS; r += RADIUS_STEP) { circle(CENTER_X, CENTER_Y, r); // 每次循环,半径r增加20,圆心不变,从而画出同心圆 } _getch(); closegraph(); return 0; }

代码解读与注意事项:

  1. 使用常量:将圆心坐标、最大半径、步长定义为常量,是好习惯。这提高了代码可读性,也便于后续修改。比如你想把圆画在别处,只需改CENTER_XCENTER_Y
  2. 循环控制for循环从RADIUS_STEP(20)开始,而不是0。因为半径为0的圆是一个点,画出来没有意义。循环条件r <= MAX_RADIUS确保了能画到最大半径的那个圆。
  3. 计算数量CIRCLE_COUNT变量展示了如何根据步长和最大半径计算出圆的数量。虽然本例中没直接使用,但在需要显示数量信息时很有用。

运行这个程序,你会看到一组蓝色的同心圆轮廓。但这看起来有些单调,我们给它加点颜色。

4.2 方案二:彩色填充同心圆环

为了让图形更美观,我们可以让每个圆环的颜色不同。这里引入一个技巧:使用HSL色彩模式或者通过RGB分量计算来生成一系列渐变色。为了简单起见,我们使用一个颜色数组。

#include <graphics.h> #include <conio.h> // 预定义一组颜色 COLORREF Colors[] = { RED, GREEN, BLUE, YELLOW, CYAN, MAGENTA, BROWN, LIGHTGRAY }; int main() { const int CENTER_X = 320; const int CENTER_Y = 240; const int MAX_RADIUS = 200; const int RADIUS_STEP = 25; const int COLOR_COUNT = sizeof(Colors) / sizeof(Colors[0]); // 计算颜色数组长度 initgraph(640, 480); // 设置背景色为白色(清屏) setbkcolor(WHITE); cleardevice(); int colorIndex = 0; for (int r = RADIUS_STEP; r <= MAX_RADIUS; r += RADIUS_STEP) { // 1. 设置填充颜色 setfillcolor(Colors[colorIndex % COLOR_COUNT]); // 取模循环使用颜色 // 2. 画填充圆(实心) fillcircle(CENTER_X, CENTER_Y, r); // 3. 设置边框颜色为黑色,并画空心圆轮廓,使圆环边界清晰 setcolor(BLACK); circle(CENTER_X, CENTER_Y, r); colorIndex++; } _getch(); closegraph(); return 0; }

关键技巧解析:

  1. 清屏与背景色setbkcolor(WHITE)设置背景色为白色,但并不会立即生效。调用cleardevice()后,整个窗口才会被填充为白色背景。这是一个常用组合。
  2. 填充与边框分离:先fillcircle画实心圆,再circle画黑色边框。顺序不能反!如果先画边框,再画填充,填充色会覆盖掉边框。最终效果是每个圆环有一种填充色,并带有清晰的黑色边界。
  3. 颜色循环colorIndex % COLOR_COUNT确保了当圆的数量超过颜色数组长度时,颜色会从头开始循环使用,避免数组越界。

实操心得:fillcircle会填充整个圆形区域。如果你想要的是“圆环”(即只有边缘一圈有色,中间是透明或背景色),目前的简单API无法直接实现。那需要更复杂的操作,比如先画一个大实心圆,再在中间画一个稍小的背景色实心圆“挖空”。这对于初学者是个很好的思考题。

4.3 方案三:交互式动态同心圆

静态图形缺乏生气。我们引入鼠标交互,让圆心跟随鼠标移动,实现动态变化的同心圆。这涉及到EasyX的消息处理机制。

#include <graphics.h> #include <conio.h> #include <stdio.h> int main() { const int MAX_RADIUS = 150; const int RADIUS_STEP = 15; initgraph(640, 480); // 启用批量绘图模式,可以显著减少闪烁 BeginBatchDraw(); int centerX = 320, centerY = 240; // 圆心初始位置 MOUSEMSG m; // 定义鼠标消息结构体 while (true) { // 1. 清空上一帧画面 cleardevice(); // 2. 获取鼠标消息(非阻塞方式) if (MouseHit()) { m = GetMouseMsg(); // 如果收到鼠标移动消息,更新圆心坐标 if (m.uMsg == WM_MOUSEMOVE) { centerX = m.x; centerY = m.y; } // 如果点击鼠标右键,退出循环 if (m.uMsg == WM_RBUTTONDOWN) { break; } } // 3. 根据最新的圆心坐标绘制同心圆 setcolor(BLUE); for (int r = RADIUS_STEP; r <= MAX_RADIUS; r += RADIUS_STEP) { circle(centerX, centerY, r); } // 4. 在圆心处画一个红色小点,便于观察 setcolor(RED); setfillcolor(RED); fillcircle(centerX, centerY, 3); // 5. 刷新显示(批量绘图的提交) FlushBatchDraw(); // 6. 短暂延迟,控制刷新频率,降低CPU占用 Sleep(10); } EndBatchDraw(); closegraph(); return 0; }

这是质的飞跃,详解如下:

  1. 消息循环:图形界面程序的核心是事件驱动。while(true)构成了主循环。MouseHit()检查是否有鼠标消息,GetMouseMsg()获取消息。
  2. WM_MOUSEMOVE:这是Windows定义的消息常量,表示鼠标移动。当鼠标移动时,m.xm.y就存储了当前的鼠标坐标。我们将圆心坐标更新为鼠标坐标。
  3. 动画与双缓冲:直接在一个不断循环中绘图,屏幕会剧烈闪烁。BeginBatchDraw()FlushBatchDraw()EndBatchDraw()是EasyX提供的批量绘图函数,它们实现了双缓冲机制。所有绘图指令先在内存中的一张“画布”上完成,然后通过FlushBatchDraw()一次性更新到屏幕,从而消除闪烁。
  4. 清屏与重绘:每次循环开始先cleardevice(),擦除旧图形,再根据新的圆心坐标画上新图形,从而形成动画。
  5. 退出机制:通过检测WM_RBUTTONDOWN(鼠标右键按下)消息来跳出循环,关闭程序。这是一个简单的交互控制。
  6. Sleep函数Sleep(10)让程序每次循环暂停10毫秒。这有两个作用:一是降低CPU使用率(否则循环会疯狂运行,占用大量资源);二是控制动画帧率,大约100FPS,使运动看起来更平滑。

运行这个程序,移动鼠标,你会看到同心圆组跟着鼠标指针一起移动,圆心处还有一个红色标记点。这已经是一个简单的交互式图形演示了。

5. 性能优化与高级技巧

当圆的数量非常多,或者动画要求很高时,基础绘制可能会遇到性能瓶颈。这里分享几个优化思路和高级技巧。

5.1 减少绘制调用:一次绘制多个图形

circle函数每次调用都会产生一次绘制指令。如果画100个圆,就是100次调用。对于静态图形,我们可以使用BeginBatchDrawEndBatchDraw包裹,但这主要解决闪烁。对于真正需要提升绘制速度的场景,可以考虑“离屏渲染”,即先将所有图形画到一张内存中的图片上,最后一次性贴到屏幕。不过对于同心圆这种简单图形,优化效果不明显,但思路值得了解。

5.2 抗锯齿处理

仔细观察你画的圆,边缘可能会有明显的“锯齿”。这是因为屏幕由像素点组成,画斜线或曲线时,只能用方形的像素去近似。EasyX提供了简单的抗锯齿函数setlinestyle(PS_SOLID | PS_ENDCAP_ROUND, 1)并不能真正消除锯齿。要实现较好的抗锯齿,通常需要更高级的图形库(如GDI+、OpenGL)。在EasyX中,一个取巧的办法是:用多个很细的、半透明的线段来模拟平滑边缘,但这非常复杂。对于初学者项目,可以暂时忽略锯齿问题,或者通过增加显示分辨率来减弱锯齿感。

5.3 封装与模块化:创建Circle类

随着项目复杂,将同心圆逻辑封装成类是好主意。这提高了代码的复用性和可读性。

// Circle.h #pragma once #include <graphics.h> class ConcentricCircles { private: int centerX, centerY; int maxRadius; int radiusStep; int circleCount; COLORREF borderColor; COLORREF* fillColors; // 动态颜色数组 bool isFilled; public: // 构造函数 ConcentricCircles(int cx, int cy, int maxR, int step, COLORREF border = BLACK, bool filled = false); // 析构函数 ~ConcentricCircles(); // 设置填充颜色数组 void setFillColors(COLORREF colors[], int count); // 更新圆心位置 void updateCenter(int x, int y); // 绘制函数 void draw() const; }; // Circle.cpp (部分实现) ConcentricCircles::ConcentricCircles(int cx, int cy, int maxR, int step, COLORREF border, bool filled) : centerX(cx), centerY(cy), maxRadius(maxR), radiusStep(step), borderColor(border), isFilled(filled), fillColors(nullptr), circleCount(maxR / step) { if (radiusStep <= 0) radiusStep = 1; // 防止除零或负步长 } void ConcentricCircles::draw() const { setcolor(borderColor); for (int r = radiusStep; r <= maxRadius; r += radiusStep) { if (isFilled && fillColors != nullptr) { // 计算当前圆环的颜色索引 int index = ((r / radiusStep) - 1) % circleCount; setfillcolor(fillColors[index]); fillcircle(centerX, centerY, r); } circle(centerX, centerY, r); // 始终画边框 } }

在主程序中,你可以这样使用:

ConcentricCircles circles(320, 240, 200, 20, BLUE, true); COLORREF myColors[] = { RED, YELLOW, GREEN }; circles.setFillColors(myColors, 3); circles.draw();

这种封装使得主程序逻辑非常清晰:创建对象、设置属性、调用绘制。如果你想画多组同心圆,或者让不同的同心圆有不同的行为,类的优势就体现出来了。

6. 常见问题与调试技巧实录

即使代码逻辑清晰,实际编写和运行时也难免遇到各种问题。下面是我在多年教学和开发中总结的一些典型坑点及解决方案。

6.1 编译错误:“无法打开源文件graphics.h”

这是最常见的问题,意味着EasyX没有正确安装。

  • 检查安装路径:确保graphics.h文件在VS的包含路径中。对于VS,通常应放在[VS安装目录]\VC\Include\下,graphics.lib放在[VS安装目录]\VC\Lib\下。
  • 项目属性设置:在VS中,右键点击项目 -> “属性” -> “VC++目录”,检查“包含目录”和“库目录”是否包含了上述路径。但通常EasyX安装程序会自动配置。
  • 重新安装:最简单的方法是重新运行EasyX安装程序,并确保关闭了所有VS实例。

6.2 链接错误:“无法解析的外部符号 _initgraph...”

这通常是库文件链接问题。

  • 确保是.cpp文件:C++项目必须使用.cpp后缀源文件。如果误存为.c,编译器会按C语言编译,而EasyX库可能是C++版本,导致链接失败。
  • 检查库名:在项目属性 -> “链接器” -> “输入” -> “附加依赖项”中,查看是否有graphics.lib。同样,EasyX安装程序通常会自动添加。
  • 平台匹配:确保项目平台(如Win32/x64)与安装的EasyX库版本匹配。

6.3 运行时报错或窗口一闪而过

  • 缺少运行库:如果提示“找不到VCRUNTIME140.dll”或类似错误,请安装对应版本的Microsoft Visual C++ Redistributable。
  • 程序提前退出:确保在closegraph()之前有等待语句,如_getch()system(“pause”)或消息循环。否则窗口会立即关闭。
  • 在交互式程序中:如果你用了while循环但没有延迟Sleep,程序可能占用极高CPU,导致系统卡顿。务必添加适当的延迟。

6.4 图形显示异常(位置错乱、颜色不对)

  • 坐标计算错误:反复检查圆心坐标计算。记住原点在左上角。使用outtextxy()函数在屏幕上打印出变量的值进行调试,例如outtextxy(10, 10, std::to_string(centerX).c_str())
  • 颜色值溢出RGB(r, g, b)每个参数必须在0-255之间,超过会被截断或导致意外颜色。
  • 绘图顺序问题:后绘制的图形会覆盖先绘制的。确保背景、填充、边框的绘制顺序符合你的预期。

6.5 动画闪烁严重

  • 未使用双缓冲:这是导致闪烁的主要原因。务必在绘制动画前调用BeginBatchDraw(),在每帧绘制完成后调用FlushBatchDraw(),程序结束时调用EndBatchDraw()
  • 清屏位置不对cleardevice()应该在一帧开始的时候调用,如果放在绘制中间或结尾,会导致部分图形被清除。

6.6 鼠标/键盘交互无响应

  • 消息类型判断错误m.uMsg是消息类型,WM_MOUSEMOVE是移动,WM_LBUTTONDOWN是左键按下,WM_KEYDOWN是按键按下。确认你判断的是正确的消息类型。
  • 消息获取方式:在动画循环中,应使用非阻塞的MouseHit()GetMouseMsg()组合。如果在循环外用GetMouseMsg(),它会阻塞程序直到有消息。
  • 焦点问题:确保图形窗口是当前活动窗口(具有焦点),否则可能接收不到输入消息。

把这些问题的解决方案整理成表格,方便快速查阅:

问题现象可能原因解决方案
编译报错“graphics.h找不到”EasyX未安装或路径错误重新运行安装程序,检查VS包含目录
链接错误“无法解析外部符号”库未链接或源文件后缀错误确保是.cpp文件,检查项目附加依赖项
窗口一闪而过程序缺少等待语句closegraph()前加_getch()或使用消息循环
图形位置不对坐标系理解错误确认原点在左上角,y轴向下为正
动画严重闪烁未使用双缓冲BeginBatchDraw()FlushBatchDraw()包裹绘制代码
鼠标移动不流畅循环太快,未处理所有消息使用MouseHit()判断,适当Sleep降低循环频率
CPU占用率100%循环内无延迟在循环内添加Sleep(1)等微小延迟
填充色覆盖边框绘图顺序错误先画填充(fillcircle),再画边框(circle)

调试图形程序,一个非常实用的方法是“可视化调试”。除了打印变量,你还可以:

  • 画辅助线:用line函数画出坐标轴,帮助定位。
  • 临时标记:在关键位置用putpixel画一个点,或者用outtextxy输出坐标值。
  • 分步执行:复杂动画可以先注释掉部分代码,让图形逐帧出现,看是哪一步出了问题。

画同心圆这个项目,就像学习游泳时在浅水区练习蹬腿。它动作单一,但涵盖了换气、协调、发力等所有基本功。当你熟练掌握了窗口创建、坐标计算、循环绘图、事件处理这些基础后,再去学习更复杂的图形变换、精灵动画、碰撞检测,就会发现底层逻辑是相通的。图形编程最大的乐趣在于所见即所得,你的每一行代码都能立刻转化为屏幕上的视觉反馈。从今天这个简单的同心圆开始,试着改变颜色序列,让圆环间隔变大变小,或者尝试让圆环半径随时间正弦变化,产生脉动的效果。这些小小的改动,都是你探索更广阔图形世界的第一步。

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

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

立即咨询