ARTICLE DETAIL

资讯详情

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

C++绘图入门:从绘制到修饰,掌握EasyX图像处理完整流程

C++绘图入门:从绘制到修饰,掌握EasyX图像处理完整流程 把同样一套绘图命令分别交给两个人结果往往完全不同一个人只能画出黑边白底的圆和方块另一个人却能画出有层次的小场景。很多人会把这种差距归因于“美术功底”但在编程入门阶段更常见的原因是只学会了“绘制命令”没有建立“修饰思维”。本文要讲的“绘制和修饰图像”核心是把绘图过程拆成两件事用几何函数把图形放到窗口里再用颜色、线型、填充、文字这些样式手段让画面变得可读、协调、有层次。我会以 Windows 下最容易上手的 C 图形库 EasyX 为例从环境准备开始逐步做到能独立绘制一张完整的静态小场景再延伸到动画中更容易踩坑的闪烁问题。如果你正在做 C/C 课程设计、学习游戏开发或者准备给项目写简易可视化界面这一章的思路可以直接用上。读完你会明白学习绘图库背 API 只是最浅的一层真正重要的是掌握“从画布到画面”的完整流程。1. 绘制和修饰图像到底在解决什么问题在没有图形库的年代初学者想在程序中显示图形通常只能用控制台输出字符。比如画一条直线要自己算好坐标用空格和“*”在屏幕上拼出来。这种方式有两个明显的问题一是字符间距不均匀横线和竖线看起来粗细完全不同二是没法填充颜色更谈不上修饰。图形库的出现把“画点、画线、画面”从数学计算变成了系统调用这是“绘制”阶段的解放。但引入图形库不等于问题消失。无论是 EasyX、SDL 还是 Qt 绘图单纯调用绘制函数画面往往非常“素”。默认情况下图形库使用黑色边框、纯白背景、单一字体画出来的东西像是工程草图而不是可展示的图像。于是“修饰图像”就成了从“能显示”到“能看”的关键一步。“修饰”在工作量上不一定比“绘制”少。在一个简单场景中绘制过程可能只需要十几个几何函数调用但如何搭配颜色、控制线宽、选择填充方式、处理文字样式却需要系统设计。这个章节真正要解决的问题是帮助初学者建立一套“绘制与修饰两步走”的思维模型第一步确定画什么矩形、圆形、多边形、文字。第二步确定画成什么样边框颜色、线宽、填充颜色、文字大小和字体。第三步确定何时显示静态画面直接刷新动态画面使用双缓冲。把这个流程想清楚即使换一个图形库你也能快速迁移知识。Graphics 只是接口背后都要处理“几何”、“样式”、“刷新”三个问题。2. 理解绘图窗口、坐标与颜色体系2.1 窗口坐标系的起点在左上角学习绘图时第一个容易出错的点就是坐标系。数学课上的坐标系原点通常在左下角x 轴向右y 轴向上而大多数图形库的窗口坐标系原点在左上角x 轴向右y 轴向下。这意味着你想画一个从左上角往右下角延伸的三角形坐标并不是随手写出来的而是需要经过一次“翻转”思考。例如窗口宽度是 800高度是 600那么右下角坐标就是 (799, 599)。如果画圆时传入圆心 (400, 300)图形会出现在窗口正中间。这个坐标系影响所有图形的位置判断建议一开始就在纸上画出窗口的四条边标好原点方向再开始写代码。EasyX 中使用initgraph(800, 600)创建绘图窗口后默认背景是黑色。通常建议先调用setbkcolor设置背景色再调用cleardevice()刷新一次让背景色真正生效。很多初学者不理解为什么要cleardevice()其实setbkcolor只是修改了“背景色状态”并不会立刻把画布刷成新颜色调用清屏函数才会完成这个动作。2.2 图像最终由像素组成图形库提供了画线、画圆、画矩形等高级函数但这些函数内部最终都落到“改变像素颜色值”这一件事上。窗口可以理解成一张二维像素表每个像素保存一个 RGB 颜色值。绘制一条线相当于沿着直线路径修改多个像素绘制一个实心圆相当于把圆内所有像素填成指定颜色。理解这一点对排查问题很有帮助。比如为什么图形边缘有锯齿因为直线和圆在像素网格上不可能绝对平滑只能用近似算法填充路径像素。为什么动画会闪烁因为每次绘制都要清空一整块画布再重新计算并写入大量像素如果清屏和绘制之间出现可见时间差人眼就会感觉到闪烁。这两个问题都来自“像素级别”的底层行为。RGB 颜色体系也值得说清楚。计算机屏幕使用红、绿、蓝三种基本色混合出几乎所有颜色每种颜色分量通常取值 0 到 255。RGB(255, 0, 0) 是红色RGB(0, 255, 0) 是绿色RGB(0, 0, 255) 是蓝色三个分量相同则是灰色。EasyX 中常见的WHITE、BLACK、RED等宏其实也是通过 RGB 组合定义的。想调制出柔和颜色直接使用RGB(r, g, b)是最直观的方式。2.3 绘制命令与修饰命令的关系EasyX 中大量 API 可以分成两类一类负责几何绘制如line、circle、rectangle、fillpolygon另一类负责样式控制如setlinecolor、setfillcolor、setlinestyle、settextstyle。样式控制函数类似“状态设置”。调用setlinecolor(RGB(255, 0, 0))后后续所有涉及边框的绘制函数都会使用红色直到再次修改线条颜色为止。这种“先设置状态再执行绘制”的模型容易让人忽略顺序。如果把circle写在setfillcolor前面画出来的圆依然是旧填充色。因此一个规范绘图的习惯是每画一个独立元素前先明确当前需要的边框色、填充色、线宽再调用几何绘制函数。通过这个概念划分“修饰图像”就不是一个模糊说法而是一系列具体的状态设置。绘制命令解决“哪里有图形”修饰命令解决“图形长什么样”两者交替执行最终组合成完整画面。3. 环境准备与前置条件3.1 开发环境选择本文示例使用 C 与 EasyX推荐在 Windows 平台使用 Visual Studio 社区版。EasyX 是针对 Windows 的图形库不支持 Linux 或 macOS这一点需要提前确认。如果你使用 Code::Blocks 等其他开发环境配置过程会稍有差异但代码思路一致。版本方面不必追求最新。EasyX 安装包一直在更新不同版本的 API 高度兼容。本文代码涉及的initgraph、line、circle、fillpolygon、BeginBatchDraw等函数在近几年的 EasyX 版本中都能正常使用。关键是确保你的开发环境使用的是 32 位或 64 位对应的图形库配置。3.2 安装 EasyX 并建立工程EasyX 安装过程比较简单从官网下载安装包关闭 Visual Studio 后运行安装程序会自动检测电脑上的 Visual Studio 版本选择对应版本完成安装。重新打开 Visual Studio 后新建一个 C 控制台应用就可以通过#include graphics.h使用 EasyX 函数。有两点需要特别注意。第一EasyX 只支持 Windows第二新建项目时建议选择“控制台应用程序”因为控制台窗口可以用于输出调试信息图形窗口则由initgraph单独创建。EasyX 安装完成后头文件和库文件一般会自动配置到开发环境如果你使用的是绿色版或非标准安装才需要手动在项目属性中配置 include 目录和 lib 目录。3.3 验证图形窗口能否正常打开完成环境安装后不要急着写复杂代码。先运行下面这个最小示例#include graphics.h #include conio.h int main() { initgraph(800, 600); setbkcolor(RGB(255, 255, 255)); cleardevice(); circle(400, 300, 100); _getch(); closegraph(); return 0; }这段代码的作用是创建一个 800 乘 600 的白色窗口以 (400, 300) 为圆心画一个半径为 100 的圆然后等待用户按任意键关闭窗口。如果运行后能看到白色窗口和黑色圆说明 EasyX 环境配置成功。这里之所以要调用_getch()是为了让程序执行到closegraph()之前暂停。如果没有这一行程序会瞬间创建窗口又立刻关闭什么都看不清。接下来所有绘图示例都会保留这个“暂停等待按键”的结构便于观察结果。4. 基础绘制点、线、矩形、圆与多边形4.1 点与线是最基本的绘制单元绘制是修饰的前提。EasyX 中点的绘制函数是putpixel它直接把某个坐标对应的像素改成指定颜色。线的函数是line接收起点和终点坐标。虽然实际项目中很少逐个画点但了解点的存在有助于理解图像本质。#include graphics.h #include conio.h int main() { initgraph(640, 480); setbkcolor(RGB(255, 255, 255)); cleardevice(); // 用红色逐个画点形成一条虚线效果 setcolor(RGB(255, 0, 0)); for (int x 50; x 300; x 10) { putpixel(x, 100, RGB(255, 0, 0)); } // 画一条实线 setlinecolor(RGB(0, 0, 255)); line(50, 150, 300, 150); _getch(); closegraph(); return 0; }注意这里用了setcolor和setlinecolor两个函数。setcolor设置的是文本和点的颜色setlinecolor设置的是线条和图形边框的颜色。两者的职责在不同图形库中不完全一样使用时容易混淆需要对照文档确认。4.2 矩形、圆与椭圆的使用场景矩形和圆是界面中最常见的几何元素常用于按钮背景、装饰图案、图形界面的基础组件。EasyX 中rectangle接收左上角和右下角坐标而不是“起点、宽度、高度”这个设计经常让初学者画错位置。rectangle(100, 50, 300, 200);这段代码表示左上角为 (100, 50)右下角为 (300, 200) 的矩形。只要记住“不接收宽高接收对角线坐标”基本不会画错。圆和椭圆的参数则不同。circle接收圆心坐标和半径比较直观ellipse接收外接矩形的左上角和右下角需要适应。使用fillrectangle、fillcircle、fillellipse时会自动使用当前边框色和填充色绘制带边框的实心图形。如果你只想要填充而不想要边框EasyX 还提供了solidrectangle、solidcircle等函数它们不使用边框色只填充内部。这一组函数在画纯色背景时非常合适。4.3 多边形适合描述不规则形状多边形能表达三角形、梯形、云朵轮廓等复杂形状。EasyX 中使用fillpolygon需要传入一个 POINT 数组以及顶点数量。POINT mountains[] { {0, 400}, {200, 180}, {400, 400} }; setfillcolor(RGB(110, 120, 100)); fillpolygon(mountains, 3);这里数组中的三个点表示三角形函数会自动把首尾连接起来并填充。和圆、矩形相比多边形强调的是顶点的顺序。如果顶点顺序混乱会出现交叉线条画面表现会很奇怪。绘制复杂场景时建议先把所有顶点写出来标好坐标方向再填入数组。5. 修饰图像颜色、线型、填充与文字5.1 用颜色建立画面层次在图形界面中颜色不仅是装饰也是信息组织方式。想做出清晰的画面不应该随机使用高饱和颜色而要按照“背景、主体、高亮”的思路来分配。比如一个简单场景中天空使用较浅的蓝色草地使用中绿色主体建筑使用暖色太阳使用饱和度较高的黄色。这样画面不会因为颜色过多而凌乱。使用 RGB 调色时可以记住几个基本公式相同比例的红绿蓝会得到灰色增加红色分量会让颜色偏暖增加蓝色分量会让颜色偏冷。EasyX 中设置填充色和边框色的函数分别是setfillcolor(RGB(255, 255, 0)); // 黄色填充 setlinecolor(RGB(0, 0, 255)); // 蓝色边框之后调用fillcircle时圆内部是黄色边框是蓝色。如果你希望圆形看起来没有边框需要把边框色设置成和填充色相同或者使用solidcircle。5.2 线型与线宽决定“精致感”线条是最容易被忽视的修饰对象。默认情况下EasyX 使用 1 像素宽的实线在窗口中看起来非常细。想让图形更有结构感可以用setlinestyle调整线宽和线型setlinestyle(PS_SOLID, 3);PS_SOLID代表实线数字 3 代表线宽为 3 像素。EasyX 还支持PS_DASH、PS_DOT等线型适合表达辅助线或虚线。线宽并不是越大越好。线条太粗会让图形轮廓发闷太细则容易显得粗糙。通常边框线宽取 2 到 3 像素比较合适这也符合屏幕显示的基本观感。需要特别注意的是线型状态会继续保持直到你再次调用setlinestyle。所以在一个大场景中如果已经为某个图形设置了虚线后面画其他图形之前要记得重新设置否则会出现“本来想画实线却画成虚线”的奇怪效果。5.3 填充顺序与覆盖关系绘制小场景时填充顺序决定了图形的可见区域。后绘制的图形会覆盖先绘制的图形这是图形库最通用的规则也是新手经常忽略的。以画一座山为例如果先画草地再画山山的底部会被草地遮住如果先画山再画草地山脚就像“沉入”草地中。两种顺序没有绝对对错取决于你想要的效果。想要表达“山在远处、草地在近处”就应该先画山再画草地。修饰图像时要养成“从远到近”的绘制顺序。天空永远是最底层其次是远处的山脉然后是近处的草地最后才是房子、树、人物等主体。如果不规划覆盖顺序即使每个图形的颜色和位置都正确整体也会错乱。5.4 文字样式是修饰的一部分界面中经常需要添加标题、说明文字。outtextxy负责在指定坐标输出文字但文字同样需要修饰。EasyX 中settextstyle设置字体、大小settextcolor设置文字颜色。settextstyle(24, 0, _T(Microsoft YaHei)); settextcolor(RGB(50, 50, 50)); outtextxy(100, 500, _T(A Simple Scene));第一个参数是字体高度第二个参数是字符宽度0 表示由系统根据高度自动匹配。字号 24 表示文字大约 24 像素高。如果输出中文建议使用 Windows 常见中文字体如 Microsoft YaHei、宋体等。在控制台项目中中文输出还需要注意字符集设置使用_T(...)可以让代码同时适应窄字符和宽字符项目减少乱码风险。6. 完整示例从零绘制一张小场景有了前面的基础和修饰方法可以综合做一个完整示例。下面代码会画出一个简单的自然场景蓝天、太阳、远处的山、绿色草地、小屋和两棵树。建议直接复制到 Visual Studio 工程中运行。#include graphics.h #include conio.h #include tchar.h const int WIDTH 800; const int HEIGHT 600; void drawSky() { setfillcolor(RGB(135, 206, 235)); solidrectangle(0, 0, WIDTH, HEIGHT); } void drawSun() { setlinecolor(RGB(255, 140, 0)); setfillcolor(RGB(255, 215, 0)); fillcircle(680, 80, 45); } void drawMountain() { POINT pts[] { {0, 500}, {180, 250}, {350, 430}, {480, 220}, {650, 410}, {800, 280}, {800, 600}, {0, 600} }; setlinecolor(RGB(100, 110, 90)); setfillcolor(RGB(155, 165, 135)); fillpolygon(pts, sizeof(pts) / sizeof(pts[0])); } void drawGrass() { setfillcolor(RGB(102, 180, 80)); solidrectangle(0, 420, WIDTH, HEIGHT); } void drawHouse() { // 墙体 setlinecolor(RGB(160, 100, 60)); setfillcolor(RGB(222, 184, 135)); fillrectangle(260, 330, 470, 470); // 屋顶 POINT roof[] { {235, 330}, {495, 330}, {365, 230} }; setlinecolor(RGB(180, 40, 40)); setfillcolor(RGB(220, 80, 60)); fillpolygon(roof, 3); // 门 setfillcolor(RGB(101, 67, 33)); solidrectangle(330, 380, 400, 470); // 窗 setlinecolor(RGB(180, 180, 180)); setfillcolor(RGB(173, 216, 230)); fillrectangle(290, 350, 325, 385); // 窗 setlinecolor(RGB(180, 180, 180)); setfillcolor(RGB(173, 216, 230)); fillrectangle(405, 350, 440, 385); } void drawTree(int x, int baseY) { // 树干 setfillcolor(RGB(139, 69, 19)); solidrectangle(x - 10, baseY - 60, x 10, baseY); // 树冠 setlinecolor(RGB(34, 100, 34)); setfillcolor(RGB(34, 139, 34)); circle(x, baseY - 100, 40); } int main() { initgraph(WIDTH, HEIGHT); drawSky(); drawSun(); drawMountain(); drawGrass(); drawHouse(); drawTree(150, 500); drawTree(660, 510); // 右下角添加标题文字 settextstyle(26, 0, _T(Microsoft YaHei)); settextcolor(RGB(255, 255, 255)); outtextxy(30, 30, _T(A Simple Scene)); _getch(); closegraph(); return 0; }这段代码的核心顺序是天空 - 太阳 - 山 - 草地 - 房子 - 树。先画天空与太阳是因为太阳需要在天空中显示随后画山让山的底部被后面的草地截断草地覆盖画面下半部分房子和树作为前景最后绘制保证它们完整显示在最上层。代码中大量使用了setlinecolor、setfillcolor与绘制函数配合的方式。每一次绘制元素前都重新设置颜色虽然看起来代码更长但能避免“上一个图形用的颜色被下一个图形继承”的状态污染问题。比如画完太阳后再画山如果不重新设置山的边框色山就会沿用太阳的橙色边框影响整体效果。运行这段程序后你会看到一幅干净的小场景。相比之前只画几个基本形状的效果修饰体现出的价值非常明显。你可以试着修改三个颜色参数天空的蓝色、草地的绿色、屋顶的红色观察画面观感变化这是理解颜色搭配最直接的练习方式。7. 动态图形与防闪烁处理静态图形只能算是图像绘制的一半。一旦画面需要动起来比如小球沿直线运动新手会立刻遇到一个全新问题画面疯狂闪烁。闪烁的根源在于每帧绘制都执行了“清空画布”和“重新绘制”。当程序在一个循环中执行cleardevice()清除上一帧再调用circle绘制圆球时清除动作和绘制动作之间存在一个短暂空白期。这个空白被显示器呈现给用户就会表现为背景闪动。随着图形数量增加闪烁会越来越明显。解决思路是双缓冲。简单说程序先在内存中准备一块和窗口一样大的“虚拟画布”把所有图形都画到虚拟画布上画完一帧后一次性提交到窗口。用户看到的是完整帧而不是绘制过程。EasyX 提供了一套批处理函数BeginBatchDraw(); // 绘制各种图形 FlushBatchDraw(); EndBatchDraw();下面是一个完整的小球运动示例。代码会在 800 乘 600 的窗口中创建一个沿水平方向移动、碰到边缘反弹的小球并且通过 BeginBatchDraw 避免闪烁#include graphics.h #include conio.h #include windows.h int main() { initgraph(800, 600); setbkcolor(RGB(255, 255, 255)); cleardevice(); int x 100; int y 300; int vx 4; int radius 40; BeginBatchDraw(); while (true) { cleardevice(); // 绘制小球 setfillcolor(RGB(70, 130, 180)); solidcircle(x, y, radius); FlushBatchDraw(); Sleep(16); x vx; if (x - radius 0 || x radius 800) { vx -vx; } if (_kbhit()) { break; } } EndBatchDraw(); closegraph(); return 0; }这段程序的关键点有三个。第一整个循环不会每帧调用initgraph窗口只需要创建一次循环里只做“清屏、绘制、提交”三步。第二FlushBatchDraw()把内存中的画面一次性显示到窗口这是避免闪烁的核心。第三Sleep(16)把每帧间隔控制在 16 毫秒左右也就是大约 60 帧每秒的刷新节奏。如果没有这个延时程序会以极快的速度刷屏造成 CPU 占用过高画面速度也无法控制。当按下任意键时_kbhit()会返回真循环退出。退出后调用EndBatchDraw()结束批处理并释放图形窗口资源。这个模式可以迁移到很多游戏和动画项目中是图形编程的基础功。8. 常见问题与排查方法问题现象可能原因排查方式解决方案图形窗口一闪而过缺少暂停等待检查代码是否包含_getch()在closegraph()前调用_getch()或使用断点调试编译提示找不到 graphics.hEasyX 未安装或环境未配置确认是否在 Windows 环境是否关闭 VS 后安装 EasyX重新安装 EasyX手动检查 include 目录配置中文文字乱码字符集设置不一致检查项目字符集属性使用_T(...)包裹中文字符串或修改项目字符集为 Unicode图形的颜色和预期不一致设置了颜色但绘制函数是旧版兼容模式确认设置颜色的位置在绘制之前把setlinecolor、setfillcolor放在绘制函数前图形边框不是想要的颜色边框颜色和填充颜色搞混检查当前边框色状态显式设置setlinecolor或使用solid系列函数去掉边框画面不停闪烁没有使用双缓冲检查是否调用了BeginBatchDraw在动画循环开始前调用BeginBatchDraw每次绘制后调用FlushBatchDraw多边形形状很奇怪顶点坐标或顺序写错在纸上画出每个点的位置验证 POINT 数组中的坐标顺序尝试按顺时针或逆时针排列绘制后看不到图形坐标超出窗口范围检查坐标是否在窗口尺寸内用outtextxy临时输出坐标值或设置窗口背景后观察程序占用 CPU 很高动画循环没有延时检查循环中是否调用Sleep在每帧提交后增加Sleep(16)或类似延时排查问题时有一个通用原则先看控制台和编译输出再看坐标和状态设置。很多绘图问题不是 API 用错了而是代码执行顺序不符合“先设状态再绘图”的规则。双击窗口闪退、画面空白、颜色错误都是初学者最高频的几类问题参照表格里的顺序检查通常能在几分钟内定位。9. 绘图代码的工程化建议9.1 用函数或类组织绘制模块真正项目中不应该把几十个绘制调用堆在 main 函数中。上面小场景已经展示了函数拆分的好处drawSky、drawSun、drawHouse各自负责一部分代码结构清晰修改某个元素时不需要在数百行代码中大海捞针。如果场景元素有状态比如每个角色有位置、大小和颜色使用结构体或类来封装会更合适。定义一个CircleBall结构体把坐标、半径、颜色、速度作为成员绘制函数只接受这个结构体对象代码就能从“面向过程画图”过渡到“面向对象组织画面”。9.2 集中管理颜色和配置颜色如果散落在代码各处后期调整会非常痛苦。一种简单做法是在文件开头定义常量const COLORREF SKY_BLUE RGB(135, 206, 235); const COLORREF GRASS_GREEN RGB(102, 180, 80);后续绘制时引用常量需要换主题时只改一处。虽然示例程序小常量看起来“多此一举”但当你开始做课程设计或小游戏时统一配置带来的便利会非常明显。9.3 绘制顺序要服务于画面逻辑绘制顺序不是随意定的而是画面语义的体现。背景层先画前景元素后画远处物体先画近处物体后画。如果需要频繁改变遮挡关系更应该把这个规则想清楚否则修改一个元素的坐标就会影响整个画面的叠加效果。9.4 及时释放窗口资源closegraph()负责释放图形窗口相关的资源。虽然程序退出后系统会自动回收资源但在长期运行的软件中频繁创建和销毁绘图窗口如果不释放内存占用会逐渐上升。正确的代码结构是initgraph和closegraph成对出现。9.5 不要忽略路径和编码问题如果使用loadimage加载外部图片图片路径最好不要依赖当前工作目录建议使用绝对路径或把图片放到项目目录下。输出中文时注意字符集使用_T或宽字符函数。过于复杂的工程配置问题往往和图形库本身无关而和项目属性、文件编码有关。10. 学习绘图后可以继续深入的方向绘制和修饰图像只是图形编程的入口。当你掌握了 EasyX 的基本绘制、样式修饰和双缓冲机制接下来有几条明确的学习路径。第一条往动画框架方向走。尝试为小球运动加入更多角色使用结构体保存对象状态加入键盘或鼠标控制。这能帮助你理解游戏循环的基本结构也就是“输入 - 更新 - 绘制”的循环模式。第二条往窗口工具方向走。使用 EasyX 绘制按钮、滑块、文本输入框等自定义控件体会界面组件如何通过基础几何图形组合出来。这种能力对课程设计中的可视化界面很有帮助。第三条往跨平台图形库方向走。SDL 和 SFML 是比 EasyX 更底层的跨平台图形库Qt 则更适合做桌面软件界面。你会发现前面学习的坐标系统、颜色状态、事件循环、双缓冲这些概念都能复用。图形库虽然各有差异但它们解决的本质问题是一致的。对于大多数初学者不需要一上来就钻研复杂的三维图形学而是先把二维绘制的基础彻底打好。建议你把文章中的小场景代码完整跑通然后亲手改三到五处颜色、形状和位置。当你能够不看文档自己组织出一个协调、不闪烁、有层次的新画面时“绘制和修饰图像”这一章的核心能力才算真正掌握。
返回列表