DxLibで中寄せで文字列を描画するのはどのくらい大変なんだろうか？

# はじめに

例えばこれがHTML5+CSS3ならば、中寄せで文字列を描画するなんてことは極めてかんたんにできる。

```html
<div>
  <p id="center">ありきたりな世界<p>
</div>
```

```css
#center {
  text-align: center;
}
```

こんな感じだろうか。しかしDxLibの場合どうやるんだ？

# 一般的にDxLibで文字列を描画するまでの流れ

```cpp
const auto font = DxLib::CreateFontToHandle(nullptr, 22, 3, DX_FONTTYPE_ANTIALIASING);
const auto yellow = DxLib::GetColor(255, 255, 0);
DxLib::DrawStringToHandle(0, 0, _T("ありきたりな世界"), yellow, font);
```

``DxLib::CreateFontToHandle``でフォントハンドルを作成し、``DxLib::GetColor``で色を指定して``DxLib::DrawStringToHandle``で文字列を描画する。

# じゃあ中寄せで描画してくれる``DrawStringToHandle``を探せばいいんだね！

しかしそんなものはない。

# 自作しよう

止む得ない。自作だ。

## 要件定義

- 描画可能領域(四角形)をうけとり、それに収まるように描画する
- 文字列は横書きで描画する
- 横方向のはみ出しは、折り返してはみ出さないようにする
- 折り返したものも含め、すべでの行は描画可能領域の横方向に対し中寄せされているように描画する
- 制御文字(改行文字含む)は考慮しない
- 実際に描画した範囲の底辺のy座標を返す
- 座標はすべて``flaot``型で扱う
- フォントハンドルを受け取って利用する

制御文字(改行文字含む)は考慮しないとしたのはなんでかというと、

