戰(zhàn))
簡(jiǎn)介這份壓縮包面向?qū)W習(xí) C 圖形繪制的初學(xué)者、課程實(shí)驗(yàn)者以及需要補(bǔ)全 BGI 圖形庫(kù)環(huán)境的開(kāi)發(fā)者提供 graphics.h 頭文件與 graphics.lib 庫(kù)文件的配套組合用于解決編譯鏈接階段缺少圖形接口聲明和預(yù)編譯實(shí)現(xiàn)的問(wèn)題并適配常見(jiàn)的 Windows 編譯環(huán)境。包內(nèi)共 2 個(gè)文件分別為 1 個(gè) .h 頭文件和 1 個(gè) .lib 庫(kù)文件整體約 11KB體積輕量、類(lèi)型簡(jiǎn)潔便于直接放入項(xiàng)目目錄并按需配置包含路徑與鏈接依賴(lài)。已有 3591 人學(xué)習(xí)或下載說(shuō)明其在 C 圖形編程入門(mén)場(chǎng)景中具有一定參考價(jià)值。頭文件可用于了解窗口初始化、坐標(biāo)設(shè)置、基本圖形繪制、顏色管理、文本輸出及事件處理等接口聲明庫(kù)文件則承載對(duì)應(yīng)預(yù)編譯實(shí)現(xiàn)配合 initgraph、rectangle、line、outtextxy 等典型調(diào)用讀者可據(jù)此搭建簡(jiǎn)單的圖形窗口與繪圖示例理解圖形庫(kù)的鏈接方式、基礎(chǔ) API 使用思路以及圖形會(huì)話(huà)的初始化與關(guān)閉流程。1. 一個(gè) .rar 里的 graphics.lib把 C 畫(huà)圖從控制臺(tái)黑框拉到能看見(jiàn)的窗口第一次做 C 課程設(shè)計(jì)的人卡點(diǎn)往往不在算法而在屏幕上怎么出現(xiàn)一條線(xiàn)??刂婆_(tái)只能 printf 字符換到框架又要先配一整套工程兩周工期根本不夠折騰。這個(gè)下載包里最核心的就是兩樣?xùn)|西一個(gè)graphics.h頭文件和一個(gè)graphics.lib靜態(tài)庫(kù)接口是 BGI 風(fēng)格的老派圖形 API——initgraph開(kāi)窗口、line畫(huà)線(xiàn)、outtextxy寫(xiě)字、getmessage收鼠標(biāo)鍵盤(pán)事件一個(gè)main函數(shù)就能跑出帶界面的結(jié)果。它適合趕課設(shè)、做圖形學(xué)入門(mén)作業(yè)、寫(xiě)小型可視化 Demo 的人也適合想把繪制循環(huán)攥在自己手里、而不是被框架回調(diào)牽著走的熟手。下面按拆包、掛接、繪圖循環(huán)、交互、排錯(cuò)的順序把它拆成能照著復(fù)現(xiàn)的步驟。2. 拆包與掛接graphics.h 與 graphics.lib 在工程里怎么落地先把選型理由說(shuō)清楚不然后面每一步都會(huì)覺(jué)得別扭。這套東西本質(zhì)上是一個(gè)薄封裝它把 Windows 下的窗口創(chuàng)建、GDI 繪圖調(diào)用、消息循環(huán)壓縮成十幾個(gè)函數(shù)用一套過(guò)程式的 API 暴露出來(lái)。相比直接寫(xiě) Win32 SDK你省掉兩百行窗口過(guò)程代碼相比上 Qt 或其它框架你省掉編譯一整棵依賴(lài)樹(shù)的時(shí)間。代價(jià)是它的能力邊界很窄——沒(méi)有控件、沒(méi)有布局、沒(méi)有硬件加速畫(huà)幾千個(gè)圖元還行做動(dòng)畫(huà)粒子多了就會(huì)掉幀。判斷標(biāo)準(zhǔn)很簡(jiǎn)單你要的是屏幕上能畫(huà)、能點(diǎn)、能動(dòng)選它你要的是做一個(gè)正經(jīng)桌面軟件別選它。2.1 目錄怎么擺include 與 lib 分離我不喜歡把.h和.lib跟源碼混在一個(gè)目錄里工程大了以后自己都分不清哪個(gè)頭文件是第三方帶進(jìn)來(lái)的。常見(jiàn)做法是按下圖這種三層結(jié)構(gòu)擺放源碼、頭文件、庫(kù)文件各歸各位project/ ├── include/ │ └── graphics.h # 第三方頭文件只讀不放源碼 ├── lib/ │ └── graphics.lib # 靜態(tài)庫(kù)或?qū)霂?kù) ├── src/ │ └── main.cpp └── CMakeLists.txt這樣擺的好處是編譯器的-I和鏈接器的-L各指一個(gè)目錄換機(jī)器或者打包給別人時(shí)路徑規(guī)則不用改。參數(shù)說(shuō)明include/對(duì)應(yīng)編譯器頭文件搜索路徑lib/對(duì)應(yīng)鏈接器庫(kù)搜索路徑兩者不能混用——把頭文件丟進(jìn)lib/目錄編譯器不會(huì)去那里找.h報(bào)的錯(cuò)是fatal error: graphics.h: No such file or directory很多人第一反應(yīng)是庫(kù)壞了其實(shí)只是路徑?jīng)]指對(duì)。還有一件事必須先確認(rèn)這個(gè).lib到底是靜態(tài)庫(kù)還是導(dǎo)入庫(kù)。判斷方法很笨但有效——如果壓縮包里除了.lib還有一個(gè)同名的.dll那.lib多半是導(dǎo)入庫(kù)程序運(yùn)行時(shí)必須把.dll拷到.exe同目錄如果只有.lib那就是靜態(tài)庫(kù)鏈接進(jìn)去就完事產(chǎn)物是單個(gè)可執(zhí)行文件拷走就能跑。這一步?jīng)]搞清后面會(huì)出現(xiàn)我這邊能跑交上去老師那邊雙擊沒(méi)反應(yīng)的經(jīng)典事故。提示靜態(tài)庫(kù)和導(dǎo)入庫(kù)的鏈接參數(shù)完全一樣區(qū)別只在運(yùn)行期。交付前把.exe單獨(dú)拷到一個(gè)空目錄雙擊一次能起來(lái)才算真的沒(méi)問(wèn)題。2.2 命令行鏈接g 與 MSVC 兩套寫(xiě)法先用命令行編譯一次比直接開(kāi) IDE 更能看清到底鏈接了什么。g 這一套MinGW-w64 環(huán)境大致是這樣g src/main.cpp -o demo.exe \ -I./include \ -L./lib \ -lgraphics -lgdi32 -luser32 -limm32 \ -stdc17 -municode逐項(xiàng)說(shuō)明-I./include告訴編譯器去哪找graphics.h-L./lib告訴鏈接器去哪找graphics.lib-lgraphics是庫(kù)名注意 g 的規(guī)則是-l后面跟去掉lib前綴和.lib/.a后綴的名字所以graphics.lib對(duì)應(yīng)-lgraphics寫(xiě)成-lgraphics.lib會(huì)直接報(bào)找不到。-lgdi32 -luser32 -limm32是這套圖形接口在底層依賴(lài)的系統(tǒng)庫(kù)GDI 負(fù)責(zé)實(shí)際繪制user32 負(fù)責(zé)窗口和消息imm32 負(fù)責(zé)輸入法消息。漏掉其中任何一個(gè)報(bào)錯(cuò)都是未定義引用且錯(cuò)誤信息里出現(xiàn)的函數(shù)名你根本沒(méi)在自己代碼里寫(xiě)過(guò)——這是最容易被誤判成庫(kù)版本不對(duì)的一類(lèi)問(wèn)題。-municode只在頭文件聲明的是寬字符版入口時(shí)加加了還報(bào)undefined reference to WinMain就把這個(gè)參數(shù)去掉再試。MSVC 的cl寫(xiě)法邏輯一樣語(yǔ)法不同cl /EHsc /std:c17 /I include src\main.cpp ^ /link /LIBPATH:lib graphics.lib gdi32.lib user32.lib imm32.lib注意/I和/link的分界/I屬于編譯器選項(xiàng)/link之后全部交給鏈接器/LIBPATH:是鏈接器的庫(kù)搜索路徑。把graphics.lib寫(xiě)在/link前面cl會(huì)把它當(dāng)源文件處理報(bào)cannot open input file。這個(gè)坑我在第一次配環(huán)境時(shí)踩過(guò)排查了半小時(shí)才發(fā)現(xiàn)是參數(shù)位置問(wèn)題。2.3 CMake 工程化把鏈接參數(shù)寫(xiě)進(jìn) CMakeLists命令行能跑通之后建議立刻轉(zhuǎn)成 CMake不然每換一臺(tái)機(jī)器就要重敲一遍參數(shù)出錯(cuò)概率極高。一份最小可用的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) # 庫(kù)目錄只影響鏈接階段 link_directories(${CMAKE_SOURCE_DIR}/lib) add_executable(gfx_demo src/main.cpp) # 私有鏈接graphics 是主庫(kù)后面三個(gè)是它依賴(lài)的系統(tǒng)庫(kù) target_link_libraries(gfx_demo PRIVATE graphics gdi32 user32 imm32)邏輯說(shuō)明include_directories和link_directories是全局作用域的老寫(xiě)法小工程夠用工程再大一點(diǎn)應(yīng)該換成target_include_directories和target_link_libraries里的完整路徑避免污染其它 target。PRIVATE的意思是這個(gè)依賴(lài)不向使用gfx_demo的下游目標(biāo)傳遞對(duì)可執(zhí)行文件來(lái)說(shuō)永遠(yuǎn)該用PRIVATE。一個(gè)容易忽略的點(diǎn)link_directories在 CMake 里必須在add_executable之前調(diào)用才生效而且不同生成器Ninja 與 Visual Studio 生成器對(duì)它的處理略有差異。如果 CMake 配置成功了但鏈接還是找不到庫(kù)直接把.lib的絕對(duì)路徑寫(xiě)進(jìn)target_link_libraries最穩(wěn)target_link_libraries(gfx_demo PRIVATE ${CMAKE_SOURCE_DIR}/lib/graphics.lib gdi32 user32 imm32)絕對(duì)路徑寫(xiě)法犧牲了一點(diǎn)可移植性但換來(lái)的是排錯(cuò)時(shí)不用懷疑是不是路徑又沒(méi)生效。在把工程交給別人之前我一般先保證它能跑再考慮優(yōu)雅。3. 從 initgraph 到 FlushBatchDraw一個(gè)不閃爍的繪制循環(huán)掛接成功只是第一步真正決定體驗(yàn)的是繪制循環(huán)怎么組織。這一章按開(kāi)窗口 → 畫(huà)東西 → 刷新 → 收尾的順序走一遍每一步都給出可跑的代碼和參數(shù)含義。3.1 initgraph 的三個(gè)參數(shù)與窗口坐標(biāo)系#include graphics.h int main() { // 640x480 的窗口第三個(gè)參數(shù)是擴(kuò)展標(biāo)志位一般傳 0 或直接省略 initgraph(640, 480, 0); setbkcolor(WHITE); // 設(shè)置背景色注意不會(huì)立即生效 cleardevice(); // 用當(dāng)前背景色清屏這一步才真正刷上去 setlinecolor(BLACK); line(0, 0, 639, 479); // 從左上角畫(huà)到右下角 getchar(); // 停住窗口等待一次輸入 closegraph(); // 釋放資源、關(guān)閉窗口 return 0; }參數(shù)說(shuō)明initgraph(width, height, flag)的前兩個(gè)參數(shù)是客戶(hù)區(qū)寬高不含標(biāo)題欄和邊框所以傳 640×480 得到的窗口在屏幕上會(huì)比 640×480 略大。第三個(gè)參數(shù)在不同分支里含義不一致有的版本用作樣式標(biāo)志有的版本干脆重載成兩參數(shù)版本——最可靠的做法是打開(kāi)graphics.h看函數(shù)聲明別照著網(wǎng)上抄。setbkcolor只改內(nèi)部狀態(tài)必須跟一次cleardevice()才能看到效果這是新手最容易困惑的一處明明設(shè)了背景色窗口還是黑的。坐標(biāo)系默認(rèn)原點(diǎn)在左上角x 向右遞增y 向下遞增這跟數(shù)學(xué)課本的直角坐標(biāo)系是反的。如果做函數(shù)圖像、物理軌跡這類(lèi)東西setorigin(x, y)可以把原點(diǎn)挪到窗口中心之后畫(huà)的就是標(biāo)準(zhǔn)笛卡爾坐標(biāo)setorigin(320, 240); // 原點(diǎn)移到窗口中心 setaspectratio(1, -1); // y 軸翻轉(zhuǎn)向上為正 line(-100, 0, 100, 0); // 現(xiàn)在畫(huà)出來(lái)是一條水平線(xiàn)y0 在窗口正中setaspectratio(1, -1)里的-1是把 y 軸方向鏡像過(guò)來(lái)。這兩個(gè)函數(shù)必須成對(duì)使用只調(diào)setorigin不調(diào)setaspectratio圖像會(huì)整體上下顛倒畫(huà)正弦曲線(xiàn)得到的是反的排查起來(lái)很費(fèi)時(shí)間。3.2 顏色、線(xiàn)型與基本圖元顏色用RGB(r, g, b)宏自己算也可以用預(yù)定義常量。線(xiàn)型通過(guò)setlinestyle控制填充靠setfillcolor配fill*系列函數(shù)setlinecolor(RGB(0, 120, 215)); setlinestyle(PS_SOLID, 2); // 實(shí)線(xiàn)線(xiàn)寬 2 像素 rectangle(50, 50, 250, 200); // 只描邊不填充 setfillcolor(RGB(255, 200, 0)); fillcircle(400, 150, 60); // 有邊框有填充的圓 solidcircle(400, 300, 40); // 只有填充無(wú)邊框 // 多邊形先給點(diǎn)數(shù)組再一次性畫(huà) POINT pts[3] { {100, 400}, {200, 300}, {300, 400} }; setfillcolor(RGB(120, 200, 120)); fillpolygon(pts, 3);參數(shù)說(shuō)明setlinestyle(style, thickness)第一個(gè)參數(shù)是線(xiàn)型枚舉PS_SOLID實(shí)線(xiàn)、PS_DASH虛線(xiàn)、PS_DOT點(diǎn)線(xiàn)第二個(gè)參數(shù)是線(xiàn)寬單位像素但虛線(xiàn)類(lèi)線(xiàn)型在多數(shù)實(shí)現(xiàn)里線(xiàn)寬只能是 1傳 2 會(huì)靜默退化成實(shí)線(xiàn)——這個(gè)行為沒(méi)有報(bào)錯(cuò)只能靠肉眼發(fā)現(xiàn)。fillpolygon(POINT*, int)第二個(gè)參數(shù)是頂點(diǎn)個(gè)數(shù)數(shù)組必須至少這么多元素越界不會(huì)崩但會(huì)畫(huà)出詭異的形狀。rectangle和fillrectangle的區(qū)別值得單獨(dú)記一句前者只描邊后者只填充想要填充加描邊必須兩個(gè)都調(diào)順序是先f(wàn)illrectangle再rectangle反過(guò)來(lái)邊框會(huì)被填充色蓋住一半。這個(gè)細(xì)節(jié)在畫(huà)柱狀圖、按鈕這類(lèi)元素時(shí)天天用到。3.3 雙緩沖BeginBatchDraw 三件套的調(diào)用時(shí)機(jī)直接在循環(huán)里cleardevice()再逐個(gè)畫(huà)圖元畫(huà)面一定閃。原因是每次繪圖調(diào)用都直接寫(xiě)到了窗口表面清屏和重繪之間存在一個(gè)肉眼可見(jiàn)的時(shí)間窗。解決辦法是批量繪制initgraph(800, 600); BeginBatchDraw(); // 開(kāi)啟批量模式之后的繪制先寫(xiě)進(jìn)內(nèi)存緩沖 int x 0; while (x 800) { cleardevice(); // 清的是后臺(tái)緩沖用戶(hù)看不見(jiàn) setfillcolor(RGB(220, 60, 60)); solidcircle(x, 300, 30); FlushBatchDraw(); // 把整幀一次性貼到窗口上 x 4; Sleep(16); // 約 60 幀/秒 } EndBatchDraw(); // 必須調(diào)用否則緩沖資源不釋放 closegraph();邏輯說(shuō)明BeginBatchDraw()之后所有繪制都落在離屏緩沖上FlushBatchDraw()是唯一把緩沖內(nèi)容提交到屏幕的動(dòng)作EndBatchDraw()收尾并釋放緩沖。三者必須成對(duì)只寫(xiě)前兩個(gè)不寫(xiě)最后一個(gè)程序退出時(shí)會(huì)殘留資源FlushBatchDraw放在循環(huán)里一幀只調(diào)一次調(diào)多次等于白開(kāi)雙緩沖。一個(gè)血淚經(jīng)驗(yàn)FlushBatchDraw之后再調(diào)用cleardevice清的是已經(jīng)提交的畫(huà)面會(huì)造成單幀閃爍正確順序永遠(yuǎn)是清屏 → 畫(huà)完全部元素 → 提交。如果畫(huà)面里有一部分靜態(tài)內(nèi)容比如坐標(biāo)軸、標(biāo)題欄不想每幀重畫(huà)可以只清動(dòng)態(tài)區(qū)域用setfillcolor配solidrectangle覆蓋舊位置比整體清屏快得多。3.4 關(guān)閉窗口與資源回收closegraph()負(fù)責(zé)銷(xiāo)毀窗口、釋放 GDI 對(duì)象、恢復(fù)正??刂婆_(tái)。放在main的 return 之前或者放在每個(gè)提前退出的分支里。如果程序里有return直接從循環(huán)中跳出而沒(méi)走closegraph窗口會(huì)殘留到進(jìn)程結(jié)束調(diào)試時(shí)看到關(guān)不掉的黑窗口多半就是這個(gè)原因。穩(wěn)妥的寫(xiě)法是把繪圖主體放進(jìn)一個(gè)函數(shù)initgraph和closegraph成對(duì)出現(xiàn)在同一個(gè)作用域內(nèi)void run() { initgraph(800, 600); // ... 全部繪制邏輯 ... closegraph(); } int main() { run(); return 0; }這樣即便中間拋異常?;赝藭r(shí)也至少保證run里的局部對(duì)象被析構(gòu)。嚴(yán)格來(lái)說(shuō)closegraph不會(huì)因?yàn)楫惓W詣?dòng)執(zhí)行需要 RAII 包裝才能做到萬(wàn)無(wú)一失但對(duì)課程設(shè)計(jì)這個(gè)量級(jí)成對(duì)寫(xiě)在一個(gè)函數(shù)里已經(jīng)能擋住九成問(wèn)題。4. 交互接進(jìn)來(lái)鼠標(biāo)鍵盤(pán)消息、文字渲染與幀率控制畫(huà)面動(dòng)起來(lái)之后下一步是能響應(yīng)操作。這一章講三個(gè)東西怎么取消息、怎么寫(xiě)中文、怎么控制節(jié)奏。三者疊在一起才是一個(gè)能演示的程序。4.1 getmessage 與 peekmessage 的區(qū)別這類(lèi)圖形庫(kù)提供兩套取消息的方式行為完全不同選錯(cuò)了程序要么卡死要么狂占 CPU。ExMessage msg; // 阻塞式?jīng)]有消息就停在這里等適合等用戶(hù)點(diǎn)一下再繼續(xù)的場(chǎng)景 while (getmessage(msg)) { if (msg.message WM_LBUTTONDOWN) { // msg.x / msg.y 是相對(duì)窗口客戶(hù)區(qū)的坐標(biāo) setfillcolor(RGB(0, 160, 220)); solidcircle(msg.x, msg.y, 20); FlushBatchDraw(); } if (msg.message WM_KEYDOWN msg.vkcode VK_ESCAPE) break; // ESC 退出 }getmessage在無(wú)消息時(shí)會(huì)阻塞所以它可以直接當(dāng)主循環(huán)用——程序閑下來(lái)就停在那里CPU 占用接近零。代價(jià)是動(dòng)畫(huà)沒(méi)法做因?yàn)橐坏┻M(jìn)到這個(gè)函數(shù)就不返回除非有輸入。需要邊動(dòng)畫(huà)邊響應(yīng)就得換成非阻塞的int x 0; BeginBatchDraw(); while (true) { // 先把消息隊(duì)列抽干一條不留 while (peekmessage(msg)) { if (msg.message WM_KEYDOWN msg.vkcode VK_ESCAPE) goto done; if (msg.message WM_LBUTTONDOWN) x msg.x; // 點(diǎn)哪兒球就跳到哪兒 } cleardevice(); setfillcolor(RGB(220, 60, 60)); solidcircle(x, 300, 30); FlushBatchDraw(); x (x 3) % 800; Sleep(16); } done: EndBatchDraw(); closegraph();參數(shù)說(shuō)明peekmessage(msg)取一條消息就返回隊(duì)列空時(shí)返回 false不阻塞getmessage(msg)隊(duì)列空時(shí)掛起等待。兩個(gè)函數(shù)都可以帶第二個(gè)參數(shù)做消息過(guò)濾比如只取鼠標(biāo)消息傳EX_MOUSE只取鍵盤(pán)傳EX_KEY過(guò)濾能減少無(wú)謂的判斷。注意peekmessage的 while 循環(huán)必須放在每幀開(kāi)頭并且抽干如果只取一條就開(kāi)始畫(huà)消息會(huì)積壓表現(xiàn)為點(diǎn)了很多下只有第一下生效。ExMessage結(jié)構(gòu)里的字段要看頭文件確認(rèn)常用的是message消息類(lèi)型、xy鼠標(biāo)坐標(biāo)、lbuttonrbutton按鍵狀態(tài)、vkcode虛擬鍵碼、wheel滾輪增量。4.2 文字輸出字體、對(duì)齊與中文亂碼寫(xiě)字用outtextxy但字體、大小、顏色都要提前設(shè)settextcolor(RGB(30, 30, 30)); setbkmode(TRANSPARENT); // 背景透明否則文字帶一塊底色 settextstyle(24, 0, L微軟雅黑); // 高度 24寬度自適應(yīng) outtextxy(20, 20, L幀率: 60 FPS); outtextxy(20, 60, L點(diǎn)擊畫(huà)布放置圓點(diǎn));參數(shù)說(shuō)明settextstyle(height, width, face)的height是字符高度像素width傳 0 表示按字體比例自適應(yīng)傳具體值會(huì)強(qiáng)制拉伸字形導(dǎo)致變形一般不要傳。第三個(gè)參數(shù)是字體名必須是寬字符串寫(xiě)成微軟雅黑在某些配置下會(huì)編譯失敗。setbkmode(TRANSPARENT)這一行幾乎不能省默認(rèn)是不透明模式文字背后會(huì)帶一塊背景色方塊疊在圖形上非常難看。中文亂碼是這一類(lèi)項(xiàng)目的高頻故障根源是字符集。源文件存成 UTF-8編譯器按 GBK 解析L中文就變成一堆問(wèn)號(hào)。處理辦法有兩條要么統(tǒng)一源文件編碼和編譯器編碼MSVC 加/utf-8g 加-finput-charsetUTF-8 -fexec-charsetUTF-8要么干脆把界面文字換成英文。趕工期的時(shí)候第二條路的性?xún)r(jià)比往往更高。4.3 幀率Sleep 掐時(shí)間還是固定步長(zhǎng)上一節(jié)的Sleep(16)是最樸素的限幀手段它的問(wèn)題是精度差——Windows 下Sleep的實(shí)際休眠時(shí)間通常比請(qǐng)求值多 1 到 15 毫秒取決于系統(tǒng)計(jì)時(shí)器分辨率。跑到 60 幀勉強(qiáng)夠想要穩(wěn)定 30 幀以上又不想畫(huà)面抖動(dòng)得自己算時(shí)間差#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; // 邏輯更新用真實(shí)時(shí)間差不受幀率波動(dòng)影響 // 這里省略具體更新重點(diǎn)是 dt 這個(gè)量 cleardevice(); // ... 繪制 ... FlushBatchDraw(); // 目標(biāo) 16.6ms 一幀超了就補(bǔ)沒(méi)超就等 auto cost durationdouble(steady_clock::now() - now).count(); if (cost 0.0166) Sleep(static_castDWORD((0.0166 - cost) * 1000)); }邏輯說(shuō)明用steady_clock而不是system_clock因?yàn)楹笳邥?huì)被系統(tǒng)時(shí)間調(diào)整影響做動(dòng)畫(huà)計(jì)時(shí)時(shí)必須單調(diào)遞增。dt是上一幀到這一幀的真實(shí)秒數(shù)所有跟時(shí)間相關(guān)的量位移、旋轉(zhuǎn)角度都乘以它這樣幀率掉下去動(dòng)畫(huà)速度也不會(huì)變——這就是所謂幀率無(wú)關(guān)。很多課設(shè)里小球在別人電腦上跑得飛快、在自己電腦上慢吞吞就是位移寫(xiě)成了每幀固定值。注意Sleep(0)不等于不睡它會(huì)讓出當(dāng)前時(shí)間片在緊循環(huán)里反而可能比Sleep(1)更耗 CPU。真正想跑滿(mǎn)幀就別睡靠垂直同步或者手動(dòng)判斷時(shí)間差。5. 鏈接與運(yùn)行排查五條最常見(jiàn)翻車(chē)記錄這一章全是現(xiàn)象描述按看到什么 → 為什么 → 怎么辦寫(xiě)。這些坑我在不同機(jī)器、不同編譯器上反復(fù)遇到過(guò)順序基本按出現(xiàn)頻率排。5.1 現(xiàn)象滿(mǎn)屏未解析的外部符號(hào)函數(shù)名自己沒(méi)寫(xiě)過(guò)原因鏈接器只拿到了graphics.lib沒(méi)拿到它依賴(lài)的系統(tǒng)庫(kù)。圖形接口內(nèi)部調(diào)用了 GDI、user32 的函數(shù)這些函數(shù)在系統(tǒng)庫(kù)的導(dǎo)入庫(kù)里不顯式鏈接就找不到。解決按報(bào)錯(cuò)信息里的函數(shù)名前綴判斷缺哪個(gè)庫(kù)——帶Gdi、CreatePen、BitBlt的補(bǔ)gdi32帶CreateWindow、GetMessage、RegisterClass的補(bǔ)user32帶Imm的補(bǔ)imm32。CMake 里就是target_link_libraries那一行加上去命令行就是-lgdi32 -luser32 -limm32。還有一種情況是所有函數(shù)都報(bào)未解析那說(shuō)明.lib根本沒(méi)被鏈接上檢查-L路徑和庫(kù)名拼寫(xiě)。5.2 現(xiàn)象雙擊 exe窗口一閃就沒(méi)了原因main執(zhí)行完直接返回了initgraph之后的繪制還沒(méi)來(lái)得及顯示進(jìn)程就退出了?;蛘吒[蔽的一種——.lib是導(dǎo)入庫(kù)運(yùn)行時(shí)找不到配套的.dll加載階段就失敗窗口創(chuàng)建都來(lái)不及。解決先確認(rèn)代碼里有沒(méi)有讓程序停住的東西最土但有效的辦法是在closegraph()前加一句getchar()或者在主循環(huán)里加一個(gè)等待 ESC 的分支。排除代碼原因后把.dll拷到.exe同目錄再試。判斷是不是 DLL 缺失可以打開(kāi)命令行在 exe 目錄下運(yùn)行一次缺失時(shí)系統(tǒng)會(huì)彈一個(gè)明確的錯(cuò)誤框。5.3 現(xiàn)象畫(huà)面劇烈閃爍尤其是有大量圖元的時(shí)候原因沒(méi)有開(kāi)雙緩沖或者開(kāi)了但調(diào)用順序錯(cuò)了。還有一種情況是每幀都在setbkcolor之后再cleardevice等于每幀多刷一次全屏。解決確認(rèn)BeginBatchDraw在循環(huán)外調(diào)一次FlushBatchDraw在循環(huán)內(nèi)調(diào)一次EndBatchDraw在退出前調(diào)一次。再檢查清屏和繪制的順序必須是清 → 畫(huà) → 提交。如果圖元數(shù)量上千光靠雙緩沖也不夠得減少每幀重繪面積把靜態(tài)部分畫(huà)到一張內(nèi)存圖像上反復(fù)貼。5.4 現(xiàn)象中文顯示成亂碼、問(wèn)號(hào)或者方框原因三種可能一是源文件編碼和編譯器輸入編碼不一致二是用了窄字符版本的輸出函數(shù)卻傳了中文三是字體名指定的字體系統(tǒng)里沒(méi)有回退到默認(rèn)字體后中文缺字形。解決先統(tǒng)一編碼源文件保存為 UTF-8編譯加/utf-8或-finput-charsetUTF-8再確認(rèn)outtextxy的字符串是L...寬字符形式最后換一個(gè)確定存在的中文字體或者干脆先用英文把功能跑通再回頭處理字體問(wèn)題。方框和問(wèn)號(hào)要區(qū)分開(kāi)——問(wèn)號(hào)是編碼問(wèn)題方框是字體缺字形兩種故障的修法完全不同。5.5 現(xiàn)象32 位配置能編過(guò)切到 64 位鏈接失敗原因.lib是 32 位的跟 64 位的編譯目標(biāo)架構(gòu)不匹配。靜態(tài)庫(kù)對(duì)架構(gòu)敏感位數(shù)不一致時(shí)鏈接器會(huì)直接報(bào)模塊計(jì)算機(jī)類(lèi)型不匹配。解決把工程配置切回 x8632 位再編。如果非要 64 位就得找對(duì)應(yīng)架構(gòu)的庫(kù)文件沒(méi)有的話(huà)只能改配置——在項(xiàng)目設(shè)置里把目標(biāo)平臺(tái)改成 Win32CMake 里就是cmake -A Win32 ..。這個(gè)錯(cuò)在換電腦、換 IDE 默認(rèn)配置時(shí)特別容易出現(xiàn)因?yàn)楹芏嗄0迥J(rèn)建的是 64 位工程。提示排錯(cuò)時(shí)先看報(bào)錯(cuò)發(fā)生在哪個(gè)階段。編譯階段報(bào)錯(cuò)是頭文件和語(yǔ)法問(wèn)題鏈接階段報(bào)錯(cuò)是庫(kù)和架構(gòu)問(wèn)題運(yùn)行階段崩潰是資源和邏輯問(wèn)題。三個(gè)階段混在一起猜效率會(huì)低很多。6. 驗(yàn)收一個(gè)繪圖 Demo三條線(xiàn)跑通再交付寫(xiě)完代碼不等于能交。我習(xí)慣用一個(gè)最小驗(yàn)收清單過(guò)一遍三條線(xiàn)繪制、交互、幀率每條都有可觀察的判據(jù)不靠感覺(jué)。檢查項(xiàng)判據(jù)不合格的典型表現(xiàn)繪制完整性靜態(tài)元素每幀都在正確位置無(wú)殘留拖影、舊圖元沒(méi)被覆蓋雙緩沖生效快速移動(dòng)的物體邊緣干凈邊緣有鋸齒狀撕裂交互響應(yīng)連續(xù)點(diǎn)擊十次十次都有反應(yīng)只響應(yīng)第一次退出路徑ESC、點(diǎn)關(guān)閉按鈕、AltF4 都能正常退進(jìn)程殘留、黑窗口關(guān)不掉獨(dú)立運(yùn)行exe 拷到空目錄雙擊能起換機(jī)器就報(bào)缺文件中文顯示標(biāo)題、按鈕文字無(wú)問(wèn)號(hào)無(wú)方框全是???驗(yàn)收里最容易被跳過(guò)的是獨(dú)立運(yùn)行這一條。在自己機(jī)器上一切正常是因?yàn)殚_(kāi)發(fā)環(huán)境的 PATH 里恰好有需要的 DLL而別人機(jī)器上沒(méi)有。我的做法是每次改完代碼先把build目錄里的 exe 單獨(dú)拷到桌面一個(gè)空文件夾雙擊跑一遍能起來(lái)再繼續(xù)。這一步花不了一分鐘但攔下過(guò)好幾次交付前才發(fā)現(xiàn)的問(wèn)題。再往上一層如果你的 Demo 要交給別人繼續(xù)改建議把窗口尺寸、顏色、幀間隔這些數(shù)字全部提到文件頭部做成常量別散落在代碼中間const int WIN_W 800; const int WIN_H 600; const int FRAME_MS 16; // 目標(biāo)幀間隔 const COLORREF BG RGB(245, 245, 245); const COLORREF FG RGB(220, 60, 60);這樣別人調(diào)參不用滿(mǎn)文件搜索數(shù)字也避免改了一處漏了另一處。看起來(lái)是小事但一個(gè)散落著魔數(shù)的繪圖程序接手的人第一件事就是重寫(xiě)。最后說(shuō)一個(gè)我自己的習(xí)慣。凡是用這類(lèi)庫(kù)做的東西我一定會(huì)在main的第一行加一句日志輸出把窗口尺寸和庫(kù)的路徑打出來(lái)closegraph之前再打一句正常退出。聽(tīng)著很笨但程序閃退時(shí)這兩行輸出能立刻區(qū)分根本沒(méi)進(jìn)主流程和跑到一半崩了比開(kāi)調(diào)試器快。從那以后我每次配新環(huán)境都強(qiáng)制走一遍命令行編譯 → 空目錄雙擊 → 看兩行日志的流程沒(méi)再出現(xiàn)過(guò)交付當(dāng)天才發(fā)現(xiàn)鏈接不上這種事。希望幫到你。本文還有配套的精品資源點(diǎn)擊獲取