とある科学の備忘録

とある科学の備忘録

CやPythonのプログラミング、Arduino等を使った電子工作をメインに書いています。また、木製CNCやドローンの自作製作記も更新中です。たまに機械学習とかもやってます。

【C++ Win32】文字の描画とフォントの作成(Win32 APIシリーズ第2回)

目標

Win32 APIを使ってWindowsアプリケーションを作るシリーズ、第二回です。
今回は、画面上に文字を表示してみようと思います。
といっても、文字を表示する方法はいくつかあり、
・CreateWindow("STATIC", .......);を使う方法
・TextOut()関数を使う方法...
等がありますが、今回紹介するのは後者です。

プログラム①

全体のプログラムです。

#include <windows.h>

LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);
char szClassName[] = "text_test";	//ウィンドウクラス
void MyPaint(HWND);  //WM_PAINTが呼ばれたときの描画動作 

int WINAPI WinMain(HINSTANCE hCurInst, HINSTANCE hPrevInst,LPSTR lpsCmdLine, int nCmdShow){
	MSG msg;
	BOOL bRet;
	WNDCLASSEX wc;
	HWND hWnd;
	ATOM atom;

	wc.cbSize = sizeof(WNDCLASSEX);
	wc.style = CS_HREDRAW | CS_VREDRAW;
	wc.lpfnWndProc = WndProc;	//プロシージャ名
	wc.cbClsExtra = 0;
	wc.cbWndExtra = 0;
	wc.hInstance = hCurInst;//インスタンス
	wc.hIcon = (HICON)LoadImage(NULL,MAKEINTRESOURCE(IDI_APPLICATION),IMAGE_ICON,0,0,LR_DEFAULTSIZE | LR_SHARED);
        wc.hCursor = (HCURSOR)LoadImage(NULL,MAKEINTRESOURCE(IDC_ARROW),IMAGE_CURSOR,0,0,LR_DEFAULTSIZE | LR_SHARED);
	wc.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);
	wc.lpszMenuName = NULL;
	wc.lpszClassName = (LPCSTR)szClassName;
	wc.hIconSm = (HICON)LoadImage(NULL,MAKEINTRESOURCE(IDI_APPLICATION),IMAGE_ICON,	0,0,LR_DEFAULTSIZE | LR_SHARED);
        
        if ((atom = RegisterClassEx(&wc)) == 0)return FALSE;

	hWnd = CreateWindow(MAKEINTATOM(atom), "Window Title", WS_OVERLAPPEDWINDOW, CW_USEDEFAULT,CW_USEDEFAULT,200,100,NULL,NULL, hCurInst, NULL);
	if (!hWnd)return FALSE;

	ShowWindow(hWnd, nCmdShow);
	UpdateWindow(hWnd);
	while ((bRet = GetMessage(&msg, NULL, 0, 0)) != 0) {
		if (bRet == -1) {
			break;
		}else {
			TranslateMessage(&msg);
			DispatchMessage(&msg);
		}
	}
	return (int)msg.wParam;
}

LRESULT CALLBACK WndProc(HWND hWnd, UINT msg, WPARAM wp, LPARAM lp){
	switch (msg) {
	case WM_PAINT:
		MyPaint(hWnd);
		break;
	case WM_DESTROY:
		PostQuitMessage(0);
		break;
	default:
		return (DefWindowProc(hWnd, msg, wp, lp));
	}
	return 0;
}

void MyPaint(HWND hWnd) {
	HDC hdc;
	PAINTSTRUCT ps;

	char *szStr = "Hello, World!!";
	hdc = BeginPaint(hWnd, &ps);

	SetBkColor(hdc, RGB(10, 10, 10));  //文字の背景色を設定
	SetTextColor(hdc, RGB(200,200,200));  //文字の色を設定
	TextOut(hdc, 10, 10, (LPCTSTR)szStr, (int)strlen(szStr));//文字を書く
	EndPaint(hWnd, &ps);
}

実行結果

以下のようなWindowが表示されます
f:id:pythonjacascript:20200225172147j:plain

解説

前回と変わった部分だけ抜き出すと、

void MyPaint(HWND);  //WM_PAINTが呼ばれたときの描画動作 

LRESULT CALLBACK WndProc(HWND hWnd, UINT msg, WPARAM wp, LPARAM lp){
	switch (msg) {
	case WM_PAINT:
		MyPaint(hWnd);
		break;
        //(そのほか色んな処理)
	}
}

void MyPaint(HWND hWnd) {
	HDC hdc;
	PAINTSTRUCT ps;

	char *szStr = "Hello, World!!";
	hdc = BeginPaint(hWnd, &ps);

	SetBkColor(hdc, RGB(10, 10, 10));  //文字の背景色を設定
	SetTextColor(hdc, RGB(200,200,200));  //文字の色を設定
	TextOut(hdc, 10, 10, (LPCTSTR)szStr, (int)strlen(szStr));//文字を書く
	EndPaint(hWnd, &ps);
}


