はじめに
もし第1回からお読みくださった方がいましたら、ここまでお付き合いくださってありがとうございます。
おそらく、ほとんどの人が「なんて古くさくてニッチで非効率な内容なんだ」と感じたのではないでしょうか。
そんな個人の趣味を全開にしたシリーズも今回で最終回です。
今回は今更ながらに文字列の出力について取り扱います。
サンプルコード
(2025.07.20現在、未完成ではありますが、完成するまで更新を続けます。のんびりとお待ちいただきたく)
(※2025.07.30追記、ほぼほぼ形になりました。あとはお絵描きが残っています。ドット絵の作成は、老眼には厳しいです...)
(※2025.08.04追記、サンプルが完成しました。完成度を高めようとすれば、まだまだ改修の余地もありますが、ひとまずここまで。反省点は次回以降に活かします)
最後で最初の"Hello World"
文字列の出力といえば、お約束の"Hello World"です。
ウィンドウに対して文字列を表示させる場合、printfやcoutは使用できません。これらは標準出力に対して出力を行うからです。
ウィンドウへの文字列出力を行うAPI関数はいくつかありますが、TextOut関数とDrawText関数が一般的かと思います。
この両者の使い方と違いを解説します。
TextOut
TextOut関数はシンプルな文字列出力のための関数です。
BOOL TextOut(HDC hdc, int x, int y, LPCSTR lpString, int c);
文字列を出力するためには、出力先デバイスコンテキスト(以降DC)へのハンドルが必要です。
ウィンドウDCハンドルを取得するためにはGetDC関数を使用します。
※これまでのサンプルでは、ウィンドウDCのハンドル取得にはBeginPaint関数を使用しましたが、これはWM_PAINTメッセージ内で使用すべき関数だからです。WM_PAINTメッセージ外では別の方法でハンドルを取得しなければなりません。
出力先DCハンドル、出力座標、出力する文字列とその長さを指定します。
HDC hDC;
LPCSTR lpszHelloWorld;
hDC = GetDC(hwnd);
lpszHelloWorld = "Hello World";
TextOut(hDC, 0, 0, lpszHelloWorld, strlen(lpszHelloWorld));
ReleaseDC(hwnd, hDC);
TextOutの特徴として、TABや改行コード等の文字が無視される点が挙げられます。
lpszHelloWorld = "Hello\r\nWorld\r\n";としても1行で表示されます。
シンプルな関数であるが故に、複数行の表示やレイアウトの整形には適していませんが、処理は高速です。
ラベルのような文字列表示に適しています。
DrawText
DrawText関数は指定のエリアに文字列出力する関数です。
int DrawText(HDC hdc, LPCTSTR lpchText, int cchText, LPRECT lprc, UINT format);
出力先DCへのハンドルが必要なこと、出力する文字列とその長さを指定する点はTextOut関数と同じです。
文字列の表示位置および表示エリアはRECT構造体で指定します。その表示エリア内で「中央揃えで表示する」等の指定を第五引数formatで行います。
HDC hDC;
LPCSTR lpszHelloWorld;
RECT rc;
hDC = GetDC(hwnd);
lpszHelloWorld = "Hello World";
rc = {0, 0, 50, 50};
DrawText(hDC, lpszHelloWorld, strlen(lpszHelloWorld), &rc, DT_LEFT | DT_TOP | DT_WORDBREAK); // 左上に表示、折り返し表示
ReleaseDC(hwnd, hDC);
例えば、ゲーム中にて主人公の名前をユーザーに入力させ、登場人物のセリフでその名前を呼ばせるとします。
主人公の名前の文字列長は可変ですので、セリフの文字列長も変わってきます。
TextOut関数を使用した場合は、文字列の改行位置を自前で計算して処理しなければなりません。
しかし、DrawText関数を使用した場合は、自動的に行ってくれます。
ちらつき防止
ゲームにおいて、画面の表示内容が文字列のみであることは稀でしょう。
たいていは描画された画像の上に、文字列を表示したいと思うのです。
文字列の出力はDCを使用するため、下図のような処理をイメージするかもしれません。
┌──┐┌──┐
│BMP││BMP│
└──┘└──┘
↓ ↓
┌──────┐
│ メモリ │
└──────┘
↓
┌──────┐ ┌────┐
│ │←│ 文字列 │
│ ウィンドウ │ └────┘
│ DC │ ┌────┐
│ │←│ 文字列 │
└──────┘ └────┘
たしかに、このやり方でも文字列は表示されます。
しかし、ウィンドウDCへ直接それも頻繁に文字列の出力を行うと、画面がちらつきを起こします。これでは、あまり見映えがよくありません。
そこで、ビットイメージと同様に、文字列も可視化されないメモリへといったん出力し、最後にウィンドウへと出力を行うダブルバッファリングの手法をとります。
下図のように、メモリへのDCを間に挟みます。
┌──┐┌──┐
│BMP││BMP│
└──┘└──┘
↓ ↓
┌──────┐
│ メモリ │
└──────┘
↓
┌──────┐ ┌────┐
│ │←│ 文字列 │
│ メモリ │ └────┘
│ DC │ ┌────┐
│ │←│ 文字列 │
└──────┘ └────┘
↓
┌──────┐
│ ウィンドウ │
│ DC │
└──────┘
初出であるメモリDCはどのように作成するのか、実際のサンプルコードをもとに解説していきます。
example001.cpp - WindowFunc
// ウィンドウ生成時
case WM_CREATE:
int nWidth, nHeight;
nWidth = myex.GetMyEnvironment().nWidth;
nHeight = myex.GetMyEnvironment().nHeight;
hDC = GetDC(hwnd);
myex.hbmOffscreen = CreateCompatibleBitmap(hDC, nWidth, nHeight);
myex.hMemOffscreen = CreateCompatibleDC(hDC);
hOld = SelectObject(myex.hMemOffscreen, myex.hbmOffscreen);
PatBlt(myex.hMemOffscreen, 0, 0, nWidth, nHeight, BLACKNESS);
ReleaseDC(hwnd, hDC);
break;
WM_CREATEはウィンドウを生成するときに呼ばれるメッセージです。
GetDC関数でウィンドウへのDCを取得します。
CreateCompatibleBitmap関数でウィンドウDCと互換性のあるビットマップへのハンドルを取得します。
CreateCompatibleDC関数が件のメモリDCを作成し、そのハンドルを返します。
そして、SelectObject関数により、メモリDCとビットマップを関連付けます。
PatBlt関数では、初期化として、ビットマップのメモリを黒で塗りつぶしています。
最後にGetDCで取得したハンドルをReleaseDC関数で解放します。
取得したハンドルは不要となるタイミングで破棄する必要があります。
ウィンドウ生成時に取得したビットマップへのハンドルと、メモリDCへのハンドルも不要となるタイミングで破棄しなければなりません。
サンプルでは、ウィンドウが存在するうちは文字列の描画を行いますので、不要となるタイミングはウィンドウが破棄されるタイミングです。
// プログラム終了
case WM_DESTROY:
SelectObject(myex.hMemOffscreen, hOld);
DeleteDC(myex.hMemOffscreen);
DeleteObject(myex.hbmOffscreen);
PostQuitMessage(0);
break;
CreateCompatibleDC関数で取得したハンドルは、DeleteDC関数で破棄します。
CreateCompatibleBitmap関数で取得したハンドルは、DeleteObject関数で破棄します。
ハンドルの取得に用いた関数により、ReleaseDCで破棄したり、EndPaintで破棄したりと色々あって混乱しそうですね。
文字色と背景色
画面へのちらつきを防止できましたが、ゲームで文字列表示を行うには、まだ不充分です。
見映えを意識するならば、フォントの大きさ、表示色についても考慮したくなります。
CExampleTitle.cpp - DrawChar
void CMyExampleTitle::DrawChar(HFONT& hfont, int x, int y, LPCWSTR lpszText, COLORREF rgbText)
{
HFONT hFontOld;
SetBkMode(hMemOffscreen, TRANSPARENT);
hFontOld = (HFONT)SelectObject(hMemOffscreen, hfont);
SetTextColor(hMemOffscreen, rgbText);
TextOutW(hMemOffscreen, x, y, lpszText, wcslen(lpszText));
SelectObject(hMemOffscreen, hFontOld);
}
サンプルにおける文字表示の関数です。
TextOutで使用する表示座標や文字列のほか、フォント情報や文字色を引数として受け取っています。
HFONTはCreateFontで作成します。CreateFontのパラメーターは多いため説明は省略しますが、フォント名や文字サイズ、文字修飾を指定できます。
関数内の処理については、まずSetBkModeで文字の背景を透明に設定しています。これにより、文字の背景色が無視されます。逆に背景色をつけたい場合はSetBkColorを使用してください。
SelectObjectでは、文字列の出力先DCと指定フォントを関連付けています。
SetTextColorでは、文字列の色を設定しています。
最後に再度SelectObjectを呼び出し、元のフォント設定に戻します。
文字色を指定できるようになりましたが、ゲームの場合、その色が常に見やすい色になるかは分かりません。
例えば、闇夜のシーンで白い文字は見やすいでしょう。しかし、雪原シーンに移動すると文字列が背景に溶け込んでしまいます。
SetBkColorを使用すれば問題は回避できますが、表示が野暮ったく感じるかもしれません。
SetTextColor(hMemOffscreen, rgbBorder);
TextOutW(hMemOffscreen, x, y - 1, lpszText, wcslen(lpszText));
TextOutW(hMemOffscreen, x + 1, y, lpszText, wcslen(lpszText));
TextOutW(hMemOffscreen, x, y + 1, lpszText, wcslen(lpszText));
TextOutW(hMemOffscreen, x - 1, y, lpszText, wcslen(lpszText));
SetTextColor(hMemOffscreen, rgbText);
TextOutW(hMemOffscreen, x, y, lpszText, wcslen(lpszText));
そこで、文字の表示位置の上下左右に別色で文字列を出力させます。すると、文字列が枠線で縁取られ、見やすくなるでしょう。
最後に
記事を書くにあたり、どのようなゲームを作成しようかと考えていたとき、最初に思いついたのがテトリスのようなパズルゲームでした。
ルール説明も不要で、用意するBMPもブロック画像のみで済み、コーディングもさほど大変ではありません。
いいことづくめで、ほぼ決めていました。
そのような折り、たまたまYouTubeで見かけたのが生成AI「Claude」の解説動画でした。
その動画内にて「テトリスを作ってください」の一文だけでコード生成され、しかもその場で動作確認も可能な様子を目の当たりにしたとき「テトリス以外のものを作ろう」と思いました。
自分の作ろうとした物に価値を見出せなくなったのだと思います。
それにしても、近年の生成AIの進化は早いですね。飛躍的です。
2025年5月、AIの普及によりMicrosoftではエンジニアの人員削減を発表しました。この流れは、今後もAIの発展にともない続いていくのでしょう。
凡庸未満の技術者である僕は、そのうち失業の憂き目にあうかも分かりません。
そして、プログラマーは一握りの天才的な技術者だけに許された職業になっていくかも分かりません。
そうなっては、"Hello World"を出力させるだけでもドキドキと胸を高鳴らせたプログラミングの楽しさは、天才だけの特権になってしまいます。
一部の特権階級から"Hello World"を取り戻さなくてはならない!趣味の世界でのプログラミングならば、それが可能だ!
そのような妄想をふくらませ、さして仕事では役に立たない趣味全開のコードや記事を書き綴ってきました。
ここまでお読みくださって本当にありがとうございます。
この記事がプログラミングや物作りを趣味とする誰かの一助になれば幸いです。