
简介graphics.h 的现代替代方案 winBGIm 以“bug-free”修复版源码形式发布面向在 CodeBlocks 中学习 C 图形编程的初学者以及需要完成课程设计或图形学实验的开发人员。压缩包内包含 4 个文件主要是两个头文件、一个静态库和一份链接配置说明分别承担图形接口声明、底层功能实现和 IDE 引用设定整体体积仅 24KB结构精简。这套源码已有 8124 人学习使用被广泛用于快速搭建可运行的图形开发环境尤其适合刚接触图形编程的 C 学习者。借助它可直接绘制点、线、矩形、椭圆等基本图元并支持像素读取与填充、文本输出、背景色与字体样式设置以及鼠标和键盘事件响应帮助初学者避开旧版 graphics.h 的常见兼容问题快速理解从 initgraph 到 closegraph 的完整流程。凭借这套源码和文档用户可在 CodeBlocks 中无需复杂手工配置即可编译运行图形程序并顺畅过渡到 OpenGL、DirectX 等现代图形库的学习。1. 一个让人又爱又恨的图形库先让 CodeBlocks 认下 graphics.h在 CodeBlocks 里新建一个 C 语言项目第一行写上#include graphics.h然后点编译大概率你会看到这样的报错graphics.h: No such file or directory。这不是你的代码写错了而是 GCC 工具链默认根本不带图形库。winBGIm 这个源码包就是把 Turbo C 时代的 BGI 图形接口整体移植到 Windows 的产物网传的 bug-free 版本刚好把 CodeBlocks 下的头文件路径和链接参数问题一并解决了。装上它之后你可以用纯 C 写窗口、画圆、显示文字、做动画适合课程作业卡在图形阶段、又不想为一个小效果引入大型 GUI 库的同学。这篇笔记按源码结构、配置流程、核心代码到二次改造的顺序把整个过程完整拆开讲一遍。2. 技术底座拆解winBGIm 和旧时代 graphics.h 差在哪2.1 它不是模拟器GDI 驱动的真图形窗口先澄清一个常见误解winBGIm 不是 DOSBox也不是在控制台里模拟图形模式。传统的 Turbo C 提供的graphics.h运行在 DOS 实模式下屏幕被切换成 320x200 或 640x480 的图形模式程序直接往显存地址写像素。winBGIm 换了一种思路它把 BGI 这套 API 的函数名和参数保留下来底层全部改调 Windows GDI 绘图接口。你调用circle()最终执行的是Ellipse()或Arc()之类的 GDI 函数画布不是显存而是一个真实存在的 Windows 窗口。这样做的好处很明显分辨率不再被锁死在 640x480窗口可以任意设定尺寸还能带标题栏程序跑起来就是普通 Windows 应用最小化、移动、遮挡都符合日常习惯。坏处也伴随而来——它依赖 GDI 的窗口消息循环如果你在非图形界面环境比如纯控制台子系统里调用initwindow多半会失败。这个版本能被称为 bug-free很大程度上就是因为把这些 Windows 相关的初始化和资源释放处理得比较干净不会再出现旧移植版那种窗口关不掉、句柄泄漏的问题。2.2 源码包目录结构拿到压缩包先认这几样解压这个源码包后你看到的文件数量其实不多但每一个都有明确分工。常见的发布形式里核心文件是这几个graphics.h // 入口头文件你的代码 include 它 winbgim.h // 真正的接口声明与内联实现graphics.h 内部会引用 libbgi.a // 预编译好的静态库链接阶段的另一半关键 winbgim.cpp // 库的源码编译成 libbgi.a 的源文件 examples/ // 示例工程通常包含数个可运行的 demo README.txt // 说明文档含链接参数和常见问题我习惯拿到压缩包后先打开README.txt因为不同版本的 winBGIm 在链接参数上略有差别。核心就一句话编译阶段要让编译器找到graphics.h链接阶段要让链接器找到libbgi.a。这两个问题解决掉绝大多数的图形库跑不起来都迎刃而解。还要留意一个细节graphics.h是入口但它本身没有多少函数实现真正的实现在winbgim.h和libbgi.a里。这导致如果你把它当普通头文件复制到系统 include 目录却忘了把libbgi.a放到链接路径下编译能通过链接照样报错。2.3 坐标系与图元模型先看懂这台画布机器winBGIm 的坐标系沿用了 BGI 的老传统原点在窗口左上角x 轴向右递增y 轴向下递增单位是像素。这在刚开始用的时候最反直觉——很多从数学坐标系过来的同学会下意识认为 y 向上为正结果画出来的图形是上下颠倒的。// 坐标轴范围x 从 0 到 639y 从 0 到 479以 640x480 窗口为例 line(0, 0, 639, 479); // 左上角到右下角的对角线 line(0, 479, 639, 0); // 左下角到右上角的对角线注意line函数接收的四个参数分别是x1, y1, x2, y2没有线宽、没有颜色颜色要单独用setcolor设置。这是 BGI 设计上比较早期的地方状态量当前颜色、当前填充样式、当前线型是全局的画图函数只关心几何位置。这种模型对初学者其实很友好但对已经习惯每个绘制调用自带完整参数的开发者来说需要改一改思路——你在调用circle之前必须先想清楚当前全局画笔状态是什么。3. 环境落地CodeBlocks 识别 graphics.h 的完整配置流程3.1 先决定是直接用预编译库还是自己编一次库bug-free 源码包里一般会带一个编译好的libbgi.a省去你自己折腾编译的步骤。但我还是建议至少手动编译一次理由很实际不同版本的 CodeBlocks 带的 GCC 位数可能不一样预编译库如果只给了 32 位版本你的 64 位工具链链接时会出现架构不匹配的报错。自己编库的过程其实不复杂在 CodeBlocks 里新建一个空白工程把winbgim.cpp拖进去编译成静态库就行。我一般会在命令行里快速验证库能不能编g -c winbgim.cpp -o winbgim.o ar rcs libbgi.a winbgim.o第一行是编译库源码生成目标文件第二行用ar把目标文件打包成静态库。看完这两条命令你就能理解所谓的libbgi.a本质上就是把winbgim.o归档在一起跟你写一个普通 C 文件再打包没有任何区别。如果你的源码包里的winbgim.cpp依赖了某些平台头文件编译报错缺失时检查一下 CodeBlocks 当前选择的编译器是否真的是 MinGW而不是系统自带的别的 GCC。3.2 CodeBlocks 里的三处设置头文件路径、库路径、链接参数在 CodeBlocks 的菜单栏依次打开 Settings - Compiler - Global compiler settings然后在 Search directories 标签页下配置头文件路径。这里要添加两个目录一个是graphics.h所在目录一个是winbgim.h所在目录通常它们在一起。如果源码包里有单独的include文件夹把路径指到那个文件夹即可。链接设置有两个地方。第一在 Linker settings 的 Link libraries 里点击 Add把libbgi.a的完整路径添加进去。第二在 Other linker options 里手动输入依赖库列表-lbgi -lgdi32 -lcomdlg32 -luuid -loleaut32 -lole32 -lstdc逐个解释一下-lbgi告诉链接器搜索libbgi.a-lgdi32和-lcomdlg32是 Windows 的图形设备和通用对话框库因为 winBGIm 的底层绘图是 GDI 实现这两个不能少-luuid、-loleaut32、-lole32是 COM 相关的运行库winBGIm 内部访问系统字体和图标时会用到最后那个-lstdc很多人会漏因为 winBGIm 的源码是 C 写的你却在用 C 语言写主程序链接器需要把 C 标准库也拉进来否则会出现一批无法解析的std::符号。注意以上所有-l参数如果是在命令行里手写编译命令应该放在源文件名的后面。GCC 的链接器按顺序扫描库链接阶段看到-lbgi时如果前面还没有需要它的目标文件这个库会被跳过。CodeBlocks 的图形界面会自动把链接参数放在命令末尾所以不太容易踩这个坑但如果你哪天改用手动编译顺序就是第一道坎。3.3 第一个冒烟的 Demo画一个会眨眼的太阳配置完成后新建一个main.c写一个最简程序验证环境。这个 demo 不需要任何工程技巧只要能看到窗口、画出东西、正常关闭就说明环境通了。#include graphics.h #include conio.h int main() { initwindow(480, 480, Blink Sun); setbkcolor(BLACK); // 背景设为黑色 setcolor(YELLOW); // 画笔颜色决定圆边框和 floodfill 的边界色 for (int i 0; i 5; i) { setfillstyle(SOLID_FILL, YELLOW); circle(240, 240, 100); floodfill(240, 240, YELLOW); // 从圆心向外灌色遇到 YELLOW 边界停下 delay(300); setfillstyle(SOLID_FILL, BLACK); floodfill(240, 240, YELLOW); // 换黑色重新灌圆中心变黑形成闭眼 delay(300); } getch(); // 等待按键避免窗口一闪而过 closegraph(); return 0; }这段代码的逻辑分三层initwindow负责开窗并画布清空circle只是画出边框真正把圆填满的是floodfill它从圆心做种子填充一直填到碰到与setcolor颜色相同的边界为止交替把填充色改成BLACK就能在同一窗口里实现眨眼动画。delay(300)是简单的毫秒级暂停winBGIm 提供这个函数你不用自己去写 Windows 的Sleep。编译运行后如果看到黄色和黑色交替变化的圆说明头文件、库、链接参数全部就位。这之后你再写的所有图形程序都复用这套环境配置。4. 核心模块实战从窗口初始化到像素级绘图4.1 initwindow 与 closegraph窗口生命周期管理initwindow是 winBGIm 里最核心的入口函数它的声明大概是这样的int initwindow(int width, int height, const char* title NULL);第一个参数是新窗口的宽度单位像素第二个是高度第三个是窗口标题传NULL会得到一个默认标题。函数成功时返回窗口句柄失败返回-1。我刚接触这个库的时候最大的教训就是不看返回值——窗口打不开后面的绘图代码全都白执行。if (initwindow(800, 600, My App) -1) { printf(initwindow failed\n); return 1; }closegraph与之对应负责释放图形环境占用的资源并关闭窗口。要注意的是程序结束前必须调用一次否则窗口句柄可能泄漏。如果前面调用了initwindow几十次却没closegraph系统句柄会逐渐耗尽表现是窗口越来越卡、最后干脆开不出来。这在调试长流程程序时很容易发生我习惯把initwindow和closegraph的调用对放在同一个函数里避免跨函数散落导致配对失控。4.2 常用图元函数line、circle、rectangle 的参数细节图元函数是画图的基础。winBGIm 保留了 BGI 风格的函数签名因此你看到的参数和教材里 Turbo C 的写法几乎一样setcolor(WHITE); line(0, 0, 200, 100); // (x1, y1) 到 (x2, y2) circle(150, 100, 30); // (圆心 x, 圆心 y, 半径) rectangle(20, 20, 120, 80); // (左上 x, 左上 y, 右下 x, 右下 y) ellipse(200, 150, 0, 360, 60, 30); // 中心 起始角/结束角 横半轴/竖半轴几个容易犯错的地方line的坐标是绝对坐标没有相对坐标的概念不存在从当前点接着画线的用法rectangle的四个参数是左上角和右下角两个点而不是起点 宽高所以画一个宽 100、高 60 的矩形时右边那个点要自己算left widthcircle的第三个参数是半径不是直径传半径的时候被除以 2 这种事经常发生。除此之外setcolor设置的是边框颜色setfillstyle设置的填充样式只对floodfill、fillpoly这类函数和部分封闭图元生效circle本身不会因为它自动变成实心圆必须先setfillstyle再floodfill。winBGIm 还提供moveto、lineto、arc、sector、bar这些相对少见但很实用的函数。bar是画实心矩形的快捷方式不用先画边框再填充sector可以画带角度的扇形做饼状图时非常合适。阅读winbgim.h里的注释你会发现它把图元函数分成了画轮廓和画填充两组按这套接口做事代码结构会清晰很多。4.3 颜色与填充setcolor、setfillstyle 的易错点颜色系统是 BGI 兼容库的老传统使用 0 到 15 的编号setcolor(RED); // 0~15 的枚举已经预定义好 setfillstyle(SOLID_FILL, BLUE); // 填充样式 填充颜色常用颜色的编号规律其实很好记0 是黑色1 蓝2 绿3 青4 红5 品红6 棕7 浅灰8 到 15 是在前八个颜色的基础上调亮。也就是说YELLOW对应的编号是 14是BROWN6加亮。如果你用 RGB 颜色习惯setrgbcolorwinBGIm 也支持但默认的 16 色模式更简单竞赛题和课程作业基本都用编号色就够了。setfillstyle第一个参数是填充图案常见的有SOLID_FILL实心、EMPTY_FILL空、HATCH_FILL网格线。很多人以为调用了setfillstyle(SOLID_FILL, RED)之后圆会自动实心实际上circle画出来的仍然只是边框。必须配合floodfill使用——这也解释了为什么上一章的 demo 每次都要从圆心灌一次颜色。floodfill的第三个参数是边界颜色它必须在画圆前用setcolor设好否则种子填充找不到边界会往整个画布扩散。4.4 文字输出outtextxy 与字体设置outtextxy是在指定坐标输出文字的接口最简单的用法是setcolor(WHITE); settextstyle(DEFAULT_FONT, HORIZ_DIR, 2); // 字体、方向、放大倍数 outtextxy(100, 100, Hello Graphics);settextstyle里最容易理解错的是方向参数HORIZ_DIR表示水平方向VERT_DIR表示竖直方向也就是把文字竖着写。放大倍数 2 意味着每个字符尺寸翻倍它和字体大小不是一回事。这个函数在纯英文环境下没什么问题但凡是尝试输出中文大概率会遇到乱码。原因是 winBGIm 内部对窄字符的处理基于当前系统代码页而源码文件的编码可能不匹配。我调试过不少次最后得出的稳妥做法是先用英文或拼音把界面文字撑起来功能跑通之后再考虑中文问题如果必须中文我会绕过outtextxy自己拿窗口句柄调用 GDI 的宽字符输出接口把字串转成wchar_t再画。这不是库本身的缺陷而是 BGI 这套接口从设计上就没考虑过多字节字符集。5. 避坑指南CodeBlocks 下跑 graphics.h 的常见问题排查5.1 报错 graphics.h: No such file or directory现象编译时提示找不到头文件。原因大多数情况是 CodeBlocks 的编译器搜索路径里没有包含源码包解压后的目录。我见过有人把graphics.h复制到项目文件夹却没配置任何路径——GCC 默认不会搜索当前项目目录之外的自定义目录项目文件夹也不一定在默认搜索列表里。解决进入 Settings - Compiler - Global compiler settings - Search directories在 Compiler 标签下添加graphics.h所在目录。注意是 Compiler 标签不是 Linker 标签很多人在 Linker 里加了半天不起作用因为链接阶段不找头文件。5.2 链接时报 undefined reference编译却很顺畅现象编译通过链接阶段刷出一屏undefined reference to initwindow之类的错误。原因头文件被找到了但libbgi.a没有被链接进来。更隐蔽的是库顺序问题如果-lbgi放在源文件之前GCC 扫描库时还没看到你的目标文件引用就不打包这个库于是符号全部未解析。另外C 语言项目链接 C 写的库漏掉-lstdc也会报一堆std::相关的未解析符号。解决在 Linker settings 里显式添加libbgi.a完整路径并在 Other linker options 里按顺序填入-lbgi -lgdi32 -lcomdlg32 -luuid -loleaut32 -lole32 -lstdc。手动命令行编译时这些参数永远放在命令末尾。5.3 图形窗口一闪而过现象运行程序后图形窗口刚弹出来就立刻消失根本看不清画了什么。原因程序主函数执行完毕系统回收进程资源任何窗口都会关闭。你在代码里画完图直接return 0窗口自然瞬间销毁。还有一种情况是initwindow返回-1绘图代码被跳过之后代码很快执行完退出。解决绘图结束后调用getch()或者delay()暂停等用户按键再closegraph()。同时检查initwindow的返回值如果为-1输出错误信息定位原因——常见失败原因是窗口尺寸传了 0 或者负数。5.4 动画闪烁、画面撕裂现象画多帧动画时图形窗口里能明显看到先擦除再绘制的过程边缘闪得很厉害。原因winBGIm 走 GDI 输出GDI 本身没有与显示器刷新同步的多缓冲机制。你每画一帧屏幕上实时更新大量像素写入就形成闪烁。解决winBGIm 提供了批量绘制的接口一般我会在动画循环开头调BeginBatchDrawing()循环结束调EndBatchDrawing()让整帧一次性提交。如果还是闪就用getimage把背景存成 IMAGE 缓冲绘制前先putimage恢复背景再画新图形这种方法等效于自己实现双缓冲。5.5 outtextxy 输出中文乱码现象outtextxy(100, 100, 你好)显示出来的是乱码或问号。原因winBGIm 的窄字符输出依赖当前 Windows 的代码页而 GCC 编译器按源码编码解析字符串字面量。源码存成 UTF-8库内部按 GBK 解释或者反过来结果都会错位。解决临时方案是把界面文案改成英文或拼音。长期方案是自定义一个文字输出函数把字符串转成wchar_t后调用 Windows 的TextOutW输出绕开 winBGIm 的窄字符路径。动手前先确认你的定位坐标是在 client 区域坐标系里否则文字会偏移到奇怪的位置。6. 二次开发改造思路为 graphics.h 包一层可替换的画板接口拿到这份源码包之后如果你想做的事不止跑通 demo而是基于它做游戏或可视化我建议先加一层薄封装把 winBGIm 的函数藏到自己的模块后面。原因很简单winBGIm 的 API 是全局状态机风格到处散落setcolor、setfillstyle改到后期很难维护而且想换底层渲染库基本等于重写。先定义一个最小的画板接口暂时以 C 语言实现/* canvas.h */ typedef struct { int width; int height; } Canvas; int canvas_init(Canvas *c, int w, int h); void canvas_clear(Canvas *c); void canvas_circle(Canvas *c, int x, int y, int r, int color); void canvas_rect(Canvas *c, int left, int top, int right, int bottom, int color); void canvas_close(Canvas *c);对应实现/* canvas.c */ #include canvas.h #include graphics.h int canvas_init(Canvas *c, int w, int h) { c-width w; c-height h; return initwindow(w, h, Canvas) -1 ? 0 : 1; } void canvas_clear(Canvas *c) { cleardevice(); } void canvas_circle(Canvas *c, int x, int y, int r, int color) { setcolor(color); setfillstyle(SOLID_FILL, color); circle(x, y, r); floodfill(x, y, color); } void canvas_close(Canvas *c) { closegraph(); }这套接口的价值在于上层业务代码只依赖canvas_clear、canvas_circle这些语义清楚的函数不直接碰graphics.h。将来如果要从 winBGIm 换成 SDL只需要重写canvas.c业务代码一行都不用动。我在把 demo 改成贪吃蛇的时候深有体会——所有碰撞检测只关心画布宽高绘图层被隔离后逻辑层几乎不受底层影响。验证改造是否成功的办法也很简单写一个 10fps 的运动小球小球在Canvas里碰壁反弹跑一分钟不掉帧、内存不涨说明封装层没有资源泄漏。从那以后我每次拿到任何源码包都会先强制自己走一遍编库、跑最小 demo、画一帧动画、测量窗口关闭是否干净这条流程环境不踏实就绝不动业务代码。希望这份笔记也能帮你在 CodeBlocks 里少踩几个坑早点把图形程序跑起来。本文还有配套的精品资源点击获取