チュートリアル:ValueTreeでUndoManagerを使用する
アプリケーションにアンドゥ/リドゥアクションを実装します。UndoableActionオブジェクトを使用して以前の中間状態を簡単に復元し、アンドゥ可能なアクションをトランザクションとしてグループ化する方法を学びます。
レベル: 中級
プラットフォーム: Windows, macOS, Linux
クラス: UndoManager, UndoableAction, ValueTree, TreeView, TreeViewItem
はじめに
このチュートリアルは、チュートリアル:ValueTreeクラスで説明されているValueTreeオブジェクトの基本的な理解を前提としています。まだ読んでいない場合は、最初にそのチュートリアルを読む必要があります。
このチュートリアルのデモプロジェクトをこちらからダウンロードしてください:PIP | ZIP。プロジェクトを解凍し、最初のヘッダファイルをProjucerで開いてください。
この手順についてサポートが必要な場合は、チュートリアル:Projucer Part 1: Projucerを始めようを参照してください。
デモプロジェクト
デモプロジェクトは、過去の履歴を簡単に復元できることを示すために、ValueTreeオブジェクトと組み合わせてUndoManagerクラスの使用を説明しています。TreeViewとTreeViewItemクラスを使用して、ValueTreeデータをツリー構造として表示します。プロジェクトをビルドして実行すると、以下のようなものが表示されるはずです:

現時点では、ValueTreeノードをドラッグアンドドロップしてデータ構造の階層を変更できます。また、子を展開したり折りたたんだりできますが、変更をアンドゥおよびリドゥすることはできません。UndoManagerクラスを使用してその機能を実装してみましょう。
ここで紹介するコードは、JUCE DemoのValueTreesDemoと大まかに類似しています。
アンドゥ/リドゥボタンの追加
まず、アンドゥとリドゥ機能を許可するために、ユーザーインターフェースに2つのTextButtonオブジェクトを追加しましょう。このセクションではラムダ関数に精通している必要があり、これらの手順についてサポートが必要な場合は、チュートリアル:リスナーとブロードキャスターを参照できます。
MainContentComponentクラスで、各ボタンのTextButton変数を宣言します [1]:
juce::TreeView tree;
juce::TextButton undoButton, redoButton; // [1]
std::unique_ptr<ValueTreeItem> rootItem;
コンストラクタのメンバー初期化リストで、TextButtonオブジェクトのテキストを設定します [2]:
MainContentComponent()
: undoButton ("Undo"),
redoButton ("Redo") // [2]
{
最後に、ボタンを表示可能にし [3]、Button::onClickヘルパーオブジェクトに割り当てるラムダ関数を準備します [4]:
//...
addAndMakeVisible (undoButton);
addAndMakeVisible (redoButton); // [3]
undoButton.onClick = [this] {};
redoButton.onClick = [this] {}; // [4]
setSize (600, 400);
次に、resized()メソッドでボタンの境界を設定できます:
void resized() override
{
// これはMainContentComponentがリサイズされるときに呼び出されます。
// 子コンポーネントを追加する場合、ここでそれらの位置を
// 更新する必要があります。
auto r = getLocalBounds();
auto buttons = r.removeFromBottom (20);
undoButton.setBounds (buttons.removeFromLeft (100));
redoButton.setBounds (buttons.removeFromLeft (100));
tree.setBounds (r);
}
UndoManagerインスタンスを引数として渡す
ValueTreeクラスはアンドゥ/リドゥ動作を自動的に処理するため、UndoableActionオブジェクトを登録するにはUndoManagerインスタンスをパラメータとして渡すだけです。これを実装するには、まずUndoManagerクラスのインスタンスを宣言します [1]:
juce::UndoManager undoManager; // [1]
次に、対応するアンドゥ/リドゥ動作を処理するためにボタンがクリックされたときに呼び出される関数を割り当てます。ラムダ関数で、それぞれUndoManager::undo()とUndoManager::redo()を以下のように呼び出します:
addAndMakeVisible (undoButton);
addAndMakeVisible (redoButton); // [3]
undoButton.onClick = [this] { undoManager.undo(); };
redoButton.onClick = [this] { undoManager.redo(); }; // [4]
setSize (600, 400);
ValueTreeItemクラスでは、UndoManagerインスタンスへの参照も保持します [2]:
private:
juce::ValueTree tree;
juce::UndoManager& undoManager; // [2]
クラスコンストラクタのメンバー初期化リストで、UndoManager参照を割り当てます [3]:
ValueTreeItem (const juce::ValueTree& v, juce::UndoManager& um)
: tree (v), undoManager (um) // [3]
{
ValueTreeItemのサブアイテムが再帰的に作成されるたびに、UndoManagerインスタンスを渡す必要があります [4]:
void refreshSubItems()
{
clearSubItems();
for (auto i = 0; i < tree.getNumChildren(); ++i)
addSubItem (new ValueTreeItem (tree.getChild (i), undoManager)); // [4]
}
これで、MainContentComponentクラスでUndoManagerインスタンスを渡すことでルートValueTreeItemをインスタンス化できます [5]:
tree.setDefaultOpenness (true);
tree.setMultiSelectEnabled (true);
rootItem.reset (new ValueTreeItem (createRootValueTree(), undoManager)); // [5]
tree.setRootItem (rootItem.get());
今度は、TreeViewに行った変更を登録するために更新が必要な3つの異なるメソッドがあります。
void itemDropped (const juce::DragAndDropTarget::SourceDetails&, int insertIndex) override
{
juce::OwnedArray<juce::ValueTree> selectedTrees;
getSelectedTreeViewItems (*getOwnerView(), selectedTrees);
moveItems (*getOwnerView(), selectedTrees, tree, insertIndex, undoManager); // [1]
}
static void moveItems (juce::TreeView& treeView, const juce::OwnedArray<juce::ValueTree>& items, juce::ValueTree newParent, int insertIndex, juce::UndoManager& undoManager)
{
v.getParent().removeChild (v, &undoManager); // [2]
newParent.addChild (v, insertIndex, &undoManager); // [3]
- [1]:アイテムがドラッグアンドドロップされるたびに、移動を処理する静的関数にundoManagerを渡します。
- [2]:前の親から子を削除し、そのアクションをundoManagerに登録する必要があります。
- [3]:次に、新しい親に子を追加し、新しいアクションをundoManagerに登録できます。
undoManager参照をValueTree関数addChild()とremoveChild()に渡すことで、UndoManagerに裏でperform()関数を呼び出すことでUndoableActionを実行させます。UndoableActionオブジェクトについては、将来のチュートリアルで説明します。
演習:Labelコンポーネントを使用して、それぞれのTextButtonオブジェクトの横に格納されたアンドゥおよびリドゥアクションの説明を表示し、それぞれgetUndoDescription()およびgetRedoDescription()関数を使用します。
イベントをトランザクションとして処理する
UndoManagerのもう1つの便利な機能は、複数のアクションを単一のアンドゥ/リドゥトランザクションとしてグループ化する機能です。undoManagerインスタンスでbeginNewTransaction()関数を呼び出すことで、UndoManagerのperform()関数へのすべての呼び出しは、次のbeginNewTransaction()呼び出しまでグループ化されます。
例として、beginNewTransaction()関数を定期的に呼び出し、アクションのグループをトランザクションとして一緒に格納するTimerを作成しましょう。MainContentComponentで、タイマーコールバックを受け取るためにTimerクラスを継承します [1]:
class MainContentComponent : public juce::Component,
public juce::DragAndDropContainer,
private juce::Timer // [1]
{
public:
対応するヘッダファイルでコールバック関数を宣言します [2]:
void timerCallback() override // [2]
{
undoManager.beginNewTransaction(); // [4]
}
コンストラクタで、トランザクション呼び出し間の希望する間隔(ミリ秒)でタイマーを開始します [3]:
startTimer (500); // [3]
}
最後に、タイマーコールバックでUndoManagerのbeginNewTransaction()関数を呼び出すことができます [4]:
void timerCallback() override // [2]
{
undoManager.beginNewTransaction(); // [4]
}
演習:タイマーを使用してアクションのグループを分離する代わりに、5つのアンドゥ/リドゥアクションごとにトランザクションを実装してください。
このコードの変更版のソースコードは、デモプロジェクトのUndoManagerTutorial_02.hファイルにあります。
まとめ
このチュートリアルを完了することで、アプリケーションの以前の状態を復元する方法を学びました。特に、以下のことを行いました:
- UndoableActionオブジェクトをUndoManagerオブジェクトに格納しました。
- UndoManagerインスタンスをValueTreeアクセス関数にパラメータとして渡しました。
- アンドゥ/リドゥアクションのグループをトランザクションとして処理しました。