🎚️

OpenSiv3Dで柔軟なUIデザインを!ライブラリFlexLayoutの紹介

2024/12/24に公開

この記事はSiv3D Advent Calendar 2024 22日目の記事です

OpenSiv3D v0.6.xのGUI事情

OpenSiv3D v0.6.xでは、簡単に動かせるGUIとしてSimpleGUIが公式に提供されています。
https://zenn.dev/reputeless/books/siv3d-documentation/viewer/tutorial-gui

これらは表示位置をプログラムでVec2{ 10, 10 }などと直打ちするようになっており、文字通りシンプルな構成となっています。

何を作ったの?

今回は、このUIレイアウトをより細かく直感的に作成することができるライブラリ、「FlexLayout」を開発しました。特徴は次のとおりです:

  • Webデザイナーでドラッグ&ドロップしてUIが作れる
  • フレックスボックスにより細かな表現が可能
    • 縦,横に並べる
    • 画面端に寄せる
    • 横幅いっぱいに広げる
    • など
  • レイアウトファイルがC++から独立しており、結果を確認しながら微調整できる
    (調整のたびにビルドし直す必要がない)

thumbnail

使い方

ここからはWebデザイナーとC++側ライブラリの使い方を説明します。

Webデザイナー

Webデザイナーはブラウザで動作するアプリです。レイアウトファイルであるXMLファイルを読み込み,編集,保存することができます。
https://sthairno.github.io/flexlayout-designer

ドラッグ&ドロップ

初めてページを開くと、真ん中にHello, World!と書いてある画面が表示されます。
左側のメニューから適当なものを選んで黒いところにドラッグしてみましょう。すると、選んだUIが追加されます。

クイック編集ボタン

Boxをクリックして選択すると、右上にいくつかボタンが表示されます。基本的なレイアウトはこのボタンで編集することができ、数クリックで配置の向き,配置,子要素の間隔,折り返しなどを操作できます。

プロパティ

右側のPropertyタブではより詳細な設定ができます。Textはダブルクリックで直接中のテキストを編集することもできます。

ファイル操作、共有リンク作成

右上の大きなボタンはファイルの読み書きと共有のボタンです。Save FileボタンでXMLファイルを保存できます。


挙動はブラウザによってまちまちです。PC版Firefoxの場合はこんな感じ。

Chrome系の一部ブラウザではSaveボタンに自動保存メニューが表示されることがあります。これをONにすると変更後にすぐ自動保存されるようになり、後述するホットリロード機能との組み合わせで変更がすぐにSiv3Dに反映されます。

共有ボタンは作成したデザインをパーマリンクとしてURLに埋め込むことができます。

デモで作成したデザインのURLはこんな感じです↓

https://sthairno.github.io/flexlayout-designer/#pj:eJxrZcwNTswtyElV8EmszC8taZJkSpVkPN-a7JRfkeqTmJSaUxCcCZJ3D_XUcyotKcnPK0UIOOfn5BcFZCZnpxYhKQvOyUxJLWpmWJhaXFKZk7pKQj7F0KCgQkIOQslAKFkIpZRsWlDBCSIEs9JyUit0i0sSi0oY04tLilJLkjO405Lzc0pz83i_t8INXAkTRNKMzw6eREPe783NjAszkjMyc1KKUvPSPEtScxUMG5oZF8HFCj3y01NBWMGtND0RhCG2LeX5zvudP8UYaJDwdwnltOTUvJLUogZM44wbmpkQYumOENDQzLygoZllAYqGXI_UnJx8HYXw_KKcFMUGAKH-jIw

ほかにもサンプルを用意したので、よければ見てみてください

C++側ライブラリ

先ほど作成したXMLをSiv3Dで読み込むにはひと手間必要です。
READMEの導入方法にしたがい、OpenSiv3DをインストールしたディレクトリにFlexLayoutのライブラリをコピペしてインストールしてください。

https://github.com/sthairno/FlexLayout

最小コード

こちらがSiv3Dでファイルを読み込む最小コードです。

Main.cpp
#include <Siv3D.hpp>
#include <FlexLayout.hpp>

void Main()
{
    FlexLayout::Layout layout;

    // レイアウトファイルを読み込み
    layout.load(U"<XMLファイルのパス>");

    while (System::Update())
    {
        // 更新処理 (シーンの大きさに合わせて調整)
        layout.updateAll(Scene::Rect());
        
        // UIを描画
        layout.drawUI();
    }
}

