在沒有滑鼠、只有鍵盤的 DOS 年代,如何用 C 語言實作一個支援畫筆、橡皮擦、存檔/讀檔、畫圓的繪圖小工具?本文深度剖析一套經典 graphics.h 鍵盤繪圖程式,涵蓋視訊模式初始化、游標 Sprite 動畫、像素級繪圖邏輯、螢幕記憶體直接存取(VGA Planar Mode)等核心技術,並提供現代 SDL2 移植建議。
| 按鍵 | 功能 | 游標外觀 |
|---|---|---|
| W | 進入畫筆模式(落筆) | ✏️ 鉛筆圖示 |
| V | 提筆(移動不畫線) | 🖱️ 空心游標 |
| R | 進入橡皮擦模式 | 🧽 橡皮擦圖示 |
| C | 在當前位置畫半徑 20 像素的圓 | — |
| S | 將畫布存為 pic.dat | — |
| E | 從 pic.dat 載入畫布 | — |
| 方向鍵 | 8 方向移動游標(含對角線) | — |
| Esc | 離開程式 | — |
程式啟動時呼叫 detectgraph() + initgraph() 進入 640×480×16 色 VGA 模式(驅動程式 EGAVGA.BGI)。為了實現無閃爍游標移動,採用Sprite 背景保存法:
getimage() 將三個游標各自存入記憶體緩衝區 wg、rg、vg。getimage() 保存游標下方背景到 fy → putimage(XOR_PUT) 畫游標 → 等待按鍵 → putimage(AND_PUT) 還原背景。putpixel(x, y, color) 直接寫入顯示記憶體。putpixel(x, y, BLACK) 以背景色覆蓋。circle(x, y, 20) 呼叫 BGI 內建中點畫圓演算法。這是本程式最「硬核」的部分。BGI 無提供全螢幕存檔 API,作者直接操作 VGA 顯示卡暫存器:
save()for (plane = 0; plane < 4; ++plane) {
outportb(0x3CE, 4); // Graphics Controller Read Map Select
outportb(0x3CF, plane); // 選擇平面 0~3
ptr = (char far *)0xA0000000L; // 顯示記憶體段地址
for (j = 0; j < 38400; ++j) // 640*480/8 = 38400 bytes per plane
putc(*ptr++, fp);
}
outportb(0x3CF, 0); // 還原預設
原理:VGA 16 色模式下,每像素 4 bits 分散在 4 個平面(Plane 0~3),每平面 38,400 bytes,共 153,600 bytes = 640×480×4 bits。
load()for (plane = 0; plane < 4; ++plane) {
outportb(0x3C4, 2); // Sequencer Map Mask
outportb(0x3C5, 1 << plane); // 啟用單一平面寫入
ptr = (char far *)0xA0000000L;
for (j = 0; j < 38400; ++j)
*ptr++ = getc(fp);
}
outportb(0x3C5, 0xF); // 還原四平面全開
#include <graphics.h>
#include <stdio.h>
#include <fcntl.h>
#include <stdlib.h>
#include <bios.h>
#include <dos.h>
void save(const char *fname);
void load(const char *fname);
void *wg, *rg, *vg, *fy; // 游標 Sprite 緩衝
int driver, mode;
int color = RED;
int x = 320, y = 225; // 游標初始位置
int x1, y1, x2, y2;
int k, k1, k2;
int main() {
/* 初始化繪圖 */
detectgraph(&driver, &mode);
initgraph(&driver, &mode, "c:\\tc\\bgi");
/* 繪製三種游標圖示於頂端 */
bar(200, 10, 206, 16); line(203, 7, 200, 10); line(203, 7, 206, 10); // 鉛筆
line(243, 7, 240, 16); line(243, 7, 246, 16); // 橡皮擦
line(283, 7, 280, 10); line(283, 7, 286, 10); line(283, 7, 283, 16); // 空心
/* 擷取游標 Sprite */
wg = malloc(imagesize(200, 7, 206, 16));
rg = malloc(imagesize(240, 7, 246, 16));
vg = malloc(imagesize(280, 7, 286, 16));
fy = malloc(imagesize(200, 7, 206, 16));
getimage(200, 7, 206, 16, wg);
getimage(240, 7, 246, 16, rg);
getimage(280, 7, 286, 16, vg);
cleardevice();
/* 畫黃色邊框 */
setcolor(YELLOW);
rectangle(4, 19, 637, 447);
/* 初始游標背景 */
x1 = x - 3; y1 = y + 1; x2 = x + 3; y2 = y + 10;
getimage(x1, y1, x2, y2, fy);
putimage(x1, y1, vg, XOR_PUT); // 先顯示空心游標
/* 主事件迴圈 */
while (1) {
while (bioskey(1) == 0); // 等待按鍵
k = bioskey(0);
putimage(x1, y1, fy, AND_PUT); // 還原背景
if ((k & 0x00FF) == 0) { // 擴充鍵
k1 = (k & 0xFF00) >> 8;
switch (k1) {
case 0x01: restorecrtmode(); exit(0); // Esc
case 72: if (y > 20) y--; break; // Up
case 75: if (x > 5) x--; break; // Left
case 77: if (x < 636) x++; break; // Right
case 80: if (y < 446) y++; break; // Down
case 71: if (x > 5 && y > 20) { x--; y--; } break; // Home
case 79: if (x > 5 && y < 446) { x--; y++; } break; // End
case 73: if (x < 636 && y > 20) { x++; y--; } break; // PgUp
case 81: if (x < 636 && y < 446) { x++; y++; } break; // PgDn
}
} else { // 一般字元鍵
k2 = k & 0x00FF;
switch (k2) {
case 'v': case 'V': putimage(x1, y1, vg, OR_PUT); break; // 提筆
case 'w': case 'W': putimage(x1, y1, wg, OR_PUT);
putpixel(x, y, color); break; // 畫筆
case 'r': case 'R': putimage(x1, y1, rg, OR_PUT);
putpixel(x, y, BLACK); break; // 橡皮擦
case 's': case 'S': save("pic.dat"); break;
case 'e': case 'E': load("pic.dat"); break;
case 'c': case 'C': setcolor(RED); circle(x, y, 20); break; // 畫圓
}
}
/* 更新游標位置與背景 */
x1 = x - 3; y1 = y + 1; x2 = x + 3; y2 = y + 10;
getimage(x1, y1, x2, y2, fy);
putimage(x1, y1, vg, XOR_PUT); // 重畫游標(此處簡化,實際應依模式選 wg/rg/vg)
}
}
/* 以下 save()/load() 同前節 VGA Planar 程式碼 */
SDL_CreateWindow + SDL_CreateRenderer 取代 initgraph。SDL_Texture 儲存三種游標,SDL_RenderCopy 繪製,無需手動背景保存(GPU 自動合成)。SDL_RenderDrawPoint 或鎖定 SDL_Texture 直接寫入像素陣列。SDL_RenderReadPixels 取得 RGBA8888 緩衝區,直接 fwrite 存成 PNG(建議用 stb_image_write.h)。SDL_PollEvent 處理 SDL_KEYDOWN / SDL_MOUSEMOTION,原生支援滑鼠,體驗大幅提升。這支程式展示了 90 年代個人電腦圖形程式設計的典型模式:
對於現代開發者,閱讀此類「裸機」程式碼能建立對幀緩衝、顯示管線、輸入事件迴圈的直觀理解,是進階圖形程式設計的寶貴基石。