はじめに
JUCEにはjuce::ComponentBuilderという、指定したjuce::ValueTreeの内容を元にGUIコンポーネントを作成できる機能があります。
場合によっては便利そうではあるのですが、公式クラスドキュメント以外での情報がほとんど無かったためコードを読みつつ使ってみたメモになります。(間違ってる箇所などあればご指摘ください)
※注意 : この記事をほぼ書き終わった後で気づきましたが削除予定の機能のようです。詳しくは記事の最後をご覧ください。
サンプルコード
ValueTreeについて
Undo/Redoやファイルの読み書き、変更時のコールバック処理などを行うための便利な機能を備えたツリー構造のデータクラスです。
詳細は割愛しますが公式クラスドキュメントやADC2017でのプレゼンなどが参考になります。
juce::ComponentBuilder::TypeHandler
juce::ComponentBuilderでGUIコンポーネントを作成するにはjuce::ComponentBuilder::TypeHandlerを継承したクラスに
- GUIコンポーネントの作成処理
- GUIコンポーネントの更新処理
の記述を行う必要があります。
juce::ComponentBuilderに対してコンポーネント取得要求があった場合や指定ValueTreeの変更を検知するとこれを使用します。
また、juce::ComponentBuilder::TypeHandler継承クラスでは作成するGUIコンポーネント型をテンプレートで指定できるようにしておくと便利です。
template<typename ComponentType>
class CustomTypeHandler : public juce::ComponentBuilder::TypeHandler
{
public:
CustomTypeHandler (const juce::Identifier &valueTreeType) : TypeHandler(valueTreeType) { }
/* 以降の例ではこちらに処理を記述するものとします。 */
};
GUIコンポーネントの作成処理 (addNewComponentFromState)
juce::Component* addNewComponentFromState (const juce::ValueTree &state, juce::Component *parent)をoverrideします。
ここではValueTreeを元にGUIコンポーネントを作成し, 引数の親コンポーネントに追加した後に作成したGUIコンポーネントを返します。
juce::Component* addNewComponentFromState (const juce::ValueTree &state, juce::Component *parent) override
{
juce::Component* comp = new ComponentType();
if (parent) parent->addAndMakeVisible(comp);
// 更新処理
updateComponentFromState(comp, state);
return comp;
}
GUIコンポーネントの更新処理 (updateComponentFromState)
ValueTreeを元にGUIコンポーネントの更新処理を行います。
主に以下を行います。
- プロパティを元に更新
- タイプIDを元に更新
- 親子要素を元に更新
void updateComponentFromState (juce::Component *component, const juce::ValueTree &state) override
{
// 以下のプロパティが定義されている前提
jassert(state.hasProperty(juce::ComponentBuilder::idProperty));
jassert(state.hasProperty(PROPs::name));
jassert(state.hasProperty(PROPs::x));
jassert(state.hasProperty(PROPs::y));
jassert(state.hasProperty(PROPs::w));
jassert(state.hasProperty(PROPs::h));
// プロパティを元にGUIコンポーネントの更新処理
component->setName(state[PROPs::name]);
component->setBounds(state[PROPs::x], state[PROPs::y], state[PROPs::w], state[PROPs::h]);
// タイプIDに合わせた更新処理
if (state.hasType(TYPEs::textButton) && state.hasProperty(PROPs::text))
{
if (auto* textButton = dynamic_cast<juce::TextButton*>(component))
textButton->setButtonText(state[PROPs::text].toString());
}
// ValueTree子要素に対応したコンポーネントを作成
getBuilder()->updateChildComponents(*component, state);
}
juce::ComponentBuilder使用例
以下を行います。
- ルートとなるValueTreeの設定(このValueTreeに対しての子要素追加はもちろんok)
- ComponentBuilderのコンストラクタ引数にルートとなるValueTreeを指定
- juce::ComponentBuilder::TypeHandler派生クラスを登録
- getManagedComponent()でValueTreeを元に作成されたルートコンポーネントを生成し取得
// ルートコンポーネントとなるValueTreeの用意
vtComponentBuilderRoot.setProperty(juce::ComponentBuilder::idProperty, "root", nullptr);
vtComponentBuilderRoot.setProperty(PROPs::name, "root", nullptr);
vtComponentBuilderRoot.setProperty(PROPs::x, 100, nullptr);
vtComponentBuilderRoot.setProperty(PROPs::y, 0, nullptr);
vtComponentBuilderRoot.setProperty(PROPs::w, 500, nullptr);
vtComponentBuilderRoot.setProperty(PROPs::h, 400, nullptr);
// componentBuilderの初期化処理
componentBuilder.reset(new juce::ComponentBuilder(vtComponentBuilderRoot));
componentBuilder->registerTypeHandler(new CustomTypeHandler<juce::Component>(TYPEs::root));
componentBuilder->registerTypeHandler(new CustomTypeHandler<juce::TextButton>(TYPEs::textButton));
// 関数内部に処理記述がないため使用できない
// componentBuilder->registerStandardComponentTypes();
// 初回getManagedComponent()時にValueTreeとTypeHandlerを用いてルートComponentが生成され、取得できる。
// このComponentの所有権はComponentBuilder側にあるため使用側でdeleteしないよう注意
builderRootComponent = componentBuilder->getManagedComponent();
addAndMakeVisible(builderRootComponent);
その後以下のようなValueTree変更操作があると自動的にコンポーネントが更新されます。
juce::ComponentBuilderでは対象ValueTreeの「juce::ComponentBuilder::idProperty」プロパティにGUIコンポーネント毎に違う文字列を指定すると管理対象となります。
// 子要素追加処理
{
auto vtTextButton = juce::ValueTree(TYPEs::textButton);
vtTextButton.setProperty(juce::ComponentBuilder::idProperty,
"textButton" + juce::String(vtComponentBuilderRoot.getNumChildren()), nullptr);
vtTextButton.setProperty(PROPs::name, "textButton", nullptr);
vtTextButton.setProperty(PROPs::x, juce::Random().nextInt({0, 400}), nullptr);
vtTextButton.setProperty(PROPs::y, juce::Random().nextInt(300), nullptr);
vtTextButton.setProperty(PROPs::w, 100, nullptr);
vtTextButton.setProperty(PROPs::h, 100, nullptr);
vtTextButton.setProperty(PROPs::text, vtTextButton[juce::ComponentBuilder::idProperty].toString(), nullptr);
vtComponentBuilderRoot.appendChild(vtTextButton, nullptr);
}
// 子要素削除処理
{
auto numChildren = vtComponentBuilderRoot.getNumChildren();
if (numChildren > 0)
{
vtComponentBuilderRoot.removeChild(numChildren - 1, nullptr);
}
}
juce::ComponentBuilder::ImageProviderについての補足
ComponentBuilderの公式クラスドキュメントを見ているとImageProviderというクラスがあります。
これにはjuce::var⇔juce::Imageの変換処理を指定するための純粋仮想関数を持っており、
これを継承したクラスをComponentBuilder::setImageProvider()で指定することができるというものです。
juce::varはValueTreeプロパティの値型で、juce::Imageにはそのままでは対応していないためこれが用意されていると思いますが
juce::ComponentBuilder内部では特にこのImageProviderを利用しておらず、おそらくComponentBuilder::TypeHandler派生クラス内でご自由にお使いください、という用途であると推測されます。
そのため、juce::ComponentBuilder以外で使い回すことも考慮するとこのImageProviderを使用しなくてもよく、変換処理を直接記述もしくはグローバル関数やstatic関数を用いる等で良さそうです。
juce::ComponentBuilderの今後
Please don’t use ComponentBuilder! It was a failed experiment and will be removed at some point… it’s just left there for backwards-compatibility for the moment. (…and it could certainly use some comments to explain this too!)
[Google翻訳] ComponentBuilder を使用しないでください。これは失敗した実験であり、ある時点で削除される予定です... 現時点では下位互換性のために残されています。 (…そして、これを説明するためにいくつかのコメントを使用することもできます!)
この記事をほぼ書き終わった後で気づきましたが削除予定の機能のようです。。
To be honest I would steer clear of the ComponentBuilder. It’s more hassle than it’s worth as you’ll spend your whole time writing property binders and actions. Using a ValueTreeObjectList is far more flexible and probably quicker in the end.
[Google翻訳] 正直なところ、ComponentBuilder は避けたいと思います。プロパティ バインダーとアクションの記述にすべての時間を費やすことになるため、価値があるよりも面倒です。 ValueTreeObjectList を使用する方がはるかに柔軟で、最終的にはおそらく高速です。
juce::ComponentBuilderを使わず、こちらのスレッドで紹介されているValueTreeObjectListクラスみたいなものを自分で用意するのが推奨されているようです。