C++
DXライブラリ
DxLib
nuklear
DxLibDay 19

DXライブラリ+nuklearでGUIを実現する

はじめに

2018/12/27追記

  • GetDrawStringWidthToHandle関数が今後利用できる旨を追記しました (Thanks @yumetodo さん)
  • テキスト描画に利用している関数をDrawStringToHandleからDrawNStringToHandleに変更しました

2018/12/21追記

  • 文中でGetDrawFormatStringWidthToHandle関数を利用していた箇所をGetDrawStringWidthToHandle関数へ変更しました(Thanks @8127 さん)
  • @yumetodo さんの指摘を記事に反映しました。
  • 記事末尾に、実際に書いたソースコードのすべてを追記しました。

この記事はDxLib Advent Calendar 2018 の19日目の記事です。

さて、DXライブラリを使っていて、困ること、ありませんか?
ありますよね?

そう、DXライブラリには、GUIがないのです。
GUIを自力でがりがりと書くのは割と大変で、心が折れることもしばしば、、、

この記事では、nuklearというGUIライブラリをDXライブラリに導入して
GUI作成を楽にすることを目指します。

nuklearで、快適なGUIライフを!

※OpenSiv3D版は こちら

目指す画面はこんな感じ
DxNk.PNG

環境

この記事は、以下のバージョンのいろいろなものを利用して執筆されました。

  • DXライブラリ : Ver 3.19d
  • nuklear : 4.00.2(2018/10/31)
  • Visual Studio 2017 : 15.9.4

準備編

まず、準備が必要です。

  1. DXライブラリ置き場 使い方説明 の手順に従ってDXライブラリの準備をする
  2. GitHub - vurtun/nuklear: A single-header ANSI C gui library からnuklear.hをダウンロードする。
  3. nuklear.hをプロジェクトに追加する。

ヘッダオンリーライブラリなので、導入がとても簡単なのが一つのメリットですね。

実装編

nuklearの初期化

nuklearを実行するためには、まず初期化する必要があります。
nuklearの初期化では、主にフォントサイズの設定を行います。

    // nuklearの初期化
    struct nk_context ctx;
    const auto dx_font = CreateFontToHandle(nullptr, 13, 1);
    struct nk_user_font nk_font;
    // フォントハンドル
    nk_font.userdata = nk_handle_id(dx_font);
    // フォントの高さ
    nk_font.height = static_cast<float>(13);
    // 文字列の幅を計算する関数を登録
    nk_font.width = [](const nk_handle handle, float h, const char* str, const int len) -> float {
        return static_cast<float>(GetDrawStringWidthToHandle(str, len, handle.id));
    };
    nk_init_default(&ctx, &nk_font);

基本的に、nk_user_font構造体にいろいろ値を入れてnk_init_default関数に突っ込めばよいのですが、
一つだけ注意点があります。

nk_user_font構造体のwidthに突っ込む関数の引数にchar* strがありますが、
こいつがゼロ終端されていないので、lenを用いてちゃんと長さを指定する必要があります。

strがゼロ終端されていると思い込んでしまうと、なんか変になります。

今回利用しているGetDrawStringWidthToHandle関数は第二引数に文字列長を指定するので大丈夫かと思いきや
yumetodoさんの指摘によると大丈夫じゃないようです。

↑DXライブラリの次バージョンで対応されるため大丈夫になりました!

入力処理

ここからは、初期化の後、メインループで行う処理に入っていきます。

まず最初に、nuklearに現在の入力を教えてあげます。

nk_input_beginとnk_input_endの間で、入力を教える関数を実行します。

    // nuklearの入力処理
    nk_input_begin(&ctx);
    auto mouse_x = 0;
    auto mouse_y = 0;
    const auto click = GetMouseInput();
    GetMousePoint(&mouse_x, &mouse_y);
    // マウスの位置を教える
    nk_input_motion(&ctx, mouse_x, mouse_y);
    // マウスのクリック状態を教える
    nk_input_button(&ctx, NK_BUTTON_LEFT, mouse_x, mouse_y, (click & MOUSE_INPUT_LEFT) != 0);
    nk_input_button(&ctx, NK_BUTTON_RIGHT, mouse_x, mouse_y, (click & MOUSE_INPUT_RIGHT) != 0);
    nk_input_button(&ctx, NK_BUTTON_MIDDLE, mouse_x, mouse_y, (click & MOUSE_INPUT_MIDDLE) != 0);
    // マウスホイールの状態を教える
    nk_input_scroll(&ctx, nk_vec2(GetMouseHWheelRotVolF(), GetMouseWheelRotVolF()));
    nk_input_end(&ctx);

ここでは、簡略化のためにマウスの入力だけ渡しているのですが
テキスト入力を使わないのであれば、マウスの入力だけで何とかなります。

描画処理

次は描画を行います。

nuklearが発行するコマンドをforループで消化していきます。