です。このプログラムでは、MyPaint関数で文字を描画しています。
具体的にどのようにして文字が表示されるのかを見ていきます。

まず、WM_PAINTメッセージは、再描写が必要なときに送られてくるメッセージです。

WM_PAINTが送られるタイミングは、

➀Windowの起動時(正確には、UpdateWindow()が実行されたとき)
②ウィンドウが変更された時(リサイズ、最小化、最大化など)
③InvalidRect()が呼ばれたとき

などです。

WinMain関数内にUpdateWindow();と書いてあるので、WM_PAINTが呼ばれます。そして、WndProc関数を経由してMyPaint関数が呼び出されます。

TextOut関数

TextOut関数を使うことで文字列をWindow上に表示できます

BOOL TextOut(
  HDC hdc,  //描画するデバイスコンテキスト
  int nXStart, //文字列の左上のx座標
  int nYStart, //文字列の左上のy座標
  LPCTSTR lpString, //文字列
  int cbString  //文字数( (int)strlen(lpString)と書くことが圧倒的に多い)
);


重要なのは第一引数の「バイスコンテキスト」です。

バイスコンテキストとは

バイスコンテキストは、ディスプレイやプリンターなどのデバイスの描画属性に関する情報を含む Windows データ構造です。出力デバイスはそれぞれデバイスコンテキストを持っており、それらは「バイスコンテキストハンドル」という値を使って識別します。

GetDC()関数を使うことでその出力デバイスのデバイスコンテキストハンドルを得ることができます。
ウィンドウ全体(クライアント領域と非クライアント領域の両方)または画面全体のデバイスコンテキストハンドルを取得する場合は、GetWindowDCを使用します。 プリンターの場合 CreateDCを使用します。

HDC GetDC(HWND hWnd);  
//hWnd   ->  ウィンドウハンドル。
//返り値 ->  hWndのデバイスコンテキストハンドル

  

バイスコンテキストには、図形の枠を描くためのペン、塗りつぶし用のブラシ、ビットマップ、色のセットを定義するパレット...などの描画に必要なものを設定することができます。
f:id:pythonjacascript:20200225222958j:plain
そして、TextOut関数やFillRect関数を使って、Win32 APIからデバイスコンテキストを通してWindowに描画します。

バイスコンテキストの利点

バイスコンテキストを使うことで、異なる出力デバイスを同様のプログラムで制御することができます。プリンタで文字を印刷するにも、モニター上に文字を表示するにもTextOut()関数を使えばよいのです。

背景色と文字色の設定

文字の表示の話に戻ります。文字色と背景色の設定は以下の様に行います。

HDC hdc;  //Windowのデバイスコンテキストハンドル
SetBkColor(hdc, RGB(10, 10, 10));  //文字の背景色を設定
//または、
SetBkMode(hdc ,TRANSPARENT);  // TRANSPARENT→背景を透明にする。OPAQUE→デフォルト

SetTextColor(hdc, RGB(200,200,200));  //文字の色を設定

 
RGB(int r, int g, int b)の引数は、色のRGB値(0~225)の値です。

フォントの設定

フォントを設定するには、

  1. フォントを作成する【CreateFont関数】
  2. フォントを適応する【SelectObject関数】
  3. 文字を書き終わったら(または、Windowを破棄する時に)、フォントオブジェクトを削除する【DeleteObject関数】

の3つの変更が必要になります。
3のDeleteObject()を行わないと、while()ループの中で一回のループにつき1つずつHFONTが作られるようになっているので、いずれはメモリが足りなくなってフリーズします。
DeleteObjectは(個人的に)忘れがちなのですが大事です。

これらをすべて実装すると、↑のプログラムのMyPaint関数は下のようになります。

void MyPaint(HWND hWnd) {
	HDC hdc;
	PAINTSTRUCT ps;

	char *szStr = "Hello, World!!";
	hdc = BeginPaint(hWnd, &ps);
	SetBkColor(hdc, RGB(10, 10, 10));  //文字の背景色を設定
	SetTextColor(hdc, RGB(200, 200, 200));  //文字の色を設定

	//Step1  フォント作成
	HFONT hFont1 = CreateFont(30, 0, //高さ, 幅
		0, 0, FW_BOLD,  //傾き(反時計回り), 文字の向き, 太字(0にするとデフォルト)
		TRUE, TRUE, FALSE,   //Italic,  下線,  打消し線
		SHIFTJIS_CHARSET, OUT_DEFAULT_PRECIS, //キャラクタセット, 物理フォントの検索方法
		CLIP_DEFAULT_PRECIS, DEFAULT_QUALITY,  //はみ出した場合の処理,  属性
		VARIABLE_PITCH | FF_ROMAN, NULL);    //ピッチ、ファミリ,  タイプフェイスのポインタ

	SelectObject(hdc, hFont1);  //Step2  フォントを適応
	TextOut(hdc, 10, 10, (LPCTSTR)szStr, (int)strlen(szStr)); //Step3  文字を描画
	DeleteObject(hFont1);   //Step4. HFONTオブジェクトを破棄
	EndPaint(hWnd, &ps);
}

