首页 / 资讯中心 / 文章详情

C++ graphics.h/graphics.lib 绘图与双缓冲实战

C++ graphics.h/graphics.lib 绘图与双缓冲实战 ★ FEATURED ARTICLE
简介这份压缩包面向学习 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 \ -stdc17 -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:c17 /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.txtcmake_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); // 现在画出来是一条水平线y0 在窗口正中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-8g 加-finput-charsetUTF-8 -fexec-charsetUTF-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 durationdouble(now - last).count(); last now; acc dt; // 逻辑更新用真实时间差不受帧率波动影响 // 这里省略具体更新重点是 dt 这个量 cleardevice(); // ... 绘制 ... FlushBatchDraw(); // 目标 16.6ms 一帧超了就补没超就等 auto cost durationdouble(steady_clock::now() - now).count(); if (cost 0.0166) Sleep(static_castDWORD((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-charsetUTF-8再确认outtextxy的字符串是L...宽字符形式最后换一个确定存在的中文字体或者干脆先用英文把功能跑通再回头处理字体问题。方框和问号要区分开——问号是编码问题方框是字体缺字形两种故障的修法完全不同。5.5 现象32 位配置能编过切到 64 位链接失败原因.lib是 32 位的跟 64 位的编译目标架构不匹配。静态库对架构敏感位数不一致时链接器会直接报模块计算机类型不匹配。解决把工程配置切回 x8632 位再编。如果非要 64 位就得找对应架构的库文件没有的话只能改配置——在项目设置里把目标平台改成 Win32CMake 里就是cmake -A Win32 ..。这个错在换电脑、换 IDE 默认配置时特别容易出现因为很多模板默认建的是 64 位工程。提示排错时先看报错发生在哪个阶段。编译阶段报错是头文件和语法问题链接阶段报错是库和架构问题运行阶段崩溃是资源和逻辑问题。三个阶段混在一起猜效率会低很多。6. 验收一个绘图 Demo三条线跑通再交付写完代码不等于能交。我习惯用一个最小验收清单过一遍三条线绘制、交互、帧率每条都有可观察的判据不靠感觉。检查项判据不合格的典型表现绘制完整性静态元素每帧都在正确位置无残留拖影、旧图元没被覆盖双缓冲生效快速移动的物体边缘干净边缘有锯齿状撕裂交互响应连续点击十次十次都有反应只响应第一次退出路径ESC、点关闭按钮、AltF4 都能正常退进程残留、黑窗口关不掉独立运行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之前再打一句正常退出。听着很笨但程序闪退时这两行输出能立刻区分根本没进主流程和跑到一半崩了比开调试器快。从那以后我每次配新环境都强制走一遍命令行编译 → 空目录双击 → 看两行日志的流程没再出现过交付当天才发现链接不上这种事。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站