0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

文字の形を自分で作る ― フォントを自作して描画するC言語の実験

Posted at

初めに

文字表示はあまりにも当たり前すぎて、普段は意識しない

本記事の狙い:「文字ってどう表示されてるの?」を一から見直す

実験の概要:「HELLO WORLD」を自作フォントで表示

そもそもフォント(字体)とは?

文字コード(ASCIIなど)は「どの文字か」を決めているだけ
「どう描くか」はフォント(字体)の仕事

自作フォントを定義する(8×8ドット文字)

ドット絵とは?ビットで表現する方法

実際のフォントデータ(例:'H'や'R'の定義)

各ビットが画素に対応している仕組み

ドット絵を描く処理

SetPixel() で画素単位の描画を行う

1文字ずつ描く仕組み

1行の文字列「HELLO WORLD」を並べて描く方法

プログラム

custom_font.c
custom_font.c
#include <windows.h>

#define CHAR_WIDTH 8
#define CHAR_HEIGHT 8
#define CHAR_SPACING 2

// HELLO WORLD に使うすべての文字のビットマップ定義(8x8)
const unsigned char FONT_H[8] = {
    0b10000001,
    0b10000001,
    0b10000001,
    0b11111111,
    0b10000001,
    0b10000001,
    0b10000001,
    0b00000000
};

const unsigned char FONT_E[8] = {
    0b11111111,
    0b10000000,
    0b10000000,
    0b11111110,
    0b10000000,
    0b10000000,
    0b11111111,
    0b00000000
};

const unsigned char FONT_L[8] = {
    0b10000000,
    0b10000000,
    0b10000000,
    0b10000000,
    0b10000000,
    0b10000000,
    0b11111111,
    0b00000000
};

const unsigned char FONT_O[8] = {
    0b01111110,
    0b10000001,
    0b10000001,
    0b10000001,
    0b10000001,
    0b10000001,
    0b01111110,
    0b00000000
};

const unsigned char FONT_W[8] = {
    0b10000001,
    0b10000001,
    0b10000001,
    0b10000001,
    0b10011001,
    0b10100101,
    0b01000010,
    0b00000000
};

const unsigned char FONT_R[8] = {
    0b11111100,
    0b10000010,
    0b10000010,
    0b11111100,
    0b10001000,
    0b10000100,
    0b10000010,
    0b00000000
};

const unsigned char FONT_D[8] = {
    0b11111110,
    0b10000001,
    0b10000001,
    0b10000001,
    0b10000001,
    0b10000001,
    0b11111110,
    0b00000000
};

const unsigned char FONT_SPACE[8] = {
    0b00000000,
    0b00000000,
    0b00000000,
    0b00000000,
    0b00000000,
    0b00000000,
    0b00000000,
    0b00000000
};

// "HELLO WORLD" という文字列を表示するために、
// 各文字に対応するドット絵データ(フォント)をポインタで並べたもの
// 各フォントデータは、文字を画面に描画するための8×8ドットのビットパターンを表している
const unsigned char* MESSAGE[] = {
    FONT_H, FONT_E, FONT_L, FONT_L, FONT_O,
    FONT_SPACE,
    FONT_W, FONT_O, FONT_R, FONT_L, FONT_D
};

#define MESSAGE_LEN (sizeof(MESSAGE) / sizeof(MESSAGE[0]))

void draw_char(HDC hdc, int x, int y, const unsigned char* font) {
    for (int row = 0; row < CHAR_HEIGHT; row++) {
        for (int col = 0; col < CHAR_WIDTH; col++) {
            if (font[row] & (1 << (7 - col))) {
                SetPixel(hdc, x + col, y + row, RGB(0, 0, 0));
            }
        }
    }
}

LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) {
    switch (msg) {
    case WM_PAINT: {
        PAINTSTRUCT ps;
        HDC hdc = BeginPaint(hwnd, &ps);

        int x = 10;
        int y = 10;
        for (int i = 0; i < MESSAGE_LEN; i++) {
            draw_char(hdc, x, y, MESSAGE[i]);
            x += CHAR_WIDTH + CHAR_SPACING;
        }

        EndPaint(hwnd, &ps);
        break;
    }
    case WM_DESTROY:
        PostQuitMessage(0);
        break;
    default:
        return DefWindowProc(hwnd, msg, wParam, lParam);
    }
    return 0;
}

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
                   LPSTR lpCmdLine, int nCmdShow) {
    const char CLASS_NAME[] = "CustomFontWindow";

    WNDCLASS wc = { };
    wc.lpfnWndProc = WndProc;
    wc.hInstance = hInstance;
    wc.lpszClassName = CLASS_NAME;
    wc.hbrBackground = (HBRUSH)(COLOR_WINDOW+1);

    RegisterClass(&wc);

    HWND hwnd = CreateWindowEx(
        0,
        CLASS_NAME,
        "Custom HELLO WORLD",
        WS_OVERLAPPEDWINDOW,
        CW_USEDEFAULT, CW_USEDEFAULT, 400, 200,
        NULL, NULL, hInstance, NULL
    );

    if (hwnd == NULL) return 0;

    ShowWindow(hwnd, nCmdShow);

    MSG msg = { };
    while (GetMessage(&msg, NULL, 0, 0)) {
        TranslateMessage(&msg);
        DispatchMessage(&msg);
    }

    return 0;
}

動作

image.png
image.png

プログラムの解説

※文字描画に関わる処理のみに絞り、windows固有の書き方については割愛します。

定義

  • MESSAGEは文字列に相当。ここでHELLO WORLDを設定している。
  • MESSAGE_LENはHELLO WORLDの文字数11が入る。
const unsigned char* MESSAGE[] = {
    FONT_H, FONT_E, FONT_L, FONT_L, FONT_O,
    FONT_SPACE,
    FONT_W, FONT_O, FONT_R, FONT_L, FONT_D
};

#define MESSAGE_LEN (sizeof(MESSAGE) / sizeof(MESSAGE[0]))

WndProc関数

  • 10,10の座標から文字数分forを回して描画していく。
  • MESSAGE配列の中にどの文字を表示するかの情報が入っている
  • draw_charを呼び出しピクセル単位で文字を描画する。
  • 一文字出力する毎にx座標を一文字分の幅8+間隔2ずつ右へずらす
        int x = 10;
        int y = 10;
        for (int i = 0; i < MESSAGE_LEN; i++) {
            draw_char(hdc, x, y, MESSAGE[i]);
            x += CHAR_WIDTH + CHAR_SPACING;
        }

draw_char関数

  • 高さ8、幅8の2重ループで処理
  • (1 << (7 - col)) は、col 番目のビットを1に設定するための式
  • font[row] & (1 << (7 - col)) で、font[row] の中で指定された col 番目のビットが 1 かどうかを確認
  • 結果が 0 でない場合(つまりビットが 1 であれば)、その位置にピクセル(黒)を描画
    for (int row = 0; row < CHAR_HEIGHT; row++) {
        for (int col = 0; col < CHAR_WIDTH; col++) {
            if (font[row] & (1 << (7 - col))) {
                SetPixel(hdc, x + col, y + row, RGB(0, 0, 0));
            }
        }
    }

例:1 行のビット列を描画する流れ

例えば、font[row] の値が次のようなビット列だったとします:

font[0] = 0b10110010;

これは、次のようにビット配置されます(左が上位ビット):

bit位置: 7 6 5 4 3 2 1 0
値 : 1 0 1 1 0 0 1 0
描画 : ■ □ ■ ■ □ □ ■ □

この1バイトは、row = 0(上から1行目)に相当します。

col 1 << (7 - col) font[0] & ... 結果 描画位置(x + col, y)
0 0b10000000 1(true) (x + 0, y + 0) ⇒ ピクセル描画
1 0b01000000 0(false) 描画しない
2 0b00100000 1 (x + 2, y + 0)
3 0b00010000 1 (x + 3, y + 0)
4 0b00001000 0 描画しない
5 0b00000100 0 描画しない
6 0b00000010 1 (x + 6, y + 0)
7 0b00000001 0 描画しない
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?