この部分がキモであり、一番重要なのですが、長いので一部だけ紹介します。
すべてのソースコードは最後に張り付けます!

    const struct nk_command* cmd;
    // nuklearの描画
    nk_foreach(cmd, &ctx) {
        switch (cmd->type) {
        case NK_COMMAND_NOP:
            break;
        case NK_COMMAND_SCISSOR: {
            const auto s = reinterpret_cast<const struct nk_command_scissor*>(cmd);
            SetDrawArea(s->x, s->y, s->x + s->w, s->y + s->h);
        } break;
        case NK_COMMAND_LINE: {
            const auto l = reinterpret_cast<const struct nk_command_line *>(cmd);
            SetDrawBlendMode(DX_BLENDMODE_ALPHA, l->color.a);
            DrawLine(l->begin.x, l->begin.y, l->end.x, l->end.y, GetColor(l->color.r, l->color.g, l->color.b));
            SetDrawBlendMode(DX_BLENDMODE_NOBLEND, 0);
        } break;
        case NK_COMMAND_RECT: {
            const auto r = reinterpret_cast<const struct nk_command_rect *>(cmd);
            SetDrawBlendMode(DX_BLENDMODE_ALPHA, r->color.a);
            DrawBox(r->x, r->y, r->x + r->w, r->y + r->h, GetColor(r->color.r, r->color.g, r->color.b), FALSE);
            SetDrawBlendMode(DX_BLENDMODE_NOBLEND, 0);
        } break;
        case NK_COMMAND_RECT_FILLED: {
            const auto r = reinterpret_cast<const struct nk_command_rect_filled *>(cmd);
            SetDrawBlendMode(DX_BLENDMODE_ALPHA, r->color.a);
            DrawRoundRect(r->x, r->y, r->x + r->w, r->y + r->h, r->rounding, r->rounding, GetColor(r->color.r, r->color.g, r->color.b), TRUE);
            SetDrawBlendMode(DX_BLENDMODE_NOBLEND, 0);
        } break;
        case NK_COMMAND_CIRCLE: {
            const auto c = reinterpret_cast<const struct nk_command_circle *>(cmd);
            SetDrawBlendMode(DX_BLENDMODE_ALPHA, c->color.a);
            DrawOval(c->x + c->w / 2, c->y + c->h / 2, c->w / 2, c->h / 2, GetColor(c->color.r, c->color.g, c->color.b), FALSE, c->line_thickness);
            SetDrawBlendMode(DX_BLENDMODE_NOBLEND, 0);
        } break;
        case NK_COMMAND_CIRCLE_FILLED: {
            const auto c = reinterpret_cast<const struct nk_command_circle_filled *>(cmd);
            SetDrawBlendMode(DX_BLENDMODE_ALPHA, c->color.a);
            DrawOval(c->x + c->w / 2, c->y + c->h / 2, c->w / 2, c->h / 2, GetColor(c->color.r, c->color.g, c->color.b), TRUE);
            SetDrawBlendMode(DX_BLENDMODE_NOBLEND, 0);
        } break;
        case NK_COMMAND_TEXT: {
            const auto t = reinterpret_cast<const struct nk_command_text*>(cmd);
            const auto font_handle = t->font->userdata.id;
            SetDrawBlendMode(DX_BLENDMODE_ALPHA, t->foreground.a);
            DrawNStringToHandle(t->x, t->y, static_cast<const char*>(t->string), t->length, GetColor(t->foreground.r, t->foreground.g, t->foreground.b), font_handle);
            SetDrawBlendMode(DX_BLENDMODE_NOBLEND, 0);
        } break;
       //他にもあるけど省略、、、
        default:
          break;
        }
    }
    nk_clear(&ctx);

nuklearを実際に使うところ

ここまでで、ようやっとnuklearを利用する準備が整いました。

ハローワールド的な簡単なウインドウを表示してみます。
※はじめにに張り付けている画像を表示するためのコードです。

    // nuklearのレイアウト作成
    enum { EASY, HARD };
    static int op = EASY;
    static float value = 0.6f;
    static int i = 20;

    if (nk_begin(&ctx, "Show", nk_rect(50, 50, 220, 220),
        NK_WINDOW_BORDER | NK_WINDOW_MOVABLE | NK_WINDOW_CLOSABLE)) {
        /* fixed widget pixel width */
        nk_layout_row_static(&ctx, 30, 80, 1);
        if (nk_button_label(&ctx, "button")) {
            /* event handling */
        }

        /* fixed widget window ratio width */
        nk_layout_row_dynamic(&ctx, 30, 2);
        if (nk_option_label(&ctx, "easy", op == EASY)) op = EASY;
        if (nk_option_label(&ctx, "hard", op == HARD)) op = HARD;

        /* custom widget pixel width */
        nk_layout_row_begin(&ctx, NK_STATIC, 30, 2);
        {
            nk_layout_row_push(&ctx, 50);
            nk_label(&ctx, "Volume:", NK_TEXT_LEFT);
            nk_layout_row_push(&ctx, 110);
            nk_slider_float(&ctx, 0, &value, 1.0f, 0.1f);
        }
        nk_layout_row_end(&ctx);
    }
    nk_end(&ctx);

まとめ

駆け足でしたが、nuklearをDXライブラリで利用する方法でした。

本記事で解説した部分のソースコードはgitlabのスニペットに上げています。

nuklear with DxLib

ライセンスはパブリックドメインの予定なので、煮るなり焼くなりご自由にどうぞ。
次はスタイルもしくは、、、nuklearのウィジェット一覧を、書き、、、書きたいです、、、

また、本記事を書くためにお試しで書いていたすべてのソースコードを以下に上げました。
今回省略したテキスト入力などの部分も全部書いてあります(すごく汚いですけど、、、、)
Tatsunoko / dxnk · GitLab