看到这个标题,可能有人觉得奇怪:画个苹果而已,找张图片一贴不就行了,为什么要用 C++ 写代码一笔一笔地画?说实话,我刚接触图形编程那会儿也这么想过。但当你真的用精灵库在窗口里从零拼出一颗苹果时,感觉完全不一样——你会突然搞懂“一个图形到底是怎么被计算机描述出来的”,而不是只会调用贴图。这篇文章就专门讲讲,怎么用 C++ 里的精灵库(像 EasyX、EGE、raylib 这一类),一步步画出一颗能看的红苹果。整个教程不需要任何美术基础,只要求你会最基本的 C++ 语法、会建一个控制台项目就够了。最终你会拿到一份完整可编译的代码,也能理解为什么每一笔要画在那里。
1. 先说清楚:这里的“精灵库”到底是什么
1.1 精灵库在 C++ 图形教学中的角色
可能很多人学 C++ 学到指针、链表就停了,觉得编程就是黑底白字。但实际上,图形库是让编程“看得见”最快的方式。所谓精灵库,简单说就是一套封装了窗口、图像、事件、坐标体系的管理库。你告诉它“在 (320, 260) 画一个半径 90 的红色圆”,它就把圆画在屏幕上;你说“按一下按键就关窗口”,它帮你处理键盘消息。它存在的意义,就是把你从底层窗口 API 里解放出来,让你专注于“画什么”,而不是“怎么让窗口蹦出来”。
在中文 C++ 学习圈里,这类库常被统称为精灵库或图形库,最常见的是 EasyX,其次有 EGE,再往上还有 SDL、SFML、raylib 这些跨平台方案。它们的核心概念是一致的:你有一个画面缓冲区,通过调用绘图函数往里填像素,然后整帧显示出来。EasyX 之所以适合当入门工具,是因为它安装几乎零配置,函数名一看就懂,solidcircle 就是实心圆,fillrectangle 就是填充矩形,中文资料也最多。这篇文章的代码就用 EasyX 写,因为它的函数形态最贴近“画积木”的思维方式。
1.2 为什么拿苹果当第一个图形练习
很多人问,画个苹果有什么好学的?恰恰因为苹果不是一个规则的几何体,它才特别适合当图形编程的入门题。你看一个真正的苹果:果身不是标准圆,而是上窄下宽的曲线;顶部有个小凹陷;果蒂歪歪扭扭伸出来;叶子带一点弧度;果皮还有高光。这些东西拆开来看,全部可以用基础图元模拟:圆形、椭圆、矩形、线条。把一个不规则物体拆解成规则图元的组合,这正是所有图形开发的基本功。
说白了,你在屏幕上画的每一颗像素,最终都是靠坐标和颜色算出来的。苹果作为练习对象,难度刚刚好——比画一个纯圆有挑战,比画一个人物简单得多。它能一次性让你练到四个最重要的技能:坐标系规划、几何体组合、绘制顺序控制、颜色感知。这四个技能后面画任何东西都用得上。所以我建议你别跳过,哪怕最后觉得画得不好看,这个过程本身已经把图形编程的路子走了一遍。
2. 画苹果前先把坐标系和窗口底子打好
2.1 最小环境准备:编译器 + 图形库
在写任何绘图代码之前,先花两分钟把环境弄好。这里我默认你用的是 Windows 环境:
- 先装 Visual Studio 2022 社区版,免费的,安装时勾选“使用 C++ 的桌面开发”工作负载。
- 打开 VS,创建一个“空项目”,项目名随意,比如 RedApple。创建好之后,在“源文件”上右键添加一个新项,选 C++ 文件(.cpp)。
- 去 EasyX 官网下载对应 VS 版本的安装包。装的时候确认 VS 没有开着,装完再打开项目。
- 在 .cpp 文件里写
#include <graphics.h>,如果能正常编译,说明库已经接上了。
有两点值得提前说。第一,老教程经常让你在项目属性里把字符集改成“使用多字节字符集”,因为早期 EasyX 和 VS 默认的 Unicode 字符集有冲突;现在新版 EasyX 已经兼容 Unicode,一般不用改。如果你编译时遇到argument of type "const char*" is incompatible with parameter of type "LPCWSTR"这种报错,再去 属性 -> 高级 -> 字符集 里改成“使用多字节字符集”也不迟。第二,如果你用的不是 VS,而是 VSCode,那也可以配,只是要在 json 文件里把 EasyX 的 include 和 lib 路径手动指给编译器。第一次配置会折腾一点,但原理一样,不复杂。
2.2 窗口、坐标系和“画布思维”
装好环境后,第一行代码永远是initgraph(640, 480)。这行意思是:创建一个宽 640 像素、高 480 像素的绘图窗口。你可以把它想成一块画布,所有图形都会显示在画布上。
接下来必须建立坐标系的概念。图形库的坐标原点是画布左上角,也就是 (0, 0)。x 轴往右是正方向,y 轴往下是正方向。记住:y 向下增大,这跟数学课上的 y 轴正好相反,是新手最容易犯迷糊的地方。比如circle(320, 260, 90)的意思是:圆心在 x=320、y=260 这个位置,半径 90。你往窗口右下角画东西,坐标值会变大;往左上角画,坐标值会变小。这个坐标体系和 Excel 表格的“第几行第几列”有点像,只不过图形库把每个像素都当成一格,坐标精度更高。
绘图窗口还有一个隐藏的图层概念:后画的图形会盖住先画的图形。这跟 Photoshop 图层一个道理,只不过这里没有图层面板,全靠代码顺序控制。所以你要在心里始终有一张“先后顺序表”:先画背景,再画主体,再画细节,最后画高光。顺序反了,画面就毁了。
2.3 开工前先把苹果的位置规划好
大多数新手画出来的苹果不好看,问题不在代码,而在动手前没有做坐标规划。我的习惯是先在纸上画一个简单的草图,标注每个元素的中心点在哪、大小是多少,再写代码。
下面这张表是我在画这颗苹果之前做的规划,画布大小是 640 x 480,苹果主体中心放在画面中央偏下一点,这样上方能留出空间给果蒂和叶子。
| 元素 | 坐标/参数 | 说明 |
|---|---|---|
| 主圆 | 圆心 (320, 260),半径 90 | 苹果的中轴主体 |
| 左侧副圆 | 圆心 (270, 285),半径 55 | 让苹果左侧鼓出去 |
| 右侧副圆 | 圆心 (370, 285),半径 55 | 让苹果右侧鼓出去 |
| 底部扁椭圆 | 包围盒 (230, 240) 到 (410, 370) | 垫出苹果下腹的圆润感 |
| 顶部凹陷 | 圆心 (320, 185),半径 22 | 用背景色盖出凹口 |
| 果蒂 | 矩形左上 (317, 120),右下 (323, 185) | 棕色小柱 |
| 叶子 | 椭圆包围盒 (290, 100) 到 (390, 150) | 横向椭圆 |
| 高光 | 椭圆包围盒 (280, 200) 到 (305, 245) | 左上方的浅色竖条 |
这张表不是拍脑袋定的,它是从苹果的视觉比例推出来的。主圆决定了苹果的大小,左右副圆把“腰”加宽,让轮廓不再是一个正圆。顶部凹陷放在主圆的上边缘附近,因为真实苹果的蒂窝就在那个位置。高光放左上角是因为默认光源在左上方,光照到圆润果皮上会被拉伸成纵向长条,这是后面做质感的重点。你之后想改苹果形状,直接调表里的数字就行,这叫参数化思维。
3. 从果身到高光:苹果的每一块“积木”怎么画
3.1 果身:三个圆拼轮廓的原理
你可能会想,苹果不是一个椭圆吗?为什么不用solidellipse一次性画出来?因为真实苹果的轮廓不是规则椭圆,它的上半部分稍微收拢,下半部分比较圆润,两侧的“肩”有点微微鼓起。用一个椭圆画,出来的东西像橄榄球,不像苹果。
所以我的做法是:一个主圆定大小,两个副圆在腰部两侧鼓出去,再用一个扁椭圆垫底部。圆与圆之间重叠的部分,因为填充颜色完全一样,看不出任何接缝,视觉上会融合成一个不规则的轮廓。这是图形编程里很常用的“形状合并”思路——复杂轮廓不是一笔画的,而是由多个简单形状堆出来的。
// 果身部分 setfillcolor(RGB(220, 30, 30)); // 苹果红 setlinecolor(RGB(150, 20, 20)); // 深红描边 setlinestyle(PS_SOLID, 3); // 实线,线宽 3 solidcircle(320, 260, 90); // 中间主圆 solidcircle(270, 285, 55); // 左侧副圆,让左腰鼓出去 solidcircle(370, 285, 55); // 右侧副圆,让右腰鼓出去 solidellipse(230, 240, 410, 370); // 底部扁椭圆,垫出圆润下腹你可能要问,三个圆叠在一起,最外圈的描边会不会在中间留下乱七八糟的线?不会。因为填充是后于描边执行的,填充颜色会覆盖掉圆与圆重叠处的描边线。而且我们给所有圆用了同样的描边颜色,即使边缘露出来一点也是协调的。这里唯一要注意的是:先设置颜色再画形状,顺序不要反,否则画出来的圆还是上一次的颜色。
3.2 顶部凹陷和底部:遮罩法
真实苹果的顶部有一个凹进去的小窝,果蒂从窝里伸出来。这个凹陷怎么画?大多数图形库没有“橡皮擦”功能,你不可能把已经画好的红色区域抠掉一块。正确的做法是:用背景色——也就是白色——在这个位置画一个圆,把红色盖掉。这就是初学者最容易想不明白的地方:想制造一个洞,不是真的挖洞,而是用背景色把那个区域“盖”出一个洞的形状。
// 顶部凹陷 setfillcolor(WHITE); solidcircle(320, 185, 22); // 白色圆盖住顶部边缘这个白色圆的位置很有讲究,它必须压在主圆上边缘上。主圆圆心 (320, 260) 半径 90,所以它的上边缘到达 y=170;白色圆的圆心 (320, 185) 半径 22,范围从 y=163 到 y=207,正好骑在主圆的边缘上。这样看上去,苹果顶部就像被咬掉了一小口,配合后面画的果蒂,凹陷感就出来了。
同样的思路也可以用在苹果底部。如果你想在底部中央做一个更明显的“脐”,可以在苹果下缘画一个比果身颜色更深一点的小弧线或小椭圆,模拟果脐的阴影。在我的代码里没有单独画这个,因为扁椭圆垫完后底部已经比较圆润了,再画可能画蛇添足。你可以在自己的版本里试试往底部放一个深红色小椭圆,看看效果。
3.3 果蒂、叶子和高光的细节处理
果蒂很简单,就是一条细长的棕色矩形。宽度只有 6 像素左右,长度 65 像素左右,颜色用深棕。别小看这 6 像素宽的小柱,它是连接地面和果身的关键,宽度太宽会显得像一根木棍,太窄又看不清。我用的是:
// 果蒂 setfillcolor(RGB(139, 90, 43)); solidrectangle(317, 120, 323, 185); // 一条窄矩形,从凹陷里伸出来叶子用的是一个横向椭圆。真实叶子是有弧度的,不过初版练习用椭圆已经足够,视觉上能认出是叶子就行。我还在叶子中间加了一条对角线,模拟叶脉,让叶子不那么呆板。这里又体现了一次“后画覆盖先画”的原则:叶子必须在果蒂之后画,这样果蒂的顶部会被叶子根部盖住,看起来像从叶子下面长出来。
// 叶子 setfillcolor(RGB(62, 160, 62)); setlinecolor(RGB(30, 100, 30)); solidellipse(290, 100, 390, 150); // 横向叶子 line(295, 140, 385, 112); // 简单叶脉最后是高光。很多人画高光喜欢画一个白色正圆,这是错误的。真实苹果表面的高光因为果皮是弧面,照射光会被拉成纵向的细长条,而且在最高点附近。所以我用的是一个浅粉白色的竖椭圆,放在苹果左上侧:
// 高光 setfillcolor(RGB(255, 200, 200)); // 淡粉白,比白色更柔和 setlinecolor(RGB(255, 200, 200)); solidellipse(280, 200, 305, 245); // 竖长椭圆,模拟弧面反光高光的位置需要反复微调:太靠中间会像贴了块补丁,太靠边缘会超出果身边界;大小占整个苹果的五分之一左右比较合适。它必须最后画,因为它是覆盖在果皮之上的“光线效果”,一旦被后面加的东西盖住,光泽就消失了。
4. 完整代码跑起来:能直接编译的苹果
4.1 完整代码清单
把上面所有积木按正确顺序拼起来,就是一份完整可运行的代码。你直接把下面的内容复制到 .cpp 文件里编译运行即可:
// 文件名:red_apple.cpp // 环境:Visual Studio 2022 + EasyX #include <graphics.h> #include <conio.h> int main() { // 初始化窗口:640 x 480 initgraph(640, 480); // 白底 setbkcolor(WHITE); cleardevice(); // ---------- 第一步:果身 ---------- setfillcolor(RGB(220, 30, 30)); setlinecolor(RGB(150, 20, 20)); setlinestyle(PS_SOLID, 3); solidcircle(320, 260, 90); // 中间主圆 solidcircle(270, 285, 55); // 左侧副圆 solidcircle(370, 285, 55); // 右侧副圆 solidellipse(230, 240, 410, 370); // 底部扁椭圆 // ---------- 第二步:顶部凹陷 ---------- setfillcolor(WHITE); solidcircle(320, 185, 22); // ---------- 第三步:果蒂 ---------- setfillcolor(RGB(139, 90, 43)); solidrectangle(317, 120, 323, 185); // ---------- 第四步:叶子 ---------- setfillcolor(RGB(62, 160, 62)); setlinecolor(RGB(30, 100, 30)); solidellipse(290, 100, 390, 150); line(295, 140, 385, 112); // 叶脉 // ---------- 第五步:高光 ---------- setfillcolor(RGB(255, 200, 200)); setlinecolor(RGB(255, 200, 200)); solidellipse(280, 200, 305, 245); // 按键关闭窗口 _getch(); closegraph(); return 0; }4.2 逐段拆解:每一行在干嘛
initgraph(640, 480)是整个绘图的入口,它创建窗口并准备画布,之后所有图形操作都在这块画布上完成。setbkcolor(WHITE)设置背景色,cleardevice()用背景色清空整块画布。如果你不设白底,EasyX 默认背景是黑色,苹果的红和黑色也能搭配,但高光和凹陷的“遮罩法”就失效了,因为遮罩圆必须用和背景一样的颜色才能隐形。
RGB(220, 30, 30)是颜色构造,三个参数分别是红、绿、蓝的强度,范围 0 到 255。220, 30, 30 是偏亮的大红色;RGB(150, 20, 20)是深红,用来描边。setlinestyle(PS_SOLID, 3)设置描边线为实线、宽度 3 像素。三个果身圆的填充色一致,重叠区域自然融为一体。
solidcircle(x, y, r)的前两个参数是圆心横纵坐标,第三个是半径。solidellipse(left, top, right, bottom)和solidrectangle(left, top, right, bottom)都是包围盒参数——你需要给出的是图形的左边界、上边界、右边界、下边界,而不是“中心 + 宽高”。这是新手最容易踩的坑,我在后面专门展开说。
_getch()的作用是等待用户按任意键,然后才执行closegraph()。没有这一行,程序会瞬间执行完并关闭窗口,你什么也看不到。VSCode 里如果用getch()可能会遇到标准库老函数被禁用的警告,VS 2022 推荐统一用_getch(),后面带下划线是微软标准库的写法。
4.3 编译运行要点
在 VS 里,按 F5(调试运行)或 Ctrl+F5(直接运行)即可。如果编译失败,报错集中在几个典型位置:第一,找不到graphics.h,说明 EasyX 没装好,或者装完没重启 VS;第二,报LNK2019链接错误,说明你没在 EasyX 官网下载匹配 VS 版本的安装包,或者项目架构是 x64 而装的库只支持 x86,去 属性 -> 平台 里切一下架构即可;第三,中文注释乱码或编译警告,检查源文件编码,用 UTF-8 with BOM 最稳妥,或者干脆把注释写成英文,不影响程序。
还有一个很容易被忽略的点:Ctrl+F5 运行时,除了绘图窗口,还会弹出一个黑色控制台窗口。这是正常的,不是 bug。如果你不想要控制台窗口,可以去项目属性里把“配置类型”改成“Windows 应用程序”,但新手阶段建议保留,因为控制台能输出调试信息,方便你随时用 printf 查看坐标和状态。
5. 画完不算完:美化、动画和交互的延伸玩法
5.1 从静态到动态:让苹果长大
静态苹果画完之后,最自然的下一步就是让它动起来。最简单的动画叫“生长动画”:苹果从一个小圆点慢慢放大到完整形状,模拟生长的过程。实现方式的核心是把绘制代码改造成一个可接收参数的函数,比如drawApple(int cx, int cy, double scale),苹果的每个坐标和半径都写成cx + 偏移量 * scale的形式,然后循环调用:
for (double scale = 0.2; scale <= 1.0; scale += 0.02) { cleardevice(); // 清空画布 drawApple(320, 260, scale); // 按当前比例绘制 Sleep(20); // 暂停 20 毫秒,形成动画 }这里Sleep(20)是控制帧率的关键,数值越小动画越流畅,太大会卡顿。每次循环先cleardevice()清空旧画面再画新一帧,这叫做“逐帧重绘”。动画的原理就是用一系列静态画面快速切换,欺骗人眼。这个生长效果看起来很有成就感,但它背后练的就是对坐标参数化的控制——也是所有小游戏开发的基础。
5.2 交互:跟着鼠标走的苹果和散落的小苹果
动画之外,交互是另一个容易上手的扩展方向。EasyX 提供鼠标消息读取,核心是GetMouseMsg()。你可以做一个“苹果跟着鼠标走”的效果:
MOUSEMSG msg; while (true) { msg = GetMouseMsg(); // 读取鼠标消息,比如移动、按下 cleardevice(); drawApple(msg.x, msg.y, 1.0); Sleep(16); }msg.x和msg.y是鼠标当前坐标,把它当作苹果的中心点传入绘制函数,苹果就跟鼠标跑了。你还可以监听左键按下事件,在随机位置画缩小版苹果:用rand()生成一个 100 到 540 的随机 x、100 到 380 的随机 y,再添加一个 0.3 到 0.8 的随机缩放比例,就能得到“一地小苹果”的效果。
这些玩法不需要引入任何新库,只复用你已经会的基本图形函数。但从“画一个静态苹果”到“让苹果动起来”“让苹果响应鼠标”,这中间跨越的正是图形编程里非常重要的两个概念:渲染循环和事件处理。如果你后续想做小游戏,比如接苹果、砸苹果、切苹果,起点都是这一套东西。
6. 我在写这个苹果时踩过的坑
6.1 坐标系搞反,苹果飞出了窗口
我第一次写这个练习时,犯的最蠢错误是把solidellipse(280, 200, 305, 245)写成了solidellipse(280, 200, 25, 45)。我心想前两个是坐标,后两个是宽和高,结果窗口里出现了一大块奇怪的色块。原因就是 Ellipse 系列函数的四个参数是包围盒的“左、上、右、下”,不是“圆心 + 宽高”。
正确的理解方式:solidellipse(a, b, c, d)画出的椭圆,左边界是 a,上边界是 b,右边界是 c,下边界是 d。所以宽度 = c - a,高度 = d - b。比如solidellipse(230, 240, 410, 370)的宽度就是 410 - 230 = 180,高度是 370 - 240 = 130,是一个扁椭圆。如果你想要一个中心在 (320, 260)、宽 120、高 80 的椭圆,你得算出四个边界:左 260、上 220、右 380、下 300,然后写solidellipse(260, 220, 380, 300),而不是直接填 120 和 80。这个坑几乎所有用 EasyX 的人都会踩一次,踩过之后你就记住了。
6.2 窗口一闪而过,以及按键函数的选择
另一个几乎人人都会遇到的问题是:运行程序,窗口一闪就没了。原因就是没有在closegraph()之前等待输入。程序执行完closegraph(),窗口立刻关闭,你连苹果长什么样都来不及看。
解决方法是加一行按键等待。在 VS 2022 里,标准库建议用_getch(),老式的getch()可能触发 C4996 错误,提示你用带下划线的安全版本。这行代码必须放在closegraph()之前,最好紧挨着它。如果你的程序里写了getch但编译报错,把它改成_getch()就行。还有一种情况:你明明写了_getch(),窗口还是闪退,那大概率是程序早早就崩溃了,比如某个坐标计算非法导致图形库抛异常,可以按 Ctrl+F5 运行让控制台窗口保留,看有没有报错信息。
6.3 渲染顺序:后画的永远盖住先画的
最后这个坑不是报错,而是画出来的东西“哪里不对”。比如有人先画高光,再画果身,结果高光被果身盖住了,苹果表面什么光泽都没有;有人先画叶子再画果蒂,结果叶子压在果蒂上面,看起来像叶子把果蒂劈成了两半。
图形库没有智能图层,只有一套规则:后画的图形覆盖先画的图形。所以绘制顺序必须从底层到顶层想清楚。这颗苹果的正确顺序是:果身(底层)→ 顶部凹陷(遮罩)→ 果蒂 → 叶子 → 高光(顶层)。如果以后你想加阴影,阴影必须先于果身画,否则会被果身盖住。这个“后画覆盖先画”的思维,比任何具体函数都重要,它适用于所有图形环境,包括网页 Canvas、Python Turtle,甚至游戏引擎的精灵排序。
最后说句实在的:如果你照这份教程画出来的“苹果”看起来像个红色土豆,别怀疑是自己不行。我第一次写这个练习的时候,画出来的东西比土豆还丑——原因不是代码写得不对,而是我没在动手前把坐标规划清楚:圆心偏了,高光盖住了果蒂,叶子又挡了果身。后来我养成了一个习惯:不管画什么,先在纸上把坐标框出来,标出每个元素的中心点和边界,再写代码。从那之后图形练习基本一次成型。你可以把这个习惯记下来,画苹果用得上,以后画人物、画地图、做小游戏都用得上。