概要
ImGui は試作やエンジニア向けのウィジェットツールキットとしてはプログラマーが扱いやすく大変便利です。しかし、一般的なPCに留まらない幅広いプラットフォーム、例えば3DSなど"特殊"なターゲットやツールチェインも考慮する特性から、どうしても最新の C++ 機能の使用やステートフルな実装を避けるため、一般的なPC向けに使う場合には軽いラッパーや実装上の工夫を行い、モダンな C++ 実装を噛ませた上で扱うのが便利で安全で善い事が多いでしょう。
ImGui 自体の紹介は Qiita でも最近幾つか見かけますのでそれらに譲る事にして、本記事では一般的なモダンな C++ を扱える、一般的なPC向け等の環境で ImGui を使う場合のコツを、ボタンの表示などに比べれば若干複雑となる"コンテキストメニュー"のステートフルな実装例を基に紹介します。
↑こんなのの実装例。
ソース
↓今回解説する"コンテキストメニュー"のステートフル実装のソースコードはこちら↓
- latest: https://github.com/usagi/usagi/blob/master/include/usagi/imgui/statefull_menu_type.hxx
- この記事の執筆時点でのバージョン: e11b5ee19ef8c79ba9a5387407c1a4fc17bb9d8f
解説
ImGui のウィジェット単位でのステート管理について
ImGui のステート管理には少々癖があるので先に簡単な例を示したい。
ImGui::Button の例
例えば単純なウィジェットパーツとしてボタンを扱う時には次のようなリソース管理がボタンのためにユーザーコードの実装として必要になる。
std::string label = "hoge";
std::string id = "xyz";
ImVec2 size;
auto clicked =
ImGui::Button
( ( id.empty() ? label : label + "###" + id ).c_str() // param-1 label & id
, size
);
↑は便宜上簡単のためローカル変数として書いたが、実際のところモダンな C++ 環境を対象にする場合は↓のようにクラスとして整理するとリソースを整理して扱い易くなる。(但し↓では簡単のため struct として public にメンバーを列べた簡単な例示とする。)
struct button
{
std::string label;
std::string id;
std::function< auto () -> void > on_click; // †1
ImVec2 size;
auto operator()() const
{
if
( ImGui::Button
( ( id.empty() ? label : label + "###" + id ).c_str()
, size
)
)
on_click();
}
};
†1: もちろん、 std::function
の代わりに必要なら boost::signals2::signal
を使い、 register_on_click
を用意して std::shared_ptr
から slot_type
に track_foreign
しても善いが、本記事では逸脱するのでこうして一言述べるに留めておく。
なお、この段階から既に ImGui を扱う上で大変重要な点として、 label
、より厳密には id
は通常重複して定義してはならないというルールがある。実際に重複してはならないのは id
で、これがうっかり重複して定義された場合、ImGuiは内部状態の管理手法やAPIの呼び出し位置と実際の処理の行われるタイミングがずれる仕様が絡み、ユーザーの意図した挙動で動作しなくなる。
ステートフルなコンテキストメニューを実装する
↑こんな調子でコンテキストメニューを実装するまでを↓で解説する。
扱う ImGui
の API 群
ImGui
の扱いに慣れていないと少々混乱しそうな数の API を触る必要があるので先に概要を整理しておく。
-
ImGui::BeginMenu
: メニューの定義を開始する API -
ImGui::EndMenu
: メニューの定義を終了する API -
ImGui::MenuItem
: 定義中のメニューにアイテムを定義する API -
ImGui::BeginPopup
: コンテキストメニューのようなポップアップの定義を開始する API -
ImGui::EndPopup
: ポップアップの定義を終了する API -
ImGui::OpenPopup
: ImGui の処理タイミングで定義済みのポップアップの表示を定義する API
以上の6つのAPIを扱う事になる。
ImGui の API における Begin/End 系
ImGui
の API には BeginMenu
と EndMenu
のようにセットで扱う必要のある API が幾つかあるが、ここにも少々の癖があるので事前に解説する。
ImGui
の Begin
/End
系 API の基本的なルールは次の通り。
-
Begin
系の API の戻り値はたいていbool
になっている。 -
End
系の API は対になるBegin
系の API が呼び出され、かつ戻り値がtrue
だった場合にのみ呼び出されなければならない。 -
Begin
系の API がtrue
を返したならば必ず対になるEnd
系の API を呼び出さなければならない。
つまり、ImGui
の Begin
/End
系の API を使う場合には、次のような実装がユーザーコードとして必要となる。
if ( ImGui::BeginXXX( ... ) )
{
...
ImGui::EndXXX( ... );
}
これを踏まえると、コンテキストメニューで扱う API は、
- Menu 系:
Begin
-->Item
-->End
- Popup 系:
Begin
-->End
-->Open
これら2系統だとわかる。そうなると後は簡単。
ネストしたメニュー構造の実装
↑APIの扱いがわかると簡単に思えるが、ここでコンテキストメニューは一般的にネストしたメニュー構造を取り、それをどのようにして ImGui
で扱えば良いか知る必要がある。
ImGui の Menu 系 API は次の特性を持ちネストした構造を実現できる。
-
ImGui::MenuItem
を呼び出すとネストしたメニュー構造の末端のリーフ的な、最終的なノードを定義できる。 -
ImGui::MenuBegin
/ImGui::MenuEnd
の中でImGui::MenuItem
に代わりImGui::MenuBegin
/ImGui::MenuEnd
を呼び出すとネストしたメニュー構造の途中の、ファイルシステムで言えばディレクトリー的な中間のノードを定義できる。
つまり、ネストしたメニュー構造は↓次のように実装すれば実現できると分かる。†2
auto operator()() const
{
// 第1階層のメニュー定義のルートノードを開始
if ( ImGui::BeginMenu( ROOT, ... ) )
{
// 第1階層に末端のメニューノード A を定義
if ( ImGui::MenuItem( ROOT-A, ... ) )
on_click( ROOT-A, ... );
// 第1階層から第2階層へネストを展開する中間のメニューノード B を定義
if ( ImGui::BeginMenu( ROOT-B ... ))
{
// 第2階層に末端のメニューノード ROOT-B-X を定義
if ( ImGui::MenuItem( ROOT-B-X, ... ) )
on_click( ROOT-B-X, ... );
// 第1階層から第2階層へネストしたメニュー定義 ROOT-B を終了
ImGui::EndMenu( ... );
}
// 第1階層のメニュー定義のルートノードを終了
ImGui::EndMenu( ... );
}
}
†2: ↑簡単のため API の引数はネストしたメニュー構造の定義を感覚として読み取りやすいように擬似的に書いた。
実際の実装
実際には愚直にメニューを定義するたびに↑のように書くのはロールプレイングゲームやシューティングの実装を1つの関数に if
と switch
の塊で定義するような事で通常は行わない。それでは実際の実装例を解説しよう。
実装上の重要な構造を抜粋して↓に示す。
struct statefull_menu_type
{
// ctor: https://github.com/usagi/usagi/blob/e11b5ee19ef8c79ba9a5387407c1a4fc17bb9d8f/include/usagi/imgui/statefull_menu_type.hxx#L28
statefull_menu_type( ... );
// https://github.com/usagi/usagi/blob/e11b5ee19ef8c79ba9a5387407c1a4fc17bb9d8f/include/usagi/imgui/statefull_menu_type.hxx#L37
auto operator()( ... );
// https://github.com/usagi/usagi/blob/e11b5ee19ef8c79ba9a5387407c1a4fc17bb9d8f/include/usagi/imgui/statefull_menu_type.hxx#L149
auto show( ... );
// https://github.com/usagi/usagi/blob/e11b5ee19ef8c79ba9a5387407c1a4fc17bb9d8f/include/usagi/imgui/statefull_menu_type.hxx#L65
auto clear( ... );
// https://github.com/usagi/usagi/blob/e11b5ee19ef8c79ba9a5387407c1a4fc17bb9d8f/include/usagi/imgui/statefull_menu_type.hxx#L78
auto remove( ... );
// https://github.com/usagi/usagi/blob/e11b5ee19ef8c79ba9a5387407c1a4fc17bb9d8f/include/usagi/imgui/statefull_menu_type.hxx#L83
auto add( ... );
// https://github.com/usagi/usagi/blob/e11b5ee19ef8c79ba9a5387407c1a4fc17bb9d8f/include/usagi/imgui/statefull_menu_type.hxx#L102
auto modify( ... );
private:
// https://github.com/usagi/usagi/blob/e11b5ee19ef8c79ba9a5387407c1a4fc17bb9d8f/include/usagi/imgui/statefull_menu_type.hxx#L155
auto _split( ... );
// https://github.com/usagi/usagi/blob/e11b5ee19ef8c79ba9a5387407c1a4fc17bb9d8f/include/usagi/imgui/statefull_menu_type.hxx#L162
auto _generate_random_number( ... );
// https://github.com/usagi/usagi/blob/e11b5ee19ef8c79ba9a5387407c1a4fc17bb9d8f/include/usagi/imgui/statefull_menu_type.hxx#L182
auto _remove( ... );
// https://github.com/usagi/usagi/blob/e11b5ee19ef8c79ba9a5387407c1a4fc17bb9d8f/include/usagi/imgui/statefull_menu_type.hxx#L225
auto _add( ... );
// https://github.com/usagi/usagi/blob/e11b5ee19ef8c79ba9a5387407c1a4fc17bb9d8f/include/usagi/imgui/statefull_menu_type.hxx#L259
auto _recursive_menu_render( ... );
// https://github.com/usagi/usagi/blob/e11b5ee19ef8c79ba9a5387407c1a4fc17bb9d8f/include/usagi/imgui/statefull_menu_type.hxx#L170
struct recursive_mapper_type;
};
この実装は↓次のように使える事を目的に書いた。
using usagi::imgui::statefull_menu_type;
// テストコードの例として簡単のため static にオブジェクトを定義する。
// 実際にはクラスのメンバー変数にする事が多いだろう。
static statefull_menu_type o;
static statefull_menu_type n;
static statefull_menu_type q;
// テストコードの例として簡単のためメニュー定義が空ならば初期化するよう定義する。
// 実際にはクラスの initialize タイミングで行う事が多いだろう。
if ( q.empty() )
// メニューの階層構造は ImGui らしい便利さを優先したい事が多いだろうから、
// テキストから簡単に階層構造を定義できるようにしてみた。
// また、コンテキストメニューは末端のノードがクリックされた際に何らかのイベントを処理したい用途が通常なので、
// イベントはラムダ式で放り込めるようにしてみた。
// ImGui では callback 系に `std::function` を採用できない事情があるためステートレスでなければならないが、
// 今回はラッパークラスを定義して任意の C++ ファンクターを扱えるので便利も善い。
q.add
( decltype( q )::value_type{ "aaa/bbb/ccc" , [&]{ q.clear(); std::cerr << 0; } }
, decltype( q )::value_type{ "aaa/bbb/ddd" , []{ std::cerr << 1; } }
, decltype( q )::value_type{ "aaa/xxx/yyy/zzz" , []{ std::cerr << 2; } }
, decltype( q )::value_type{ "aaa/xxx/yyy/www" , []{ std::cerr << 3; } }
, decltype( q )::value_type{ "ppp/xxx/sss/ttt" , []{ std::cerr << 4; } }
, decltype( q )::value_type{ "ppp/xxx/sss/uuu/vvv", []{ std::cerr << 5; } }
, decltype( q )::value_type{ "qqq/xxx/sss/ttt" , [&]{ q.remove( "qqq/xxx/sss/ttt" ); std::cerr << 6; } }
, decltype( q )::value_type{ "qqq/xxx/sss/uuu/vvv", [&]{ q.remove( "qqq/xxx/sss/uuu/vvv" ); std::cerr << 7; } }
);
if ( n.empty() )
n.add
( decltype( n )::value_type{ "nnn/1/2/3", [&]{ std::cerr << "n3 "; } }
, decltype( n )::value_type{ "nnn/4/5/6", [&]{ std::cerr << "n6 "; } }
, decltype( n )::value_type{ "nnn/7/8/9", [&]{ std::cerr << "n9 "; } }
);
if ( o.empty() )
o.add
( decltype( o )::value_type{ "ooo/1/2/3", [&]{ std::cerr << "o3 "; } }
, decltype( o )::value_type{ "ooo/4/5/6", [&]{ std::cerr << "o6 "; } }
, decltype( o )::value_type{ "ooo/7/8/9", [&]{ std::cerr << "o9 "; } }
);
// 蛇足的な注意となるが、ここでもボタンの ID は重複してはならない。
if ( ImGui::Button( "open context menu 1" ) )
// 実際にはクラスの update 処理中などに何らかのトリガーに応じて呼ぶことになるだろう。
q.show();
if ( ImGui::Button( "open context menu 2" ) )
n.show();
if ( ImGui::Button( "open context menu 3" ) )
o.show();
// 実際にはクラスの render 処理中に定義する事になるだろ。
q();
n();
o();
これは↓次のように動作してくれる。
実装の tips
- ctor
- ルートノードのIDはコンテキストメニューごとに重複しなければ善くユーザーが設定したいケースはそうそう無いので今回は乱数をIDに入れておいた。UUIDv4でも構わないし、より単純に現実的に重複しない情報量の整数値を単にインクリメントして適当な文字列と結合して使っても善いだろう。
-
operator()()
- ImGui API を実際に呼び付けて表示するメニューを定義するが、この際にポップアップ以外のメニューもこの実装だけで扱えるように
BeginPopup
/EndPopup
を挟むかどうかを設定可能に工夫してある。 - 実際のネストしたメニュー構造は
_recursive_menu_render
を再帰呼び出しする事で実装している。
- ImGui API を実際に呼び付けて表示するメニューを定義するが、この際にポップアップ以外のメニューもこの実装だけで扱えるように
-
_recursive_menu_render
- 処理すべき「ある階層のメニュー構造」を
recursive_mapper_type
として受け取り、末端ならばMenuItem
、そうでなければBeginMenu
-->_recursive_menu_render
-->EndMenu
を再帰呼び出しで実装している。
- 処理すべき「ある階層のメニュー構造」を
-
recursive_mapper_type
- 若干の手抜きのため
std::map
を基底に継承させたネストされたメニュー構造を定義している。std::map
を継承して使っては云々という場合は僅かに記述コストが増えるが単にメンバーにコンポジションする構造にすれば善い。 -
map::value_type
のpair::first
がメニューのラベル(≃今回実装ではID)、pair::second
がネストした子構造を持つ場合の子構造(ない場合には empty )とし、メニューの末端としてクリックされた場合に発火させたいファンクターをメンバー_f
として保持する構造とした。_recursive_menu_render
はこの再帰的にネスト可能としたmap
構造をベースにしたrecursive_mapper_type
型の値を range-based-for で単純に列挙しながら再帰的にメニュー構造を定義している。 -
std::map
をベースにしたのは単に key によって自動的にソートされる事を目的とした実装で、これは必要ならばstd::vector< std::pair< ... > >
として任意にソートしたり、あるいはより高度に Boost Multi-index Library を用いたり、列挙順序を制御したりできる実装にしても善い。
- 若干の手抜きのため
-
add
,remove
,clear
- これらは ImGui の内部処理のタイミングと API の呼び出しのタイミングのずれを考慮して、
add
された内容、remove
された内容を一端メンバー変数にバッファリングした上で、実際にはoperator()
時点でまとめて_remove
-->_add
または_clear
されるように実装してある。そのせいで少々複雑で冗長になっているが、いつでもadd
,remove
を呼び出せるため、テストコードのようにメニューの末端のノードへ設定するファンクター内からadd
,remove
,clear
を呼び出す事もできるようになっている。
- これらは ImGui の内部処理のタイミングと API の呼び出しのタイミングのずれを考慮して、
このような実装を施しながら使うと ImGui も一般的なPC向けの開発中に使うにあたって便利となる。
参考
おまけ
- この記事は 札幌C++勉強会が主催するオンラインもくもく会の から Qiita へ寄稿されました。
- ごめんなさい実はもくもくで記事を書いていて疲れてきたので解説がわりと手抜き感でてしまいました(ノω・)テヘ