简介:这是一份适用于 C/C++ 传统图形编程的 graphics.h 头文件及配套库文件资源包,主要面向需要在老版 Visual Studio(如 VC98 环境)中完成图形学实验、控制台绘图练习或课程设计的开发者。将相应头文件与库文件分别拷贝到 VS 的 Include 与 Lib 目录后,即可直接调用 initgraph、line、circle 等图形函数,免去手动配置编译环境的麻烦。压缩包体积仅 46KB,包含 4 个文件,其中 2 个 .h 头文件负责函数声明与常量定义,2 个 .lib 库文件提供链接阶段所需的实现代码,结构简单、即下即用,适合刚从 C 语言入门转向图形界面开发的学习者。目前平台已有 9183 人浏览学习,说明对解决经典图形编程环境配置问题具有较高的参考价值。
1. graphics.h 是 C 语言图形编程的入坑起点,也可能是第一道劝退门槛
graphics.h 是 Turbo C 时代图形接口(BGI)的头文件,也是很多人第一次在 C 语言里接触像素绘图时遇到的那个“老朋友”。它能解决一个很朴素的问题:在课程作业或竞赛演示里,用一两百行 C 代码画出直线、圆、矩形、填充色和简单动画,而不用引入大型图形框架。适合正在上 C 语言课的学生、带图形学实验课的老师,以及想快速做一个算法可视化 Demo 的从业者。
但它的坑也肉眼可见:环境配置繁琐、坐标系统反直觉、中文乱码、虚拟机上黑屏。这些从不是你的代码写错,而是你对这套老接口的运行方式还不够熟。这篇文章会把从环境搭建、API 参数、动画写法到故障排查的完整路径讲清楚,照着做就能跑起来。
2. 跑通 graphics.h:环境选型与最小可编译工程
2.1 为什么还在用 graphics.h:BGI 的运行机制与历史包袱
graphics.h 不是标准 C 库的一部分,它属于老版本 Turbo C 里自带的 Borland Graphics Interface,缩写 BGI。头文件里只声明了 initgraph、setcolor、line、circle 这些函数,真正干活的代码在对应的图形驱动文件里,通常是 .BGI 后缀的二进制驱动,比如 EGAVGA.BGI。也就是说,graphics.h 的绘图本质上是运行时加载驱动,再由驱动直接操作显存和硬件寄存器。
理解了这一点,你就明白为什么现代操作系统上直接写#include <graphics.h> 编译容易通过,一运行就黑屏或报错——因为没有那层驱动加载环节。老程序必须在 DOS 环境或兼容层里运行,让 initgraph 能找到 .BGI 文件,并且让显卡模式切换逻辑生效。这也是后续所有环境选型的出发点:要么找 DOS 模拟器,要么找把 BGI 调用映射到 Windows 或 Linux 图形 API 的兼容实现。
2.2 方案一:Turbo C 3.0 + DOSBox(最保真,适合课程作业)
如果你要原汁原味地把教材里的代码跑出来,最常见做法是装一个 DOSBox 模拟器,里面放一套老版本 Turbo C。DOSBox 负责模拟完整的 VGA 显示环境,initgraph 的 DETECT 模式能正常探测显卡,.BGI 驱动也能顺利加载。
我一个典型启动脚本会这样写,把装有 Turbo C 的目录挂载为 C 盘:
dosbox -c "mount c: /home/me/tc" -c "c:" -c "cd tc" -c "tc.exe"说明:第一段命令把本机的 tc 目录映射成 DOSBox 里的 C 盘,第二段切换到 C 盘,第三段进入 Turbo C 的安装目录,第四段启动 IDE。如果你是 Windows 本机目录,把 /home/me/tc 换成对应路径即可。
进去之后,在 IDE 里写下面这段最小程序,然后编译运行:
#include <graphics.h> #include <conio.h> int main() { int gd = DETECT, gm; initgraph(&gd, &gm, "."); circle(320, 240, 50); getch(); closegraph(); return 0; }说明:initgraph 的第一个参数传 DETECT 表示让系统自动检测显卡类型,检测结果会回填到 gm 里,第三个参数传英文句点 ".",意思是 BGI 驱动文件从当前目录加载。circle 的 x 和 y 是圆心坐标,第三个参数是半径。getch 让程序等待按键后再退出,否则窗口一闪而过。
这个方案的好处是兼容性极好,老代码原样编译基本都能跑;坏处是编辑体验差、分辨率低、窗口小,不适合做较大的项目。它的定位明确:拿来跑课程实验、验证老代码,而不是做新开发。
2.3 方案二:Windows 下用集成工具包,省去 DOS 模拟
如果你在 Windows 上,又不想每次开 DOSBox,另一个常见做法是使用把 BGI 移植到 Windows 图形环境的集成工具包。这些工具包通常是某位开发者把老 Borland 编译器的 Windows 版驱动封装了一下,让 graphics.h 的函数最终调用 Windows 的绘图 API,窗口可以实实在在显示在桌面上。
使用步骤一般是:
- 下载并解压工具包,注意把整个目录放在不含中文字符和空格的路径下。
- 打开 IDE,新建一个 C 语言工程。
- 在工程设置里把 include 路径指向工具包自带的 include 目录,把库路径指向 lib 目录。
- 新建源文件,写代码时仍然include <graphics.h>。
- 编译运行,代码和 DOS 时代的写法完全一致,连 initgraph 的参数都不用改。
这套方案解决的最大痛点是从 DOSBox 切来切去,它把一个窗口挂在 Windows 桌面上,调试起来顺手很多。缺点是这些工具包对陈旧的代码更友好,对现代的 64 位编译器和宽字符处理就不太行了。如果你只是交一次课程作业,它比 DOSBox 效率高;如果要长期维护,我更推荐下面第三条路。
2.4 方案三:在现代编译器里用兼容实现,可移植性最强
还有一类场景是你在 Linux 或 macOS 上做实验,不想用 Windows 工具包,也不想装 DOSBox。这时最常见的路线是找一套用现代图形库封装 BGI 接口的兼容实现,它提供同名头文件和同名函数,底层把绘图翻译成系统的窗口事件和帧缓冲。
我用这种方案时,会先确认三件事:
- include 目录里有没有真正的 graphics.h,和系统自带的头文件是否冲突。
- 库文件是否已经编译好,编译命令里有没有加 -l 参数链接它。
- 运行时会不会要求额外放置 .BGI 驱动文件,还是库里已经内置了软件渲染路径。
这三步里最容易踩坑的是头文件冲突。有些系统自带的图形相关头文件也叫 graphics.h,如果你的编译命令没指定 include 搜索路径,编译器可能找到错误版本。所以编译命令里我一般会显式指定 -I 指向兼容库目录,避免玄学问题。
这条路的优势是代码能跟着现代编译器走,可以配合 Makefile 或 CMake 管理;缺点是某些老代码里的细节行为(比如字体表、颜色映射)和真实 BGI 不完全一致,偶尔会有显示偏差。作为工程取舍,我通常优先用它做新项目,用 DOSBox 做老代码的核对基准。
3. 核心 API 与坐标系统:把像素、颜色和窗口关系一次理清
3.1 initgraph 三个参数:驱动、模式、路径,写错一个就黑屏
graphics.h 里最重要的函数是 initgraph,它的签名是: void initgraph(int *graphdriver, int *graphmode, char *pathtodriver);
第一个参数传驱动编号,第二个参数传分辨率模式,第三个参数传驱动文件路径。最常见的组合是第一个参数传 DETECT(自动检测),第二个参数传 0 或者留空让系统决定,第三个参数传 "."。但自动检测不等于不踩坑,在虚拟机或者某些兼容环境下,DETECT 可能会误判显卡,选一个太老或太高的模式,导致画面花掉或直接退出。
老模式的常用取值与分辨率关系如下:
| 模式常量 | 典型分辨率 | 适用场景 |
|---|---|---|
| DETECT | 自动探测 | 绝大部分场景 |
| VGA | 640x480 | 标准老程序 |
| EGA | 640x350 | 老硬件模拟 |
| CGA | 640x200 | 最低兼容 |
我建议在可靠性优先时,不要完全依赖 DETECT,而是手动指定 VGA 和 640x480,这样无论宿主机什么环境,画面大小都是可预期的。修改方式是把 initgraph 改成 initgraph(&gd, &gm, ""),然后在调用前给 gd 赋 DETECT、给 gm 赋 VGA 对应的数值,或者直接在库文档里查 VGA 模式编号。手动指定模式后,坐标范围就固定了,这也是排除黑屏问题最快的一招。
3.2 绘图函数族:line、circle、rectangle、bar、ellipse 的边界与填充规则
graphics.h 的绘图函数非常多,但工程上常用的没几个。你需要把它们按“描边”和“填充”两类分开记:
- line(x1, y1, x2, y2):画直线,不填充。
- circle(x, y, radius):画空心圆,不填充。
- rectangle(x1, y1, x2, y2):画空心矩形,两个点是左上角和右下角。
- bar(x1, y1, x2, y2):画实心矩形,不需要额外填充。
- ellipse(x, y, startAngle, endAngle, xRadius, yRadius):画椭圆弧,角度范围可以控制是整圆还是扇形。
画一个圆并填充的代码如下:
#include <graphics.h> #include <conio.h> int main() { int gd = DETECT, gm; initgraph(&gd, &gm, ""); setfillstyle(SOLID_FILL, RED); setcolor(RED); circle(160, 120, 40); floodfill(160, 120, RED); getch(); closegraph(); return 0; }说明:setfillstyle 设置填充模式和填充颜色,setcolor 设置描边颜色,circle 画出红色边界圆,floodfill 从内部点 (160, 120) 开始填充,直到碰到边界色 RED 才停止。注意 floodfill 的第三个参数必须传边界颜色,通常就是刚才 setcolor 设置的颜色;如果你把边界色参数写成填充色,它找不到闭合边界,会一直往边缘扩散,把整个屏幕填满。
这个搭配是 BGI 最常见的翻车点之一:其实完整流程应该是先 setcolor 设边界色,再 circle,再 setfillstyle 设填充色,最后 floodfill。如果你先调 setfillstyle 再调 setcolor,边界色和填充色就会串,最终结果完全不是预期的样子。
3.3 颜色与填充特殊性:setcolor、setbkcolor、setfillstyle 的配合逻辑
BGI 支持的颜色是老 16 色调色板,颜色编号从 0 到 15,对应 BLACK、BLUE、GREEN、CYAN、RED、MAGENTA、BROWN、LIGHTGRAY、DARKGRAY、LIGHTBLUE、LIGHTGREEN、LIGHTCYAN、LIGHTRED、LIGHTMAGENTA、YELLOW、WHITE。
这里有个容易忽略的点:setbkcolor 设置背景色,但它只影响整个屏幕的背景,不清除已有内容;clearviewport 才会把整个视口按背景色清空。如果程序里想“刷新画面”,很多新手会用 setbkcolor + clearviewport,结果发现颜色变深了、画面闪得厉害。更稳的做法是保留旧画面不动,只在需要擦除的区域用原来的背景色重画,或者直接用 putimage 回写背景缓冲。
颜色错误在实际运行中表现出的症状通常是:圆画完之后 floodfill 颜色不对,或者整屏被某个颜色铺满,或者图形“消失”在背景里。遇到这种状况,不要怀疑显卡驱动,先检查 setfillstyle、setcolor、floodfill 三个调用顺序和参数,90% 的问题都在这里。
3.4 文本输出与字体:为什么 outtextxy 默认会乱码
graphics.h 里的 outtextxy 用来在指定位置输出文本,但它默认使用的是 BGI 内嵌的 8x8 点阵英文字体。如果你直接传入中文字符串,绝大多数兼容实现会显示成乱码或空白。原因是点阵字体表里没有中文字形,中文编码又涉及双字节,老接口根本不认识。
绕过方式有两种:一种是把文本内容用英文或拼音表达,配合 settextstyle 设置字体大小,勉强够用;另一种是自绘汉字字形,把每个汉字先做成位图数据,再用 putimage 贴上去。很多课程项目里所谓的“中文菜单”,其实都是后端维护一张汉字点阵表,然后逐字贴图,而不是靠 outtextxy 直接显示中文。
所以在设计程序结构时,我建议把界面文案与绘图逻辑分离。需要显示中文时,用位图方式;需要稳定和通用时,用英文字符。这样既不依赖 BGI 字体,也不容易被不同平台差异绊住。
4. 动手写一个可运行的图形 Demo:小球运动且不闪屏
4.1 Demo 目标和整体结构:从 initgraph 到 draw 循环
一个能体现 graphics.h 核心用法的 Demo,我通常会选“小球沿圆周运动,同时画一条从圆心指向小球的线”。它同时覆盖直线、圆、颜色、坐标计算、循环刷新和键盘退出,足够检验环境是否正常。
整体流程是:初始化图形模式 → 保存一块空白背景区域 → 进入循环 → 每帧先恢复背景 → 画圆心 → 画小球 → 延时 → 处理按键退出 → 释放内存、关闭图形模式。这里关键的帧循环设计如下:
#include <graphics.h> #include <conio.h> #include <math.h> #include <stdlib.h> #define RADIUS 20 int main() { int gd = DETECT, gm; int x0 = 320, y0 = 240; int track = 120; int x, y, size; void *canvas; double t; initgraph(&gd, &gm, ""); size = imagesize(x0 - track - RADIUS, y0 - track - RADIUS, x0 + track + RADIUS, y0 + track + RADIUS); canvas = malloc(size); getimage(x0 - track - RADIUS, y0 - track - RADIUS, x0 + track + RADIUS, y0 + track + RADIUS, canvas); while (!kbhit()) { for (t = 0; t < 6.283; t += 0.05) { if (kbhit()) break; x = x0 + (int)(track * cos(t)); y = y0 + (int)(track * sin(t)); putimage(x0 - track - RADIUS, y0 - track - RADIUS, canvas, COPY_PUT); setcolor(LIGHTBLUE); line(x0, y0, x, y); setfillstyle(SOLID_FILL, RED); setcolor(RED); circle(x, y, RADIUS); floodfill(x, y, RED); delay(16); } } free(canvas); closegraph(); return 0; }代码逻辑说明:imagesize 先计算一个矩形区域所需的内存字节数,getimage 把这块空白背景抓取到 canvas。之后每帧循环的第一步 putimage 把 canvas 贴回相同位置,相当于擦掉上一帧的画面,避免用 clearviewport 整屏清空。这样动画不会闪屏,也是老代码里最常见的缓存技巧。
参数说明:track 是轨道半径,控制小球离圆心的距离;9 里的 6.283 是 2π,让角度 t 走完一整圈;0.05 是角度步长,调大动画更跳跃,调小更平滑;delay(16) 约等于 60 帧每秒,这是 VGA 时代的常用刷新节奏。如果你发现动画太快,优先加 delay,而不是在循环里空转,空转会白白占满 CPU。
4.2 getimage/putimage 的机制:为什么它能治闪屏
BGI 本身就是立即模式,没有双缓冲概念,每画一笔都直接写屏幕。所以当你用 clearviewport 整屏清空时,屏幕会经历“全空 → 逐笔重绘”的过程,人眼就会看到闪烁。解决思路不是去优化逐笔绘制的速度,而是不整屏清空。
getimage 的作用是把屏幕上某个矩形区域的像素数据复制到内存里,putimage 负责把这块数据原样贴回屏幕。如果你在做动画之前先把背景存下来,每帧动画开始前把背景贴回去,那屏幕恢复的是“上一次图形出现之前的画面”,而不是变成空白,闪烁自然减少。
这个手法在 graphics.h 里算标准答案,但在更现代的图形库里早被双缓冲替代。如果你以后转到其他框架,雙缓冲会直接把绘制发生在幕后缓冲区,最后一次性呈现。理解 getimage/putimage 的用意,能帮你更平滑地理解什么叫“像素缓存”和“帧缓冲”。
4.3 键盘退出与帧率控制:kbhit、getch、delay 的配合细节
kbhit 是个容易被忽略但很重要的函数,它检测“是否有键盘输入”而不阻塞程序。动画循环里每帧调用一次 kbhit,按键时返回真,然后退出循环,程序才能正常结束。单独用 getch 会阻塞在那里等按键,动画就无法持续运行,所以正确的组合是:
while (!kbhit()) { /* 每帧绘制逻辑 */ delay(16); }delay 的参数单位是毫秒,它把当前线程挂起一段时间,作用是限制帧率。如果你去掉 delay,动画速度会被主频托着走,可能快到看不出轨迹;如果 delay 参数太大,动画又像幻灯片。我一般用 16 毫秒对应大概 60 帧,课程演示时也可以调到 30 毫秒让节奏更明快。
还有一点要注意:循环退出后不要马上 closegraph,先释放通过 malloc 申请的 canvas,再关闭图形模式,否则在某些兼容层上可能产生内存层面的小告警。老编译器不一定报错,但养成先释放再关闭的习惯,后面移植到现代环境时能少很多麻烦。
5. 避坑:graphics.h 最常见的五个翻车现场
5.1 初始化黑屏:DETECT 模式在虚拟机里选错驱动
现象:程序运行后屏幕一黑,过一会儿恢复,或者干脆报错退出。原因:虚拟机显卡和 BGI 的 DETECT 自动探测结果不匹配,探测模式选到不支持的显存布局。解决:不要依赖 DETECT,手动指定模式。把 int gd = DETECT, gm; 改成 int gd = VGA, gm = VGAMAX;,然后 initgraph(&gd, &gm, ""),它会把分辨率锁定为 640x480,绕过探测。实际经验里,这一步能解决八成黑屏。
5.2 编译不过:头文件找不到与图形库未链接
现象:编译器提示 graphics.h 找不到,或者 link 阶段报一堆未定义符号。原因:include 路径没有指向 BGI 头文件目录,链接时没有加图形库参数。解决:先确认你用的工具链是哪个,再看它的 include 和 lib 目录配置。DOSBox 里的 Turbo C 需要把 .BGI 驱动放在当前目录;Windows 工具包需要在工程设置里手动加图形库;Linux 下的兼容实现则需要编译命令加 -l 参数。这属于环境配置问题,不是代码问题,排错时先看编译器输出信息,而不是盯着 C 语言代码逐行找。
5.3 图像上下颠倒:BGI 原点在左上角
现象:用数学函数画的抛物线、正弦曲线都倒过来了。原因:BGI 的坐标系统是屏幕坐标,原点在左上角,x 向右增大,y 向下增大;而数学坐标里 y 向上增大。解决:封装一个坐标转换函数,把逻辑坐标转成屏幕坐标再传给绘图函数。常见做法是:
int toScreenX(int x) { return x; } int toScreenY(int y) { return maxY - y; }说明:maxY 是当前模式下的最大 y 值,比如 640x480 里取 479。所有“数学坐标画图”的代码都经 toScreenY 转换,图形方向和直觉一致。这个小封装可以说是 graphics.h 里最实用的工具函数,没有之一。
5.4 颜色错乱:填充色与边界色混用
现象:floodfill 要么填不进去,要么把整个屏幕填满。原因:floodfill 的第三个参数需要边界颜色,而你传成了填充色;或者 setcolor 和 setfillstyle 的调用顺序反了。解决:遵循固定套路——先 setcolor 设边界色,画边框,再 setfillstyle 设填充色,最后 floodfill 传边界色。我自己写代码时会专门抽一个 fillCircle 辅助函数:
void fillCircle(int x, int y, int r, int border, int fill) { setcolor(border); circle(x, y, r); setfillstyle(SOLID_FILL, fill); floodfill(x, y, border); }说明:这样调用方不用关心边界色和填充色的先后问题,传参时也不容易把两个颜色写反。BGI 的 16 色调色板在不同兼容实现里可能有细微色差,但参数逻辑始终一样。
5.5 中文乱码:点阵字体不认汉字
现象:outtextxy 输出中文文本,窗口里显示成乱码、问号或方块。原因:BGI 自带字体表只有英文点阵,不包含中文字形。解决:不要在 graphics.h 原生的 outtextxy 里直接放中文字符串。你能做的是要么用英文界面,要么自己准备汉字点阵。课程项目里如果必须显示中文,常见做法是先把每个汉字做成小位图,再用 putimage 逐字贴出,虽然麻烦,但至少可控。只要你理解了这是字体表的限制,就不会在同一个坑里反复试。
6. 进阶:用一层 draw 封装把 graphics.h 代码带到现代窗口环境
6.1 定义最小绘制接口
如果你不满足于只在教材环境里跑通,我推荐一个让我少走很多弯路的做法:在 graphics.h 之上再包一层薄薄的统一接口。这样业务代码只依赖你自己定义的 draw 接口,不直接依赖 graphics.h,将来换任何现代图形库时,只替换实现文件,不用动业务逻辑。
先定义一个极小的头文件:
/* draw.h */ #ifndef DRAW_H #define DRAW_H void draw_open(int width, int height); void draw_close(void); void draw_pixel(int x, int y, int color); void draw_line(int x1, int y1, int x2, int y2, int color); void draw_text(int x, int y, const char *s, int color); void draw_present(void); int draw_key(void); #endif说明:draw_open 负责初始化窗口,draw_close 负责清理,draw_pixel 和 draw_line 是基础图形原语,draw_present 表示把缓冲显示到屏幕,draw_key 询问是否有按键输入。这个接口刻意省略了很多复杂能力,只保留最常用的部分,因为接口越窄,被替换成本越低。
6.2 用 graphics.h 实现这套接口
底层的实现文件长这样:
/* draw_graphics_impl.c */ #include "draw.h" #include <graphics.h> #include <conio.h> void draw_open(int width, int height) { int gd = DETECT, gm; initgraph(&gd, &gm, ""); } void draw_close(void) { closegraph(); } void draw_pixel(int x, int y, int color) { putpixel(x, y, color); } void draw_line(int x1, int y1, int x2, int y2, int color) { setcolor(color); line(x1, y1, x2, y2); } void draw_text(int x, int y, const char *s, int color) { setcolor(color); outtextxy(x, y, (char *)s); } void draw_present(void) { /* graphics.h 是立即模式,画面直接落屏,无需交换缓冲 */ } int draw_key(void) { return kbhit(); }说明:这个实现几乎是一层透传。graphics.h 本身没有缓冲机制,所以 draw_present 是空操作。将来你换到某个现代图形库时,只需要把 draw_open 改成创建窗口和渲染器,把 draw_present 改成交换前后缓冲,其他函数一一对应即可。参数说明:color 参数沿用 graphics.h 的 0-15 编号,你可以在接口里约定自己的颜色枚举,然后在实现层转换成不同图形库的颜色值。
6.3 验证方法:一条对角线和一个延时循环
封装完成之后,用一个简单的验证程序检查接口是否正常:画一条从左上角到右下角的对角线,如果显示成 45°,说明坐标和颜色链路都通了;再做 10 秒动画,用 draw_key 退出,说明输入链路也正常。验证通过后,这份代码就是你可以反复复用的基础层。
#include "draw.h" int main() { draw_open(640, 480); draw_line(0, 0, 639, 479, 14); draw_present(); delay(2000); draw_close(); return 0; }说明:14 是黄色,delay 2000 毫秒让窗口停留两秒,方便肉眼确认。只要这个程序正常,说明你的 draw 接口和 graphics.h 绑定层没问题,业务代码可以放心往上写。
我最初把所有绘图代码都直接塞在 main 里,每次换环境都要改一大片。后来某次要在另一个图形框架上做模拟演示,吃够了移植的苦,才下狠心抽出这层接口。现在不管是写课程实验还是竞赛 Demo,我都先从 draw 接口出发,底层选 graphics.h 还是别的库只是换一个实现文件的问题。这个习惯让我少翻了很多次车,希望帮到你。
本文还有配套的精品资源,点击获取