简介:这份压缩包面向学习 C++ 图形绘制的初学者、课程实验者以及需要补全 BGI 图形库环境的开发者,提供 graphics.h 头文件与 graphics.lib 库文件的配套组合,用于解决编译链接阶段缺少图形接口声明和预编译实现的问题,并适配常见的 Windows 编译环境。包内共 2 个文件,分别为 1 个 .h 头文件和 1 个 .lib 库文件,整体约 11KB,体积轻量、类型简洁,便于直接放入项目目录并按需配置包含路径与链接依赖。已有 3591 人学习或下载,说明其在 C++ 图形编程入门场景中具有一定参考价值。头文件可用于了解窗口初始化、坐标设置、基本图形绘制、颜色管理、文本输出及事件处理等接口声明,库文件则承载对应预编译实现;配合 initgraph、rectangle、line、outtextxy 等典型调用,读者可据此搭建简单的图形窗口与绘图示例,理解图形库的链接方式、基础 API 使用思路以及图形会话的初始化与关闭流程。
1. 一个 .rar 里的 graphics.lib,把 C++ 画图从控制台黑框拉到能看见的窗口
第一次做 C++ 课程设计的人,卡点往往不在算法,而在"屏幕上怎么出现一条线"。控制台只能 printf 字符,换到框架又要先配一整套工程,两周工期根本不够折腾。这个下载包里最核心的就是两样东西:一个graphics.h头文件和一个graphics.lib静态库,接口是 BGI 风格的老派图形 API——initgraph开窗口、line画线、outtextxy写字、getmessage收鼠标键盘事件,一个main函数就能跑出带界面的结果。它适合赶课设、做图形学入门作业、写小型可视化 Demo 的人;也适合想把绘制循环攥在自己手里、而不是被框架回调牵着走的熟手。下面按拆包、挂接、绘图循环、交互、排错的顺序,把它拆成能照着复现的步骤。
2. 拆包与挂接:graphics.h 与 graphics.lib 在工程里怎么落地
先把选型理由说清楚,不然后面每一步都会觉得别扭。这套东西本质上是一个薄封装:它把 Windows 下的窗口创建、GDI 绘图调用、消息循环压缩成十几个函数,用一套过程式的 API 暴露出来。相比直接写 Win32 SDK,你省掉两百行窗口过程代码;相比上 Qt 或其它框架,你省掉编译一整棵依赖树的时间。代价是它的能力边界很窄——没有控件、没有布局、没有硬件加速,画几千个图元还行,做动画粒子多了就会掉帧。判断标准很简单:你要的是"屏幕上能画、能点、能动",选它;你要的是"做一个正经桌面软件",别选它。
2.1 目录怎么摆:include 与 lib 分离
我不喜欢把.h和.lib跟源码混在一个目录里,工程大了以后自己都分不清哪个头文件是第三方带进来的。常见做法是按下图这种三层结构摆放,源码、头文件、库文件各归各位:
project/ ├── include/ │ └── graphics.h # 第三方头文件,只读不放源码 ├── lib/ │ └── graphics.lib # 静态库或导入库 ├── src/ │ └── main.cpp └── CMakeLists.txt这样摆的好处是编译器的-I和链接器的-L各指一个目录,换机器或者打包给别人时,路径规则不用改。参数说明:include/对应编译器头文件搜索路径,lib/对应链接器库搜索路径,两者不能混用——把头文件丢进lib/目录,编译器不会去那里找.h,报的错是fatal error: graphics.h: No such file or directory,很多人第一反应是"库坏了",其实只是路径没指对。
还有一件事必须先确认:这个.lib到底是静态库还是导入库。判断方法很笨但有效——如果压缩包里除了.lib还有一个同名的.dll,那.lib多半是导入库,程序运行时必须把.dll拷到.exe同目录;如果只有.lib,那就是静态库,链接进去就完事,产物是单个可执行文件,拷走就能跑。这一步没搞清,后面会出现"我这边能跑,交上去老师那边双击没反应"的经典事故。
提示:静态库和导入库的链接参数完全一样,区别只在运行期。交付前把
.exe单独拷到一个空目录双击一次,能起来才算真的没问题。
2.2 命令行链接:g++ 与 MSVC 两套写法
先用命令行编译一次,比直接开 IDE 更能看清到底链接了什么。g++ 这一套(MinGW-w64 环境)大致是这样:
g++ src/main.cpp -o demo.exe \ -I./include \ -L./lib \ -lgraphics -lgdi32 -luser32 -limm32 \ -std=c++17 -municode逐项说明:-I./include告诉编译器去哪找graphics.h;-L./lib告诉链接器去哪找graphics.lib;-lgraphics是库名,注意 g++ 的规则是-l后面跟去掉lib前缀和.lib/.a后缀的名字,所以graphics.lib对应-lgraphics,写成-lgraphics.lib会直接报找不到。-lgdi32 -luser32 -limm32是这套图形接口在底层依赖的系统库:GDI 负责实际绘制,user32 负责窗口和消息,imm32 负责输入法消息。漏掉其中任何一个,报错都是"未定义引用",且错误信息里出现的函数名你根本没在自己代码里写过——这是最容易被误判成"库版本不对"的一类问题。-municode只在头文件声明的是宽字符版入口时加,加了还报undefined reference to WinMain就把这个参数去掉再试。
MSVC 的cl写法逻辑一样,语法不同:
cl /EHsc /std:c++17 /I include src\main.cpp ^ /link /LIBPATH:lib graphics.lib gdi32.lib user32.lib imm32.lib注意/I和/link的分界:/I属于编译器选项,/link之后全部交给链接器,/LIBPATH:是链接器的库搜索路径。把graphics.lib写在/link前面,cl会把它当源文件处理,报cannot open input file。这个坑我在第一次配环境时踩过,排查了半小时才发现是参数位置问题。
2.3 CMake 工程化:把链接参数写进 CMakeLists
命令行能跑通之后,建议立刻转成 CMake,不然每换一台机器就要重敲一遍参数,出错概率极高。一份最小可用的CMakeLists.txt:
cmake_minimum_required(VERSION 3.16) project(gfx_demo CXX) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) # 头文件目录:只影响编译阶段 include_directories(${CMAKE_SOURCE_DIR}/include) # 库目录:只影响链接阶段 link_directories(${CMAKE_SOURCE_DIR}/lib) add_executable(gfx_demo src/main.cpp) # 私有链接:graphics 是主库,后面三个是它依赖的系统库 target_link_libraries(gfx_demo PRIVATE graphics gdi32 user32 imm32)逻辑说明:include_directories和link_directories是全局作用域的老写法,小工程够用;工程再大一点应该换成target_include_directories和target_link_libraries里的完整路径,避免污染其它 target。PRIVATE的意思是这个依赖不向使用gfx_demo的下游目标传递,对可执行文件来说永远该用PRIVATE。
一个容易忽略的点:link_directories在 CMake 里必须在add_executable之前调用才生效,而且不同生成器(Ninja 与 Visual Studio 生成器)对它的处理略有差异。如果 CMake 配置成功了但链接还是找不到库,直接把.lib的绝对路径写进target_link_libraries最稳:
target_link_libraries(gfx_demo PRIVATE ${CMAKE_SOURCE_DIR}/lib/graphics.lib gdi32 user32 imm32)绝对路径写法牺牲了一点可移植性,但换来的是排错时不用怀疑"是不是路径又没生效"。在把工程交给别人之前,我一般先保证它能跑,再考虑优雅。
3. 从 initgraph 到 FlushBatchDraw:一个不闪烁的绘制循环
挂接成功只是第一步,真正决定体验的是绘制循环怎么组织。这一章按"开窗口 → 画东西 → 刷新 → 收尾"的顺序走一遍,每一步都给出可跑的代码和参数含义。
3.1 initgraph 的三个参数与窗口坐标系
#include <graphics.h> int main() { // 640x480 的窗口,第三个参数是扩展标志位,一般传 0 或直接省略 initgraph(640, 480, 0); setbkcolor(WHITE); // 设置背景色,注意不会立即生效 cleardevice(); // 用当前背景色清屏,这一步才真正刷上去 setlinecolor(BLACK); line(0, 0, 639, 479); // 从左上角画到右下角 getchar(); // 停住窗口,等待一次输入 closegraph(); // 释放资源、关闭窗口 return 0; }参数说明:initgraph(width, height, flag)的前两个参数是客户区宽高,不含标题栏和边框,所以传 640×480 得到的窗口在屏幕上会比 640×480 略大。第三个参数在不同分支里含义不一致,有的版本用作样式标志,有的版本干脆重载成两参数版本——最可靠的做法是打开graphics.h看函数声明,别照着网上抄。setbkcolor只改内部状态,必须跟一次cleardevice()才能看到效果,这是新手最容易困惑的一处:明明设了背景色,窗口还是黑的。
坐标系默认原点在左上角,x 向右递增,y 向下递增,这跟数学课本的直角坐标系是反的。如果做函数图像、物理轨迹这类东西,setorigin(x, y)可以把原点挪到窗口中心,之后画的就是标准笛卡尔坐标:
setorigin(320, 240); // 原点移到窗口中心 setaspectratio(1, -1); // y 轴翻转,向上为正 line(-100, 0, 100, 0); // 现在画出来是一条水平线,y=0 在窗口正中setaspectratio(1, -1)里的-1是把 y 轴方向镜像过来。这两个函数必须成对使用,只调setorigin不调setaspectratio,图像会整体上下颠倒,画正弦曲线得到的是反的,排查起来很费时间。
3.2 颜色、线型与基本图元
颜色用RGB(r, g, b)宏自己算,也可以用预定义常量。线型通过setlinestyle控制,填充靠setfillcolor配fill*系列函数:
setlinecolor(RGB(0, 120, 215)); setlinestyle(PS_SOLID, 2); // 实线,线宽 2 像素 rectangle(50, 50, 250, 200); // 只描边,不填充 setfillcolor(RGB(255, 200, 0)); fillcircle(400, 150, 60); // 有边框有填充的圆 solidcircle(400, 300, 40); // 只有填充,无边框 // 多边形:先给点数组,再一次性画 POINT pts[3] = { {100, 400}, {200, 300}, {300, 400} }; setfillcolor(RGB(120, 200, 120)); fillpolygon(pts, 3);参数说明:setlinestyle(style, thickness)第一个参数是线型枚举,PS_SOLID实线、PS_DASH虚线、PS_DOT点线,第二个参数是线宽,单位像素,但虚线类线型在多数实现里线宽只能是 1,传 2 会静默退化成实线——这个行为没有报错,只能靠肉眼发现。fillpolygon(POINT*, int)第二个参数是顶点个数,数组必须至少这么多元素,越界不会崩但会画出诡异的形状。
rectangle和fillrectangle的区别值得单独记一句:前者只描边,后者只填充,想要"填充加描边"必须两个都调,顺序是先fillrectangle再rectangle,反过来边框会被填充色盖住一半。这个细节在画柱状图、按钮这类元素时天天用到。
3.3 双缓冲:BeginBatchDraw 三件套的调用时机
直接在循环里cleardevice()再逐个画图元,画面一定闪。原因是每次绘图调用都直接写到了窗口表面,清屏和重绘之间存在一个肉眼可见的时间窗。解决办法是批量绘制:
initgraph(800, 600); BeginBatchDraw(); // 开启批量模式,之后的绘制先写进内存缓冲 int x = 0; while (x < 800) { cleardevice(); // 清的是后台缓冲,用户看不见 setfillcolor(RGB(220, 60, 60)); solidcircle(x, 300, 30); FlushBatchDraw(); // 把整帧一次性贴到窗口上 x += 4; Sleep(16); // 约 60 帧/秒 } EndBatchDraw(); // 必须调用,否则缓冲资源不释放 closegraph();逻辑说明:BeginBatchDraw()之后所有绘制都落在离屏缓冲上,FlushBatchDraw()是唯一把缓冲内容提交到屏幕的动作,EndBatchDraw()收尾并释放缓冲。三者必须成对,只写前两个不写最后一个,程序退出时会残留资源;FlushBatchDraw放在循环里,一帧只调一次,调多次等于白开双缓冲。
一个血泪经验:FlushBatchDraw之后再调用cleardevice,清的是已经提交的画面,会造成单帧闪烁;正确顺序永远是"清屏 → 画完全部元素 → 提交"。如果画面里有一部分静态内容(比如坐标轴、标题栏)不想每帧重画,可以只清动态区域,用setfillcolor配solidrectangle覆盖旧位置,比整体清屏快得多。
3.4 关闭窗口与资源回收
closegraph()负责销毁窗口、释放 GDI 对象、恢复正常控制台。放在main的 return 之前,或者放在每个提前退出的分支里。如果程序里有return直接从循环中跳出而没走closegraph,窗口会残留到进程结束,调试时看到"关不掉的黑窗口"多半就是这个原因。
稳妥的写法是把绘图主体放进一个函数,initgraph和closegraph成对出现在同一个作用域内:
void run() { initgraph(800, 600); // ... 全部绘制逻辑 ... closegraph(); } int main() { run(); return 0; }这样即便中间抛异常,栈回退时也至少保证run里的局部对象被析构。严格来说closegraph不会因为异常自动执行,需要 RAII 包装才能做到万无一失,但对课程设计这个量级,成对写在一个函数里已经能挡住九成问题。
4. 交互接进来:鼠标键盘消息、文字渲染与帧率控制
画面动起来之后,下一步是能响应操作。这一章讲三个东西:怎么取消息、怎么写中文、怎么控制节奏。三者叠在一起,才是一个"能演示"的程序。
4.1 getmessage 与 peekmessage 的区别
这类图形库提供两套取消息的方式,行为完全不同,选错了程序要么卡死要么狂占 CPU。
ExMessage msg; // 阻塞式:没有消息就停在这里等,适合"等用户点一下再继续"的场景 while (getmessage(&msg)) { if (msg.message == WM_LBUTTONDOWN) { // msg.x / msg.y 是相对窗口客户区的坐标 setfillcolor(RGB(0, 160, 220)); solidcircle(msg.x, msg.y, 20); FlushBatchDraw(); } if (msg.message == WM_KEYDOWN && msg.vkcode == VK_ESCAPE) break; // ESC 退出 }getmessage在无消息时会阻塞,所以它可以直接当主循环用——程序闲下来就停在那里,CPU 占用接近零。代价是动画没法做,因为一旦进到这个函数就不返回,除非有输入。
需要边动画边响应,就得换成非阻塞的:
int x = 0; BeginBatchDraw(); while (true) { // 先把消息队列抽干,一条不留 while (peekmessage(&msg)) { if (msg.message == WM_KEYDOWN && msg.vkcode == VK_ESCAPE) goto done; if (msg.message == WM_LBUTTONDOWN) x = msg.x; // 点哪儿球就跳到哪儿 } cleardevice(); setfillcolor(RGB(220, 60, 60)); solidcircle(x, 300, 30); FlushBatchDraw(); x = (x + 3) % 800; Sleep(16); } done: EndBatchDraw(); closegraph();参数说明:peekmessage(&msg)取一条消息就返回,队列空时返回 false,不阻塞;getmessage(&msg)队列空时挂起等待。两个函数都可以带第二个参数做消息过滤,比如只取鼠标消息传EX_MOUSE,只取键盘传EX_KEY,过滤能减少无谓的判断。注意:peekmessage的 while 循环必须放在每帧开头并且抽干,如果只取一条就开始画,消息会积压,表现为"点了很多下只有第一下生效"。
ExMessage结构里的字段要看头文件确认,常用的是message(消息类型)、xy(鼠标坐标)、lbuttonrbutton(按键状态)、vkcode(虚拟键码)、wheel(滚轮增量)。
4.2 文字输出:字体、对齐与中文乱码
写字用outtextxy,但字体、大小、颜色都要提前设:
settextcolor(RGB(30, 30, 30)); setbkmode(TRANSPARENT); // 背景透明,否则文字带一块底色 settextstyle(24, 0, L"微软雅黑"); // 高度 24,宽度自适应 outtextxy(20, 20, L"帧率: 60 FPS"); outtextxy(20, 60, L"点击画布放置圆点");参数说明:settextstyle(height, width, face)的height是字符高度(像素),width传 0 表示按字体比例自适应,传具体值会强制拉伸字形导致变形,一般不要传。第三个参数是字体名,必须是宽字符串,写成"微软雅黑"在某些配置下会编译失败。setbkmode(TRANSPARENT)这一行几乎不能省,默认是不透明模式,文字背后会带一块背景色方块,叠在图形上非常难看。
中文乱码是这一类项目的高频故障,根源是字符集。源文件存成 UTF-8,编译器按 GBK 解析,L"中文"就变成一堆问号。处理办法有两条:要么统一源文件编码和编译器编码(MSVC 加/utf-8,g++ 加-finput-charset=UTF-8 -fexec-charset=UTF-8),要么干脆把界面文字换成英文。赶工期的时候,第二条路的性价比往往更高。
4.3 帧率:Sleep 掐时间还是固定步长
上一节的Sleep(16)是最朴素的限帧手段,它的问题是精度差——Windows 下Sleep的实际休眠时间通常比请求值多 1 到 15 毫秒,取决于系统计时器分辨率。跑到 60 帧勉强够,想要稳定 30 帧以上又不想画面抖动,得自己算时间差:
#include <chrono> using namespace std::chrono; auto last = steady_clock::now(); double acc = 0.0; while (true) { auto now = steady_clock::now(); double dt = duration<double>(now - last).count(); last = now; acc += dt; // 逻辑更新用真实时间差,不受帧率波动影响 // 这里省略具体更新,重点是 dt 这个量 cleardevice(); // ... 绘制 ... FlushBatchDraw(); // 目标 16.6ms 一帧,超了就补,没超就等 auto cost = duration<double>(steady_clock::now() - now).count(); if (cost < 0.0166) Sleep(static_cast<DWORD>((0.0166 - cost) * 1000)); }逻辑说明:用steady_clock而不是system_clock,因为后者会被系统时间调整影响,做动画计时时必须单调递增。dt是上一帧到这一帧的真实秒数,所有跟时间相关的量(位移、旋转角度)都乘以它,这样帧率掉下去动画速度也不会变——这就是所谓"帧率无关"。很多课设里小球在别人电脑上跑得飞快、在自己电脑上慢吞吞,就是位移写成了每帧固定值。
注意:Sleep(0)不等于不睡,它会让出当前时间片,在紧循环里反而可能比Sleep(1)更耗 CPU。真正想跑满帧就别睡,靠垂直同步或者手动判断时间差。
5. 链接与运行排查:五条最常见翻车记录
这一章全是现象描述,按"看到什么 → 为什么 → 怎么办"写。这些坑我在不同机器、不同编译器上反复遇到过,顺序基本按出现频率排。
5.1 现象:满屏未解析的外部符号,函数名自己没写过
原因:链接器只拿到了graphics.lib,没拿到它依赖的系统库。图形接口内部调用了 GDI、user32 的函数,这些函数在系统库的导入库里,不显式链接就找不到。
解决:按报错信息里的函数名前缀判断缺哪个库——带Gdi、CreatePen、BitBlt的补gdi32;带CreateWindow、GetMessage、RegisterClass的补user32;带Imm的补imm32。CMake 里就是target_link_libraries那一行加上去,命令行就是-lgdi32 -luser32 -limm32。还有一种情况是所有函数都报未解析,那说明.lib根本没被链接上,检查-L路径和库名拼写。
5.2 现象:双击 exe,窗口一闪就没了
原因:main执行完直接返回了,initgraph之后的绘制还没来得及显示,进程就退出了。或者更隐蔽的一种——.lib是导入库,运行时找不到配套的.dll,加载阶段就失败,窗口创建都来不及。
解决:先确认代码里有没有让程序停住的东西,最土但有效的办法是在closegraph()前加一句getchar()或者在主循环里加一个等待 ESC 的分支。排除代码原因后,把.dll拷到.exe同目录再试。判断是不是 DLL 缺失,可以打开命令行在 exe 目录下运行一次,缺失时系统会弹一个明确的错误框。
5.3 现象:画面剧烈闪烁,尤其是有大量图元的时候
原因:没有开双缓冲,或者开了但调用顺序错了。还有一种情况是每帧都在setbkcolor之后再cleardevice,等于每帧多刷一次全屏。
解决:确认BeginBatchDraw在循环外调一次,FlushBatchDraw在循环内调一次,EndBatchDraw在退出前调一次。再检查清屏和绘制的顺序,必须是"清 → 画 → 提交"。如果图元数量上千,光靠双缓冲也不够,得减少每帧重绘面积,把静态部分画到一张内存图像上反复贴。
5.4 现象:中文显示成乱码、问号或者方框
原因:三种可能,一是源文件编码和编译器输入编码不一致;二是用了窄字符版本的输出函数却传了中文;三是字体名指定的字体系统里没有,回退到默认字体后中文缺字形。
解决:先统一编码,源文件保存为 UTF-8,编译加/utf-8或-finput-charset=UTF-8;再确认outtextxy的字符串是L"..."宽字符形式;最后换一个确定存在的中文字体,或者干脆先用英文把功能跑通,再回头处理字体问题。方框和问号要区分开——问号是编码问题,方框是字体缺字形,两种故障的修法完全不同。
5.5 现象:32 位配置能编过,切到 64 位链接失败
原因:.lib是 32 位的,跟 64 位的编译目标架构不匹配。静态库对架构敏感,位数不一致时链接器会直接报"模块计算机类型不匹配"。
解决:把工程配置切回 x86(32 位)再编。如果非要 64 位,就得找对应架构的库文件,没有的话只能改配置——在项目设置里把目标平台改成 Win32,CMake 里就是cmake -A Win32 ..。这个错在换电脑、换 IDE 默认配置时特别容易出现,因为很多模板默认建的是 64 位工程。
提示:排错时先看报错发生在哪个阶段。编译阶段报错是头文件和语法问题,链接阶段报错是库和架构问题,运行阶段崩溃是资源和逻辑问题。三个阶段混在一起猜,效率会低很多。
6. 验收一个绘图 Demo:三条线跑通再交付
写完代码不等于能交。我习惯用一个最小验收清单过一遍,三条线:绘制、交互、帧率,每条都有可观察的判据,不靠感觉。
| 检查项 | 判据 | 不合格的典型表现 |
|---|---|---|
| 绘制完整性 | 静态元素每帧都在正确位置,无残留 | 拖影、旧图元没被覆盖 |
| 双缓冲生效 | 快速移动的物体边缘干净 | 边缘有锯齿状撕裂 |
| 交互响应 | 连续点击十次,十次都有反应 | 只响应第一次 |
| 退出路径 | ESC、点关闭按钮、Alt+F4 都能正常退 | 进程残留、黑窗口关不掉 |
| 独立运行 | exe 拷到空目录双击能起 | 换机器就报缺文件 |
| 中文显示 | 标题、按钮文字无问号无方框 | 全是??? |
验收里最容易被跳过的是"独立运行"这一条。在自己机器上一切正常,是因为开发环境的 PATH 里恰好有需要的 DLL,而别人机器上没有。我的做法是每次改完代码,先把build目录里的 exe 单独拷到桌面一个空文件夹,双击跑一遍,能起来再继续。这一步花不了一分钟,但拦下过好几次交付前才发现的问题。
再往上一层,如果你的 Demo 要交给别人继续改,建议把窗口尺寸、颜色、帧间隔这些数字全部提到文件头部做成常量,别散落在代码中间:
const int WIN_W = 800; const int WIN_H = 600; const int FRAME_MS = 16; // 目标帧间隔 const COLORREF BG = RGB(245, 245, 245); const COLORREF FG = RGB(220, 60, 60);这样别人调参不用满文件搜索数字,也避免改了一处漏了另一处。看起来是小事,但一个散落着魔数的绘图程序,接手的人第一件事就是重写。
最后说一个我自己的习惯。凡是用这类库做的东西,我一定会在main的第一行加一句日志输出,把窗口尺寸和库的路径打出来;closegraph之前再打一句"正常退出"。听着很笨,但程序闪退时这两行输出能立刻区分"根本没进主流程"和"跑到一半崩了",比开调试器快。从那以后我每次配新环境,都强制走一遍"命令行编译 → 空目录双击 → 看两行日志"的流程,没再出现过交付当天才发现链接不上这种事。希望帮到你。
本文还有配套的精品资源,点击获取