初めに
文字表示はあまりにも当たり前すぎて、普段は意識しない
本記事の狙い:「文字ってどう表示されてるの?」を一から見直す
実験の概要:「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;
}
動作
プログラムの解説
※文字描画に関わる処理のみに絞り、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 |
□ | 描画しない |