imguiで日本語文字を出力するためにOSからのフォントを読み込みしたこと。
imgui
(厳密にいうとDear ImGui
)というGUIライブラリーはC++環境でUI窓を作らせてくれる便利なライブラリです。QtまたはNana、Juceなどに比べればちょっと違った構造を持っていますが、プロトタイプまたは既存のビデオゲームなどでのデバッグUIなどなどによく使われるらしいです。(有名ゲームメーカーのSEGAさんのソニックゲームのエンジンにも使われるらしいです。)
しかし、imgui
は基本的にはUnicodeの描画を実装していません。しかしUnicodeの文字に該当するglpyhを生成するAPIは存在しますので日本語、韓国語などの文字を描画するには以下のような関数を呼び出します。
// Create glyphs. ImGuiIO& io = ImGui::GetIO() io.Fonts->AddFontFromFileTTF(/* Font file path */, 18.0f, nullptr, io.Fonts->GetGlyphRangesJapanese());
/* Font file path */
には読み込みするフォントファイルの経路が入ります。普通は直接経路を指定することで文字コードに該当するGlyphを絞り出しますが、私はOSからフォントファイルが設置しているかを確認し、そしてフォントファイルの経路を取って便利にGlyphを生成するようにしたかったんです。
ですので実装してみたところ、余計に長い道のりの末に日本語の文字を描画することになりました。そしてもしかしてまた参考にするためにこの記事をもってやり方のメモをしたいと思います。
やり方
表の世界
// Check japanese (meiryo UI) font is exist on system. { std::string japaneseFontName = ""; if (windowManager.IsFontExistOnSystem(u8"Meiryo UI") == true) { japaneseFontName = u8"Meiryo UI"; } // If font name is found, add font file to imgui. Otherwise, just pass it. if (japaneseFontName.empty() == false) { // Get system font path from manager. const auto optPath = windowManager.GetFontPathOnSystem(japaneseFontName); MDY_ASSERT_FORCE(optPath.has_value() == true, "Unexpected error occurred."); // Create glyphs. io.Fonts->AddFontFromFileTTF(optPath.value().c_str(), 18.0f, nullptr, io.Fonts->GetGlyphRangesJapanese()); } }
「表の世界」では上のコードだけで読み込みが終わります。
- "Meiryo UI"というフォントがシステム(Windows)にあるかを
IsFontExistOnSystem
を使用して確認する。確認してあったら、フォントを読み込みすることが出来たといえる。探せなかった場合にはそのままに置く。 GetFontPathOnSystem
という関数を使用して、該当するフォントネームを持ったファイルの経路を取る。失敗する場合にはASSERTをする- 持ってきた経路を
ImGuiIO::AddFontFromFileTTF
を使用してGlyphを生成する。GetGlyphRangesJapanese
を使用してUnicodeでの日本語の文字コードの範囲を決める。 - PROFIT(終)
そして以下のように
ImGui::Text
で文字列を出力しようとしたら
ImGui::Text(u8"Hello world! お早う御座います。\n夜空の星が輝く陰で悪(ワル)の笑いがこだまする\n" u8"星から星に泣く人の涙背負って宇宙の始末!\n" u8"銀河旋風ブライガー! お呼びとあらば即、参上!!");
次のように文字がちゃんと見えるようになります。
ここで重要なのはOSからの情報を取得する関数、IsFontExistOnSystem
とGetFontPathOnSystem
です。ここからはWindows 7以上のOSに限定される内容となりますので、他のOSではまた別となる工夫が必要です。
裏の世界(こわい)
裏の世界では、WIN32のAPIを使用してフォントがあるかを確認します。そしてレジストリを探索して、フォントのデータベースでファイルの名前を探して完全な経路として返します。
IsFontExistOnSystem
bool DDyWindowInformationWindows::IsFontExistOnSystem(_MIN_ const std::string& iFontKey) const { HDC pDC = GetDC(NULL); LOGFONT logFont; // Set font information structure. logFont.lfCharSet = DEFAULT_CHARSET; logFont.lfPitchAndFamily = 0; // @reference https://www.gpgstudy.com/forum/viewtopic.php?t=7047 std::wstring fontKey; { USES_CONVERSION; fontKey = A2W(iFontKey.c_str()); } wcscpy(logFont.lfFaceName, fontKey.c_str()); // EnumFontFamiliesEXW (Multi-byte dirty function) returns `0` when found given font face-name. const auto flag = EnumFontFamiliesExW(pDC, &logFont, EnumFontFamExProc, reinterpret_cast<LPARAM>(&fontKey), 0); return flag == 0; }
int CALLBACK EnumFontFamExProc( _MIN_ const LOGFONT* lpelfe, _MIN_ const TEXTMETRIC* lpntme, _MIN_ DWORD FontType, _MIN_ LPARAM lparam) { std::wstring* ptrTargetFaceName = reinterpret_cast<std::wstring*>(lparam); if (*ptrTargetFaceName == lpelfe->lfFaceName) { return 0; } else { return 1; } }
EnumFontFamiliesExW
では、LOGFONT
構造体を引数として情報に当てはまるフォントのLOGFONT
構造体を、EnumFontFamExProc
というコールバック関数を呼んで何かを処理するようにしてくれます。(コールバック関数は直接作成しなければなりません。)コールバック関数は返す値が0だったらイテレーションを停止して関数の呼び出しを終了します。0じゃない場合には最後のフォント情報までコールバックを繰り返します。
ちょっと面倒くさいことが、WIN32ではWCHAR(マルチバイト文字)を使いますので、std::string
かchar
などを使う場合には文字列をstd::wstring
などに変換しなければなりません。ここで15年前の小テクニックを使用して簡単にwstring
に変換します。
// @reference https://www.gpgstudy.com/forum/viewtopic.php?t=7047 std::wstring fontKey; { USES_CONVERSION; fontKey = A2W(iFontKey.c_str()); // iFontKey is std::string. }
小テクニックを使うにはヘッダーファイル#include <atlconv.h>
が必要となります。
GetFontPathOnSystem
ここではRegOpenKeyEx
RegQueryInfoKey
RegEnumValue
などを使用して、レジストリのデータベースを探索します。フォントのテーブルがあるところはSoftware\\Microsoft\\Windows NT\\CurrentVersion\\Fonts
になります。
std::optional<std::string> DDyWindowInformationWindows::GetFontPathOnSystem(_MIN_ const std::string& iFontKey) const { // Check validity. if (this->IsFontExistOnSystem(iFontKey) == false) { return std::nullopt; } // @reference https://www.gpgstudy.com/forum/viewtopic.php?t=7047 std::wstring fontFacename; { USES_CONVERSION; fontFacename = A2W(iFontKey.c_str()); } // Find path using registry. static const auto fontRegistryPath = L"Software\\Microsoft\\Windows NT\\CurrentVersion\\Fonts"; HKEY hKey; { // Open Windows font registry key const auto result = RegOpenKeyEx(HKEY_LOCAL_MACHINE, fontRegistryPath, 0, KEY_READ, &hKey); if (result != ERROR_SUCCESS) { return std::nullopt; } } DWORD maxValueNameSize, maxValueDataSize; { const auto result = RegQueryInfoKey(hKey, 0, 0, 0, 0, 0, 0, 0, &maxValueNameSize, &maxValueDataSize, 0, 0); if (result != ERROR_SUCCESS) { return std::nullopt; } } DWORD valueIndex = 0; LPWSTR valueName = new WCHAR[maxValueNameSize]; LPBYTE valueData = new BYTE[maxValueDataSize]; DWORD valueNameSize, valueDataSize, valueType; std::wstring wsFontFile; { // Look for a matching font name LONG result; do { wsFontFile.clear(); valueDataSize = maxValueDataSize; valueNameSize = maxValueNameSize; result = RegEnumValue(hKey, valueIndex, valueName, &valueNameSize, 0, &valueType, valueData, &valueDataSize); valueIndex++; if (result != ERROR_SUCCESS || valueType != REG_SZ) { continue; } // Find a match. std::wstring wsValueName(valueName, valueNameSize); if (wsValueName.find(fontFacename) != std::wstring::npos) { wsFontFile.assign((LPWSTR)valueData, valueDataSize); break; } } while (result != ERROR_NO_MORE_ITEMS); } // Release temporary chunk. delete[] valueName; delete[] valueData; // Close registry. RegCloseKey(hKey); if (wsFontFile.empty()) { return std::nullopt; } // Build full font file path WCHAR winDir[MAX_PATH]; GetWindowsDirectory(winDir, MAX_PATH); std::wstringstream ss; ss << winDir << "\\Fonts\\" << wsFontFile; wsFontFile = ss.str(); return std::string(wsFontFile.begin(), wsFontFile.end()); }