NEUROMANTIC

自分でC/C++/UE4/Graphics/ゲームなどをやったことをメモするブログ

imguiで日本語文字を出力するためにOSからのフォントを読み込みしたこと。

github.com

imgui(厳密にいうとDear ImGui)というGUIライブラリーはC++環境でUI窓を作らせてくれる便利なライブラリです。QtまたはNana、Juceなどに比べればちょっと違った構造を持っていますが、プロトタイプまたは既存のビデオゲームなどでのデバッグUIなどなどによく使われるらしいです。(有名ゲームメーカーのSEGAさんのソニックゲームのエンジンにも使われるらしいです。)

f:id:neuliliilli:20190211135801p:plain
基本的にはASCII以外の文字は「?」文字で化けてしまいます。

しかし、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());
  }
}

「表の世界」では上のコードだけで読み込みが終わります。

  1. "Meiryo UI"というフォントがシステム(Windows)にあるかをIsFontExistOnSystemを使用して確認する。確認してあったら、フォントを読み込みすることが出来たといえる。探せなかった場合にはそのままに置く。
  2. GetFontPathOnSystemという関数を使用して、該当するフォントネームを持ったファイルの経路を取る。失敗する場合にはASSERTをする
  3. 持ってきた経路をImGuiIO::AddFontFromFileTTFを使用してGlyphを生成する。GetGlyphRangesJapaneseを使用してUnicodeでの日本語の文字コードの範囲を決める。
  4. PROFIT(終)
    f:id:neuliliilli:20190211140550p:plain
    情け無用
    そして以下のようにImGui::Textで文字列を出力しようとしたら
ImGui::Text(u8"Hello world! お早う御座います。\n夜空の星が輝く陰で悪(ワル)の笑いがこだまする\n"
u8"星から星に泣く人の涙背負って宇宙の始末!\n"
u8"銀河旋風ブライガー! お呼びとあらば即、参上!!");

次のように文字がちゃんと見えるようになります。

ここで重要なのはOSからの情報を取得する関数、IsFontExistOnSystemGetFontPathOnSystemです。ここからは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::stringcharなどを使う場合には文字列を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());
}

参照

stackoverflow.com

www.gpgstudy.com