[改行対応DrawFormatString | ＤＸライブラリ質問掲示板](http://dxlib.o.oo7.jp/cgi/patiobbs/patio.cgi?mode=view&no=3778)
を見ればわかるように議論が発散してしまうから。

## 使うべきDxLibの関数を調べる

### 文字列描画

まず肝心の文字列描画だが、座標を``float``であつかうことと、フォントハンドルを使うことから、``DxLib::DrawStringFToHandle``を利用する。

### 描画時の文字列の幅

次に、文字列の折り返しを考えるので、文字列を描画したときの文字幅を調べたい。候補としては

```cpp
extern int GetDrawStringWidthToHandle(const TCHAR *String, int StrLen, int FontHandle, int VerticalFlag = FALSE);
extern int GetDrawStringCharInfoToHandle(DRAWCHARINFO *InfoBuffer, size_t InfoBufferSize, const TCHAR *String, int StrLen, int FontHandle, int VerticalFlag = FALSE);
```

の2つがある。


#### ``GetDrawStringWidthToHandle``

http://dxlib.o.oo7.jp/function/dxfunc_graph2.html#R17N13
に解説がある。``StrLen``に指定するのはbyte数である。

#### ``GetDrawStringCharInfoToHandle``

<dl>
  <dt>宣言</dt>
  <dd><code>int GetDrawStringCharInfoToHandle(DRAWCHARINFO *InfoBuffer, size_t InfoBufferSize, const TCHAR *String, int StrLen, int FontHandle, int VerticalFlag = FALSE);</code></dd>
  <dt>概略</dt>
  <dd>フォントハンドルを使用した文字列の１文字毎の情報を取得する</dd>
  <dt>引数</dt>
  <dd>
    <dl>
      <dt><code>DRAWCHARINFO *InfoBuffer</code></dt>
      <dd>文字の情報を格納する<code>DRAWCHARINFO</code>構造体の配列への先頭アドレス、<code>NULL</code>でもOK</dd>
      <dt><code>size_t InfoBufferSize</code></dt>
      <dd>文字の情報を格納する<code>DRAWCHARINFO</code>構造体の配列の要素数</dd>
      <dt><code>const TCHAR *String</code></dt>
      <dd>描画時の幅を調べたい文字列</dd>
      <dt><code>int StrLen</code></dt>
      <dd>調べたい文字列の長さ(byte)</dd>
      <dt><code>int FontHandle</code></dt>
      <dd>描画幅を取得する際に使用するフォントデータの識別番号</dd>
      <dt><code>int VerticalFlag = FALSE</code></dt>
      <dd>縦書きかどうかのフラグ、<code>FALSE</code>を指定すると横書きになります</dd>
    </dl>
  </dd>
  <dt>戻り値</dt>
  <dd>
    <dl>
      <dt>-1以外</dt>
      <dd>描画上の文字数、<code>InfoBuffer</code>の要素数はこれより大きい必要がある</dd>
      <dt>-1</dt>
      <dd>エラー発生</dd>
    </dl>
  </dd>
</dl>

```cpp
typedef struct tagDRAWCHARINFO
{
	TCHAR					Char[ 13 ] ;				// 文字
	BYTE					Bytes ;						// 文字のバイト数
	float					DrawX, DrawY ;				// 描画位置
	float					SizeX, SizeY ;				// 描画サイズ
} DRAWCHARINFO, *LPDRAWCHARINFO ;
```

``Char``の要素数は13ありますが、これは将来的にDxLibがUnicodeの結合文字列を正しく扱えるようになったときのための予約で実際には13も使いません。格納されいているbyte数(要素数ではない)は``Bytes``が該当します。要素数がほしいなら``Bytes/sizeof(TCHAR)``とかしましょう。

``DrawX``は指定した文字列を``(0, 0)``に描画したときの対象文字の左上のx座標です。``DrawY``はいらない子です。

``DrawX + SizeX``は次の文字の``DrawX``と等しくなります。

#### どれを使うか

折り返しを考慮すると、``GetDrawStringCharInfoToHandle``を呼ばないとどの文字までが一行に収まるかわかりません。で、この2つの関数ですが、内部ではフォントハンドルに紐付いたフォントグリフの情報を取りに行っているので、あんまり何度も呼び出したくないです。そこで``GetDrawStringCharInfoToHandle``のみを使うことにしましょう

### 改行したときのy座標を決定する

改行したときのy座標はどうすればいいのかと思ってDxLibの実装を見ていると

```cpp:DxFont.cpp
extern int FontCacheStringDrawToHandleST(
	int				DrawFlag,
	int				xi,
	int				yi,
	float			xf,
	float			yf,
	int				PosIntFlag,
	int				ExRateValidFlag,
	double			ExRateX,
	double			ExRateY,
	int				RotateValidFlag,
	float			RotCenterX,
	float			RotCenterY,
	double			RotAngle, 
	const wchar_t *	StrData,
	unsigned int	Color,
	MEMIMG *		DestMemImg,
	const RECT *	ClipRect,
	int				TransFlag,
	FONTMANAGE *	ManageData,
	unsigned int	EdgeColor,
	int				StrLen,
	int				VerticalFlag,
	SIZE *			DrawSize,
	int *			LineCount,
	DRAWCHARINFO *	CharInfos,
	size_t			CharInfoBufferSize,
	int *			CharInfoNum,
	int				OnlyType /* 0:通常描画 1:本体のみ 2:縁のみ */
)
{
	//中略

	// 改行時にＹ座標に加算する値を算出
	if( VerticalFlag == TRUE )
	{
		DrawPosSubAdd = ( ManageData->LineSpaceValidFlag ? ManageData->LineSpace : ManageData->BaseInfo.FontHeight ) * ExRateX ;
	}
	else
	{
		DrawPosSubAdd = ( ManageData->LineSpaceValidFlag ? ManageData->LineSpace : ManageData->BaseInfo.FontHeight ) * ExRateY ;
	}
}
```

というのを見つけました。``ManageData->LineSpaceValidFlag ? ManageData->LineSpace : ManageData->BaseInfo.FontHeight``が大事そうです。探してみると``DxLib::GetFontLineSpaceToHandle``関数がヒットしました。

```cpp
extern int GetFontLineSpaceToHandle(int FontHandle);
```

<dl>
  <dt>宣言</dt>
  <dd><code>int GetFontLineSpaceToHandle(int FontHandle);</code></dd>
  <dt>概略</dt>
  <dd>フォントハンドルの行間を取得する</dd>
  <dt>引数</dt>
  <dd>
    <dl>
      <dt><code>int FontHandle</code></dt>
      <dd>描画幅を取得する際に使用するフォントデータの識別番号</dd>
    </dl>
  </dd>
  <dt>戻り値</dt>
  <dd>
    <dl>
      <dt>-1以外</dt>
      <dd>行間(ドット単位)</dd>
      <dt>-1</dt>
      <dd>エラー発生</dd>
    </dl>
  </dd>
</dl>

## ``GetDrawStringCharInfoToHandle``関数をSTLで使いやすくする。

要は配列を返すんですから、``std::vector``の出番ですね！

```cpp
namespace draw_string_center_impl {
	std::vector<DRAWCHARINFO> get_draw_string_char_info(const std::basic_string<TCHAR>& string, int font_handle) {
		std::vector<DRAWCHARINFO> info;
		info.resize(string.size());
		auto char_info_num = GetDrawStringCharInfoToHandle(info.data(), info.size(), string.c_str(), string.length() * sizeof(TCHAR), font_handle, false);
		if (char_info_num < 0) throw std::runtime_error("fail in function DxLib::GetDrawStringCharInfoToHandle");
		if (info.size() < static_cast<std::size_t>(char_info_num)) {
			info.resize(char_info_num + 1);
			//再取得
			char_info_num = GetDrawStringCharInfoToHandle(info.data(), info.size(), string.c_str(), string.length() * sizeof(TCHAR), font_handle, false);
			if (char_info_num < 0 || info.size() < static_cast<std::size_t>(char_info_num)) throw std::runtime_error("fail to detect draw info.");
		}
		info.resize(char_info_num);
		return info;
	}
}
```

こんな感じでラップできます。とりあえず文字列の要素数だけ確保してやることでなるべく``GetDrawStringCharInfoToHandle``を一度だけ呼ぶようにする作戦です。

## 実装する

あとはひたすら書くだけです。

```cpp
float draw_string_center(
	float draw_area_x_left, float draw_area_x_right, float draw_area_y_top, float draw_area_y_bottom,
	const std::basic_string<TCHAR>& string,
	unsigned int color, int font_handle,
	unsigned int edge_color = 0
)
{
	if (0 == string.length())  throw std::invalid_argument("empty string not allowed.");
	if (draw_area_x_right < draw_area_x_left || draw_area_y_bottom < draw_area_y_top) throw std::invalid_argument("");

	//一文字ずつの描画幅情報を取得する
	const auto info = draw_string_center_impl::get_draw_string_char_info(string, font_handle);

	//ManageData->LineSpaceValidFlag ? ManageData->LineSpace : ManageData->BaseInfo.FontHeight
	const auto line_space = DxLib::GetFontLineSpaceToHandle(font_handle);
	const float area_width = draw_area_x_right - draw_area_x_left;
	const auto total_draw_width = info.back().DrawX + info.back().SizeX - info.front().DrawX;
	if (total_draw_width <= area_width) {
		//一行ですむ場合
		const float padding = (area_width - total_draw_width) / 2.0f;
		DxLib::DrawStringFToHandle(draw_area_x_left + padding, draw_area_y_top, string.c_str(), color, font_handle, edge_color, false);
		return static_cast<float>(line_space);
	}

	//複数行になる場合

	const float area_height = draw_area_y_bottom - draw_area_y_top;

	//描画開始
	std::size_t current_string_byte_pos = 0;
	std::size_t line_front_string_byte_pos = 0;
	float current_y_relative = 0.0f;
	auto line_front_it = info.begin();
	for (auto it = info.begin(); it < info.end(); current_string_byte_pos += it->Bytes, ++it) {
		const auto line_width_contain_current_it_point_char = it->DrawX + it->SizeX - line_front_it->DrawX;
		if (area_width < line_width_contain_current_it_point_char) {
			using namespace std::string_literals;
			//次の行に行く前に描画、itが指す文字は含まない
			const std::size_t str_len_byte = current_string_byte_pos - line_front_string_byte_pos;
			//it->DrawXは前の文字の右端に等しい
			const float line_width = it->DrawX - line_front_it->DrawX;
			const float padding = (area_width - line_width) / 2.0f;
			const auto line_string = string.substr(line_front_string_byte_pos / sizeof(TCHAR), (str_len_byte / sizeof(TCHAR)));
			DxLib::DrawStringFToHandle(draw_area_x_left + padding, draw_area_y_top + current_y_relative, line_string.c_str(), color, font_handle, edge_color, false);
			//itが指す文字が先頭になる
			line_front_string_byte_pos = current_string_byte_pos;
			current_y_relative += line_space;
			line_front_it = it;
			if (area_height < current_y_relative) return current_y_relative;//描画可能領域(y)を超えたら終了
		}
	}
	//最終行の描画
	const auto last_line_width = info.back().DrawX + info.back().SizeX - line_front_it->DrawX;
	const float padding = (area_width - last_line_width) / 2.0f;
	const auto line_string = string.substr(line_front_string_byte_pos / sizeof(TCHAR));
	DxLib::DrawStringFToHandle(draw_area_x_left + padding, draw_area_y_top + current_y_relative, line_string.c_str(), color, font_handle, edge_color, false);
	return current_y_relative + line_space;
}
```

もうちょっとスッキリ書きたかったなと言う思い。横が溢れたらその一文字前までを描画というルーチンを書くのがわりとだるい。``it->DrawX - line_front_it->DrawX;``とか後で見てわかる気がしない。

## 動作例

![image.png](https://qiita-image-store.s3.amazonaws.com/0/94177/989786eb-6f1f-65f7-349c-fdadbdbdc10b.png)


折り返しがある例

![image.png](https://qiita-image-store.s3.amazonaws.com/0/94177/a9ddbaf6-d843-7176-a0cd-74a639ecd694.png)

うん、いい感じですね。

# 参照

- [yumetodo / dxlib_n4092 — Bitbucket](https://bitbucket.org/yumetodo/dxlib_n4092)
- [中よせ文字 | ＤＸライブラリ質問掲示板](http://dxlib.o.oo7.jp/cgi/patiobbs/patio.cgi?mode=view&no=4092)
- [改行対応DrawFormatString | ＤＸライブラリ質問掲示板](http://dxlib.o.oo7.jp/cgi/patiobbs/patio.cgi?mode=view&no=3778)
- [GetDrawStringWidthToHandle | ＤＸライブラリ置き場　リファレンスページ](http://dxlib.o.oo7.jp/function/dxfunc_graph2.html#R17N13)
- [月の見える丘｜ぱくたそフリー写真素材](https://www.pakutaso.com/20150142021post-5083.html)
- [真っ直ぐすぎる | 1st ―― in the rain | 長い長いお話。 | 星月夜の窓](http://web.archive.org/web/20040921134611/http://hosidukiyo.hp.infoseek.co.jp/novels/main08/m08_04.html)

# License

[CC BY 4.0](https://creativecommons.org/licenses/by/4.0/deed.ja)

<a href="https://commons.wikimedia.org/wiki/File:CC-BY_icon.svg#/media/File:CC-BY_icon.svg"><img src="https://upload.wikimedia.org/wikipedia/commons/thumb/1/16/CC-BY_icon.svg/1200px-CC-BY_icon.svg.png" alt="CC-BY icon.svg" width="88px"></a>