ホットリロード

Siv3DにはDirectoryWatcherというファイルの更新を検知できる機能があります。せっかくなのでこれを組み込んで、XMLファイルの変更を検知して自動で再読み込みするようにしました。

有効にするには、load関数にFlexLayout::EnableHotReload::Yesを引数として与えます。

layout.load(U"<XMLファイルのパス>", FlexLayout::EnableHotReload::Yes);


自動保存とホットリロードを組み合わせて、実行結果を見ながら調整する様子

検索

最後に、画面上に配置したBoxやボタンの参照を取得する方法について説明します。
デザイナーで作成した要素には、IDを設定することができます。

デザイナーでは、要素のプロパティタブの一番上にある項目から設定できます。

C++側でIDから要素を取得するには、WebではおなじみgetElementById関数を使います。

auto box = layout.document()->getElementById(U"button1");

ここからボタンのインスタンスに変換するにはas関数を使います。

auto button1 = layout.document()->getElementById(U"button1")->as<FlexLayout::SimpleGUI::Button>();

SimpleGUIのラッパーにはUIの状態を取得できる関数が用意されています。例えばButtonではclicked()でクリックされているかを知ることができます。

if (button1->clicked())
{
    // なにかの処理
}

サンプルコード

デザイン

https://sthairno.github.io/flexlayout-designer/#pj:eJxrZczySazMLy3Rq8jNaZJkVpBkimhKdsqvKAjOzC3ISXUP9dRzKi0pyc9rZliYWlxSmZO6UkI-xdCgoEJCDkLJQChZCKWUbFpQwQkiBNOSU_NKUosYobSE1PfGZoZFEENW8Xzn_c71XUL1u8B39u8S8ikmEKNMIEaZQMwHUUmZKVkpiXnpqUW6SfkVjc2MizKSMzJzUopS83IdnUL9HD0VnEJDQvz9kjJT0pPATjVsAABB50tg

ソースコード

Main.cpp
#include <Siv3D.hpp>
#include <FlexLayout.hpp>

void Main()
{
	FlexLayout::Layout layout;

	// レイアウトファイルを読み込み
	layout.load(U"Layout.xml", FlexLayout::EnableHotReload::Yes);

	auto dangerBox = layout.document()
		->getElementById(U"danger-box") // IDがoutline-boxの要素を検索
		.value(); // 要素を取得

	auto button1 = layout.document()
		->getElementById(U"button1") // IDがbutton1の要素を検索
		->as<FlexLayout::SimpleGUI::Button>() // Button型にキャスト
		.value(); // 要素を取得

	while (System::Update())
	{
		// 更新処理
		layout.updateAll(Scene::Rect());

		// danger-boxに合わせて黄色と黒の縞模様を描画
		if (auto rect = dangerBox.rect())
		{
			constexpr auto StripeAngle = 45_deg;
			constexpr double StripeWidth = 20;
			const auto renderRect =
				RectF{ 0, 0, rect->size }.asQuad()
				.rotatedAt(0, 0, -StripeAngle)
				.boundingRect();

			ScopedViewport2D view(rect->asRect());
			Transformer2D t(Mat3x2::Rotate(StripeAngle));

			for (int32 i = 0; i * StripeWidth < renderRect.w; i++)
			{
				RectF{ renderRect.x + i * StripeWidth, renderRect.y, StripeWidth, renderRect.h }
    				.draw(i % 2 ? Palette::Yellow : Palette::Black);
			}
		}

		// UIを描画
		layout.drawUI();

		// button1がクリックされたら
		if (button1->clicked())
		{
			// メッセージを表示
			Print << U"Button1 clicked!";
		}
	}
}

実行結果

活用例

ゲームUI


https://gist.github.com/sthairno/0719d46db2c5ee9af13841e0904cdd7a

プレゼンテーション (!?)

個人的な目標はこれを使ってSiv3Dでプレゼンすることです。Siv3Dの強みを活かしてPowerPointやCanvaよりもインタラクティブでかっこいい画面を作りたい...

少し前に部内LT会で実際にチャレンジしてみました💪

https://github.com/sthairno/SiverPoint

Discussion