Help us understand the problem. What is going on with this article?

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

More than 1 year has passed since last update.

はじめに

例えばこれが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の実装を見ていると

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:縁のみ */
)
{
    //中略

    // 改行時に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;とか後で見てわかる気がしない。

動作例

image.png

折り返しがある例

image.png

うん、いい感じですね。

参照

License

CC BY 4.0

CC-BY icon.svg

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした