1.CreateFontIndirectではうまくいかない?
Windowsで、フォントに任意の扁平(長体)をかけて描画するには、CreateFontIndirect(もしくはCreateFont)Apiを使えばいい、はずだった。90年代のプログラミング本にはそうした記述があった記憶がある。が、現在、実際にはOpenTypeフォントや一部のTrueTypeフォントでは、うまくいかない。
OpenTypeフォントなら今時はDirectWriteを使うべきなのではという声もあるだろうが、20世紀からある由緒正しい(?)Windows Apiを使って何とかしてみよう、というお題。
なお、以下の記述は、Windows Apiの関連仕様をすべて正しく把握できている保証はなく、試行錯誤した経験則と推測によるもの。(突っ込み歓迎)
とりあえず試してみる。
基本的にはCreateFontIndirectに渡す適切なLOGFONT構造体を作るだけ。以下、扁平に必要なLOGFONT構造体を作る最低限のコード。
1 2 3 4 5 6 7 8 9 10 11 |
LOGFONT CreateLogFont( const std::wstring& fontname, int height_px, int width_px ) { LOGFONT LFont = {}; LFont.lfHeight = -height_px; LFont.lfWidth = width_px / 2; // 全角の場合の半分の数値 wcscpy( LFont.lfFaceName, fontname.c_str() ); return LFont; } |
実際にVclアプリケーションで「MS ゴシック」と「游ゴシック」で試してみる。
1 2 3 4 5 6 7 8 9 10 11 12 |
void __fastcall TForm1::Button1Click(TObject *Sender) { const int height_px = 50, width_px = 70; LOGFONT LFont1 = CreateLogFont( L"MS ゴシック", height_px, width_px ); Image1->Canvas->Font->Handle = CreateFontIndirect( &LFont1 ); Image1->Canvas->TextOut( 0, 0, L"扁平かける" ); LOGFONT LFont2 = CreateLogFont( L"游ゴシック", height_px, width_px ); Image2->Canvas->Font->Handle = CreateFontIndirect( &LFont2 ); Image2->Canvas->TextOut( 0, 0, L"扁平かける" ); } |
フォント名以外、全く同一の指定をしているはずなのに、結果はこうなってしまう。
右側の「游ゴシック」の方は、明らかに扁平率が違う。
しかも、天地の描画位置も微妙にずれている。等間隔に線を描画してみると分かる。
2.TEXTMETRIC構造体にない3メンバ
さて、”おかしな扁平” “ずれた位置”になってしまうことに関係しそうなのは、TEXTMETRIC構造体になく、NEWTEXTMETRICEX構造体には存在する以下の3メンバ。
Microsoftの解説は英語しかないが、google翻訳で和訳。
メンバ | 解説(Google翻訳) |
---|---|
ntmSizeEM | フォントの正方形のサイズを指定します。 この値は概念上の単位(つまり、フォントが設計された単位)になります。 |
ntmCellHeight | フォントの概念的な単位での高さを指定します。 この値は、ntmSizeEMメンバーの値と比較する必要があります。 |
ntmAvgWidth | フォントの平均文字幅を概念上の単位で指定します。 この値は、ntmSizeEMメンバーの値と比較する必要があります。 |
分かったような分からないような説明だが、ともかく、このNEWTEXTMETRIC構造体の値は、昔ながらのApi EnumFontFamiliesExのコールバック関数EnumFontFamProcで取得できる。
コードは後回しにして、先の2フォントの値はこうなっている。
MS ゴシック | 游ゴシック | |
---|---|---|
ntmSizeEM | 256 | 2048 |
ntmCellHeight | 256 | 2636 |
ntmAvgWidth | 128 | 1972 |
MSゴシックの場合はシンプルだ。ntmAvgWidthはntmSizeEMのちょうど半分。ntmSizeEMとntmCellHeightの値が全く同じ。
一方、游ゴシックはntmAvgWidthがntmSizeEMの半分より大きい。ntmCellHeightもtmSizeEMの値を上回る。
先ほどの描画結果とにらみ合わせてみると…。
以下のように推察される。
・ntmAvgWidth……ntmSizeEMと同じサイズにしたい場合、この値をlfWidthに指定して初めて正方になる
・ntmCellHeight……ntmSizeEMと同じサイズを指定した場合、描画時に、この値分の領域を上下に取る(つまりずれる)
3.3メンバを考慮した指定と描画
つまり、フォントに、望み通りの扁平(長体)をかけ、望み通りの位置に描画するには
(1)LOGFONTの指定時に、lfWidthにntmSizeEMとntmAvgWidthを考慮した値を指定する
(2)描画時の天地位置に、ntmSizeEMとntmCellHeightを考慮した値を指定する
ことが必要になる。
以下、サンプルコード。フォントごとにこれらの値を取得しておき、LOGFONT構造体作成時および描画時に値を調整する。必要な値はVclFormのコンストラクタで取得している。(丸め処理等は省略)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
// 正しい扁平・長体と描画位置調整に必要な値を格納する構造体: struct notional_size { int size_em; int cell_height; int avg_width; }; // フォント名と上記構造体を結びつけるmap: std::map<std::wstring,notional_size> font_nsize_map_; // EnumFontFamiliesEXのコールバック関数 int CALLBACK EnumFontFamExProc( ENUMLOGFONTEX* lpelf, NEWTEXTMETRICEX* lpntm, DWORD nFontType, LPARAM* lparam ) { std::wstring fontname = lpelf->elfLogFont.lfFaceName; struct notional_size nsize; nsize.size_em = lpntm->ntmTm.ntmSizeEM; nsize.cell_height = lpntm->ntmTm.ntmCellHeight; nsize.avg_width = lpntm->ntmTm.ntmAvgWidth; font_nsize_map_[fontname] = nsize; return 1; } // VclFormのコンストラクタ __fastcall TForm1::TForm1(TComponent* Owner) : TForm(Owner) { HDC DC = GetDC(0); LOGFONT LFont = {}; EnumFontFamiliesEx( DC, &LFont, (FONTENUMPROC)(EnumFontFamExProc), 0, 0 ); ReleaseDC(0, DC); } |
LOGFONT作成関数は以下のように書き換える
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
LOGFONT CreateLogFontEx( const std::wstring& fontname, int height_px, int width_px ) { LOGFONT LFont = {}; // 幅の指定に調整をかける notional_size nsize = font_nsize_map_[fontname]; LFont.lfWidth = ( width_px * nsize.avg_width ) / nsize.size_em; LFont.lfHeight = -height_px; wcscpy( LFont.lfFaceName, fontname.c_str() ); return LFont; } |
実際に描画するコードでは位置調整をする。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
void __fastcall TForm1::Button2Click(TObject *Sender) { const int height_px = 50, width_px = 70; std::wstring fontname1( L"MS ゴシック" ); // 描画位置の調整 struct notional_size nsize1 = font_nsize_map_[fontname1]; int y1 = -( height_px * ( nsize1.cell_height - nsize1.size_em ) ) / ( nsize1.size_em * 2.0 ); LOGFONT LFont1 = CreateLogFontEx( fontname1, height_px, width_px ); Image1->Canvas->Font->Handle = CreateFontIndirect( &LFont1 ); Image1->Canvas->TextOut( 0, y1, L"扁平かける" ); std::wstring fontname2( L"游ゴシック" ); // 描画位置の調整 struct notional_size nsize2 = font_nsize_map_[fontname2]; int y2 = -( height_px * ( nsize2.cell_height - nsize2.size_em ) ) / ( nsize2.size_em * 2.0 ); LOGFONT LFont2 = CreateLogFontEx( fontname2, height_px, width_px ); Image2->Canvas->Font->Handle = CreateFontIndirect( &LFont2 ); Image2->Canvas->TextOut( 0, y2, L"扁平かける" ); } |
実行してみると、望み通りの扁平描画になる。
天地位置も正常。
あらゆるOpenTypeフォントでうまくいくのかどうかは不明だが、手持ちのOpenTypeフォントでは上手くいっている。
C++Builder10.2.3、Windows10で確認。