LoginSignup
10
4

More than 5 years have passed since last update.

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

Last updated at Posted at 2018-12-18

はじめに

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

10
4
6

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
10
4