Edited at
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