このようになります。実行結果は以下の通り。
f:id:pythonjacascript:20200225224341j:plain
  

では、CreateFont関数の引数を説明していきます。

HFONT CreateFont(
    int nHeight,  //1文字の高さ(px)
    int nWidth,   //1文字あたりの横幅(px)。高さに合わせて自動で設定するには0にする
    int nEscapement,   //水平方向からの文字列の角度(反時計回り)
    int nOrientation,   //各文字の向きを水平方向から反時計回りに回転
    int nWeight,   //フォントの太さ。0, FALSE →デフォルト、FW_BOLD→太字
    BYTE bItalic,   //TRUE→イタリック体(斜め文字)になる(通常は0 or FALSE)
    BYTE bUnderline,   //TRUE→アンダーラインが引かれる(通常は0 or FALSE)
    BYTE cStrikeOut,   //TRUE→アンダーラインが引かれる(通常は0 or FALSE)
    BYTE nCharSet,   //キャラクタセットの設定
    BYTE nOutPrecision,   //出力精度 通常は OUT_DEFAULT_PRECIS
    BYTE nClipPrecision,   //はみ出た場合の処理 通常はCLIP_DEFAULT_PRECIS
    BYTE nQuality,   //属性 
    BYTE nPitchAndFamily,   //ピッチ、ファミリ
    LPCTSTR lpszFacename);   //フォント名

nWeightについて

FW_BOLD以外にも、色々あります。

fnWeightの例→FW_DONTCARE(デフォルト)
FW_THIN	,  FW_EXTRALIGHT, FW_ULTRALIGHT, W_EXTRALIGHT,  FW_LIGHT, FW_NORMAL,  FW_REGULAR,  FW_NORMAL, FW_MEDIUM,
FW_SEMIBOLD, FW_DEMIBOLD, FW_SEMIBOLD, FW_BOLD, FW_EXTRABOLD, FW_ULTRABOLD,  FW_EXTRABOLD, FW_BLACK, FW_HEAVY


上の順番でだんだん文字が太くなっていきます。


nCharSetについて

設定値 使用用途
ANSI_CHARSET ANSIフォントを使用。アルファベットと一部記号のみで日本語非対応。
DEFAULT_CHARSET デフォルト
SHIFTJIS_CHARSET SHIFT JISを使う場合。日本語OK

nEscapementとnOrientationについて

1/10°単位で指定できるので、45°回転させるには、450を代入します。
f:id:pythonjacascript:20200225230849j:plain
  

nQualityについて

出力品質を設定します。論理フォントと実際の物理フォントの属性をどの程度一定させるかを設定します。

DEFAULT_QUALITY 通常
ANTIALIASED_QUALITY フォントのふちが滑らかになる

f:id:pythonjacascript:20200225232530j:plain
うーん...違いが分からん...

nPitchAndFamilyについて

以下の設定値をOR(|)でつないだものを指定します。

VARIABLE_PITCH | FF_ROMAN  //例


ピッチ=文字の間隔のこと

設定値 用途
DEFAULT_PITCH デフォルト
FIXED_PITCH 固定幅
VARIABLE_PITCH 可変幅

f:id:pythonjacascript:20200225233021j:plain
  

ファミリ

設定値 使用用途
FF_DECORATIVE 装飾付きフォント。例:Old English
FF_DONTCARE デフォルト
FF_MODERN 一定のストローク幅を持つフォント。例:Pica, Elite, and Courier New
FF_ROMAN 可変ストローク幅を持ち、セリフ付き。例: MS Serif
FF_SCRIPT 手書きのようなフォント。 Script and Cursive are examples.
FF_SWISS 可変ストローク幅、セリフなし。 例:MS?Sans Serif is an example.

 
f:id:pythonjacascript:20200225234447j:plain
....??


次に行きます。

lpszFacenameについて

フォント名を指定します。

HFONT hFont1 = CreateFont(30, 0, 0, 0, 0, FALSE, FALSE, FALSE, SHIFTJIS_CHARSET, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS, DEFAULT_QUALITY,
        VARIABLE_PITCH | FF_DONTCARE, "HG行書体");


というふうに書き換えて実行すると、
f:id:pythonjacascript:20200225235955j:plain
こうなります。


利用可能なフォントの一覧を取得するには、一例としてPythonをあげると、

import pygame
 print(pygame.font.get_fonts())

を実行してください。