首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> C語言 >> 關於C語言

C 語言鍵盤繪圖程式:從 Turbo C 時代的像素級繪圖到現代移植指南

     編輯:關於C語言

在沒有滑鼠、只有鍵盤的 DOS 年代,如何用 C 語言實作一個支援畫筆、橡皮擦、存檔/讀檔、畫圓的繪圖小工具?本文深度剖析一套經典 graphics.h 鍵盤繪圖程式,涵蓋視訊模式初始化、游標 Sprite 動畫、像素級繪圖邏輯、螢幕記憶體直接存取(VGA Planar Mode)等核心技術,並提供現代 SDL2 移植建議。

功能概覽與操作鍵

按鍵功能游標外觀
W進入畫筆模式(落筆)✏️ 鉛筆圖示
V提筆(移動不畫線)🖱️ 空心游標
R進入橡皮擦模式🧽 橡皮擦圖示
C在當前位置畫半徑 20 像素的圓—
S將畫布存為 pic.dat—
E從 pic.dat 載入畫布—
方向鍵8 方向移動游標(含對角線)—
Esc離開程式—

核心架構解析

1. 視訊模式與雙緩衝游標

程式啟動時呼叫 detectgraph() + initgraph() 進入 640×480×16 色 VGA 模式(驅動程式 EGAVGA.BGI)。為了實現無閃爍游標移動,採用Sprite 背景保存法:

  1. 預先繪製三種游標圖示(鉛筆、橡皮擦、空心)於螢幕頂端。
  2. getimage() 將三個游標各自存入記憶體緩衝區 wg、rg、vg。
  3. 主迴圈中:getimage() 保存游標下方背景到 fy → putimage(XOR_PUT) 畫游標 → 等待按鍵 → putimage(AND_PUT) 還原背景。

2. 像素級繪圖與擦除

  • 畫筆:putpixel(x, y, color) 直接寫入顯示記憶體。
  • 橡皮擦:putpixel(x, y, BLACK) 以背景色覆蓋。
  • 畫圓:circle(x, y, 20) 呼叫 BGI 內建中點畫圓演算法。

3. 全螢幕存檔/讀檔:VGA Planar Mode 直接操作

這是本程式最「硬核」的部分。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 程式碼 */

現代化移植建議(SDL2 / OpenGL)

  1. 視訊初始化:SDL_CreateWindow + SDL_CreateRenderer 取代 initgraph。
  2. 游標 Sprite:用 SDL_Texture 儲存三種游標,SDL_RenderCopy 繪製,無需手動背景保存(GPU 自動合成)。
  3. 像素操作:SDL_RenderDrawPoint 或鎖定 SDL_Texture 直接寫入像素陣列。
  4. 存檔:SDL_RenderReadPixels 取得 RGBA8888 緩衝區,直接 fwrite 存成 PNG(建議用 stb_image_write.h)。
  5. 輸入處理:SDL_PollEvent 處理 SDL_KEYDOWN / SDL_MOUSEMOTION,原生支援滑鼠,體驗大幅提升。

歷史價值與啟發

這支程式展示了 90 年代個人電腦圖形程式設計的典型模式:

  • 直接操作硬體暫存器(VGA Planar)突破函式庫限制。
  • Sprite 背景保存法在無硬體游標、無雙緩衝環境下實現流暢互動。
  • 鍵盤 8 方向移動邏輯是早期遊戲角色控制的雛形。

對於現代開發者,閱讀此類「裸機」程式碼能建立對幀緩衝、顯示管線、輸入事件迴圈的直觀理解,是進階圖形程式設計的寶貴基石。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved