はじめに
例えばこれがHTML5+CSS3ならば、中寄せで文字列を描画するなんてことは極めてかんたんにできる。
<div>
<p id="center">ありきたりな世界<p>
</div>
#center {
text-align: center;
}
こんな感じだろうか。しかしDxLibの場合どうやるんだ?
一般的にDxLibで文字列を描画するまでの流れ
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 | DXライブラリ質問掲示板
を見ればわかるように議論が発散してしまうから。
使うべきDxLibの関数を調べる
文字列描画
まず肝心の文字列描画だが、座標をfloat
であつかうことと、フォントハンドルを使うことから、DxLib::DrawStringFToHandle
を利用する。
描画時の文字列の幅
次に、文字列の折り返しを考えるので、文字列を描画したときの文字幅を調べたい。候補としては
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
- 宣言
int GetDrawStringCharInfoToHandle(DRAWCHARINFO *InfoBuffer, size_t InfoBufferSize, const TCHAR *String, int StrLen, int FontHandle, int VerticalFlag = FALSE);
- 概略
- フォントハンドルを使用した文字列の1文字毎の情報を取得する
- 引数
-
DRAWCHARINFO *InfoBuffer
- 文字の情報を格納する
DRAWCHARINFO
構造体の配列への先頭アドレス、NULL
でもOK size_t InfoBufferSize
- 文字の情報を格納する
DRAWCHARINFO
構造体の配列の要素数 const TCHAR *String
- 描画時の幅を調べたい文字列
int StrLen
- 調べたい文字列の長さ(byte)
int FontHandle
- 描画幅を取得する際に使用するフォントデータの識別番号
int VerticalFlag = FALSE
- 縦書きかどうかのフラグ、
FALSE
を指定すると横書きになります
- 戻り値
-
- -1以外
- 描画上の文字数、
InfoBuffer
の要素数はこれより大きい必要がある - -1
- エラー発生
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の実装を見ていると
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:縁のみ */
)
{
//中略
// 改行時にY座標に加算する値を算出
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
関数がヒットしました。
extern int GetFontLineSpaceToHandle(int FontHandle);
- 宣言
int GetFontLineSpaceToHandle(int FontHandle);
- 概略
- フォントハンドルの行間を取得する
- 引数
-
int FontHandle
- 描画幅を取得する際に使用するフォントデータの識別番号
- 戻り値
-
- -1以外
- 行間(ドット単位)
- -1
- エラー発生
GetDrawStringCharInfoToHandle
関数をSTLで使いやすくする。
要は配列を返すんですから、std::vector
の出番ですね!
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
を一度だけ呼ぶようにする作戦です。
実装する
あとはひたすら書くだけです。
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;
とか後で見てわかる気がしない。
動作例
折り返しがある例
うん、いい感じですね。
参照
- yumetodo / dxlib_n4092 — Bitbucket
- 中よせ文字 | DXライブラリ質問掲示板
- 改行対応DrawFormatString | DXライブラリ質問掲示板
- GetDrawStringWidthToHandle | DXライブラリ置き場 リファレンスページ
- 月の見える丘|ぱくたそフリー写真素材
- 真っ直ぐすぎる | 1st ―― in the rain | 長い長いお話。 | 星月夜の窓