JUCEプログラミング、undoとredoのチュートリアル、Using an UndoManager with a ValueTree1

アンドゥとリドゥが実装できるとのことでやってみます。

アンドゥとリドゥは実装がややこしそうだけど、シンプルにできるのかな?

JUCEプログラミング、undoとredoの機能のチュートリアルである「Using an UndoManager with a ValueTree」をやっています。まずは、アンドゥ機能を実装するための基礎となるデモアプリを作成しました。内容の詳細を理解するというより、動作していることを重視して作成しました。プログラムの詳細説明まで至っていない点、ご了承ください。

公式のチュートリアルページはこちらです。

目次

こんな人の役に立つかも

・JUCEチュートリアルをしている人

・JUCEチュートリアル「Using an UndoManager with a ValueTree」をやっている人

・JUCE、TreeViewクラスのアンドゥ、リドゥ機能を利用したい人

デモファイルについて

チュートリアルのデモとしてダウンロードできるソースファイルに、「UndoManagerValueTreeTutorial01.h」と「UndoManagerValueTreeTutorial02.h」があります。

UndoManagerValueTreeTutorial01.h:アンドゥ、リドゥの機能を追加していくベースのプログラムです。ValueTreeクラスで、構造化されたデータをドラッグアンドドロップで移動させることができるようなUIのアプリがビルドできます。

UndoManagerValueTreeTutorial02.h:チュートリアルの実装がすべて行われたプログラムです。

ということで、まずは「UndoManagerValueTreeTutorial01.h」をProjucerのテンプレートから作成したプロジェクトに実装していきます。

Projucerテンプレートからデモプログラムの準備

チュートリアルを進めていくためのベースとなるデータツリー構造表示のデモアプリをProjucerテンプレートから作成していきます。

プロジェクト作成

今回は、GUI関連の実装でしたので、GUIテンプレートを利用しました。

私は次のように「UndoManager01」という名称を付けました。

実装

MainComponent.h

次の「ValuteTreeItem」クラスをMainComponent.hファイル内にそのままコピペします。MainComponentクラスの上に貼り付けました。

class ValueTreeItem : public juce::TreeViewItem,
    private juce::ValueTree::Listener
{
public:
    ValueTreeItem(const juce::ValueTree& v)
        : tree(v)
    {
        tree.addListener(this);
    }

    juce::String getUniqueName() const override
    {
        return tree["name"].toString();
    }

    bool mightContainSubItems() override
    {
        return tree.getNumChildren() > 0;
    }

    void paintItem(juce::Graphics& g, int width, int height) override
    {
        g.setColour(juce::Colours::white);
        g.setFont(15.0f);

        g.drawText(tree["name"].toString(),
            4, 0, width - 4, height,
            juce::Justification::centredLeft, true);
    }

    void itemOpennessChanged(bool isNowOpen) override
    {
        if (isNowOpen && getNumSubItems() == 0)
            refreshSubItems();
        else
            clearSubItems();
    }

    juce::var getDragSourceDescription() override
    {
        return "Drag Source";
    }

    bool isInterestedInDragSource(const juce::DragAndDropTarget::SourceDetails& dragSourceDetails) override
    {
        return dragSourceDetails.description == "Drag Source";
    }

    void itemDropped(const juce::DragAndDropTarget::SourceDetails&, int insertIndex) override
    {
        juce::OwnedArray<juce::ValueTree> selectedTrees;
        getSelectedTreeViewItems(*getOwnerView(), selectedTrees);

        moveItems(*getOwnerView(), selectedTrees, tree, insertIndex);
    }

    static void moveItems(juce::TreeView& treeView, const juce::OwnedArray<juce::ValueTree>& items,
        juce::ValueTree newParent, int insertIndex)
    {
        if (items.size() > 0)
        {
            std::unique_ptr<juce::XmlElement> oldOpenness(treeView.getOpennessState(false));

            for (auto i = items.size(); --i >= 0;)
            {
                auto& v = *items.getUnchecked(i);

                if (v.getParent().isValid() && newParent != v && !newParent.isAChildOf(v))
                {
                    if (v.getParent() == newParent && newParent.indexOf(v) < insertIndex)
                        --insertIndex;

                    v.getParent().removeChild(v, nullptr);
                    newParent.addChild(v, insertIndex, nullptr);
                }
            }

            if (oldOpenness != nullptr)
                treeView.restoreOpennessState(*oldOpenness, false);
        }
    }

    static void getSelectedTreeViewItems(juce::TreeView& treeView, juce::OwnedArray<juce::ValueTree>& items)
    {
        auto numSelected = treeView.getNumSelectedItems();

        for (auto i = 0; i < numSelected; ++i)
            if (auto* vti = dynamic_cast<ValueTreeItem*> (treeView.getSelectedItem(i)))
                items.add(new juce::ValueTree(vti->tree));
    }

private:
    juce::ValueTree tree;

    void refreshSubItems()
    {
        clearSubItems();

        for (auto i = 0; i < tree.getNumChildren(); ++i)
            addSubItem(new ValueTreeItem(tree.getChild(i)));
    }

    void valueTreePropertyChanged(juce::ValueTree&, const juce::Identifier&) override
    {
        repaintItem();
    }

    void valueTreeChildAdded(juce::ValueTree& parentTree, juce::ValueTree&)      override { treeChildrenChanged(parentTree); }
    void valueTreeChildRemoved(juce::ValueTree& parentTree, juce::ValueTree&, int) override { treeChildrenChanged(parentTree); }
    void valueTreeChildOrderChanged(juce::ValueTree& parentTree, int, int)              override { treeChildrenChanged(parentTree); }
    void valueTreeParentChanged(juce::ValueTree&)                                   override {}

    void treeChildrenChanged(const juce::ValueTree& parentTree)
    {
        if (parentTree == tree)
        {
            refreshSubItems();
            treeHasChanged();
            setOpen(true);
        }
    }

    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(ValueTreeItem)
};

ValueTreeItemクラスは、ValueTreeクラスでデータを保持しています。ValueTreeクラスでデータを管理するとアンドゥ、リドゥができるようになります。ValueTreeクラスのデータをpaintで表示したりするようなクラスが今回コピペしたValueTreeItemクラスの実装のようです。今回、ここについては、チュートリアルのクラスをコピペして利用することにします。

次に、MainCompnentクラスの実装へ追加します。

class MainComponent  : public juce::Component
                      ,public juce::DragAndDropContainer//[1]
{
public:
//...略...

    //[2]
    static juce::ValueTree createTree(const juce::String& desc);
    static juce::ValueTree createRootValueTree();
    static juce::ValueTree createRandomTree(int& counter, int depth);

private:
    //[3]
    juce::TreeView tree;
    std::unique_ptr<ValueTreeItem> rootItem;

    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MainComponent)
};

[1]に、ドラッグアンドドロップコンテナクラスを継承します。[2]では、ValueTree内のデータをランダムに生成するような3つの関数を定義しています。この3つの関数でアプリが起動するときに、コンストラクタのreset関数から、createRootValueTree関数が呼び出され、順次他の2つの関数が内部でよびだされてValueTreeItemクラスのインスタンスの生成時に表示するValueTreeクラスのデータとして渡されます。

[3]に、TreeViewクラスのtreeと、ValueTreeItemクラスのポインタをrootItemとしてprivateなメンバに定義しました。TreeViewクラスは木構造のデータを表示するようなコンポーネントクラスです。

MainComponent.cpp

MainComponent.cppに定義してあるMainComponentクラスのコンストラクタに、次のように実装を行います。

MainComponent::MainComponent()
{
    //[1]木構造のデータを表示するためのTreeViewコンポーネントです。
    addAndMakeVisible(tree);

    tree.setDefaultOpenness(true);
    tree.setMultiSelectEnabled(true);
    rootItem.reset(new ValueTreeItem(createRootValueTree()));
    tree.setRootItem(rootItem.get());

    setSize(600, 400);
}

次に、デストラクタです。デストラクタでは、TreeViewへ紐づける項目にnullを入れるようです。

MainComponent::~MainComponent()
{
    //[1]TreeViewクラスのインスタンスを、次のように処理をします。
    tree.setRootItem(nullptr);
}

paint関数では、ウィンドウ内の描画領域は、背景の塗りつぶしのみとしました。

void MainComponent::paint (juce::Graphics& g)
{
    g.fillAll (getLookAndFeel().findColour (juce::ResizableWindow::backgroundColourId));
}

resized関数はGUIコンポーネントの配置です。TreeViewクラスのオブジェクト、treeをGUI上に配置します。

void MainComponent::resized()
{
    tree.setBounds(getLocalBounds());
}

最後に、その他追加した関数として、ヘッダに定義したcreate〇〇の3関数の処理を実装しています。

これらの関数は、ValueTreeクラスのオブジェクトへデータをランダムに追加する関数です。次のように追加しました。

juce::ValueTree MainComponent::createTree(const juce::String& desc)
{
    juce::ValueTree t("Item");
    t.setProperty("name", desc, nullptr);
    return t;
}

juce::ValueTree MainComponent::createRootValueTree()
{
    auto vt = createTree("ValueTree");

    auto n = 1;
    vt.addChild(createRandomTree(n, 0), -1, nullptr);

    return vt;
}

juce::ValueTree MainComponent::createRandomTree(int& counter, int depth)
{
    auto t = createTree("Item " + juce::String(counter++));

    if (depth < 3)
        for (auto i = 1 + juce::Random::getSystemRandom().nextInt(7); --i >= 0;)
            t.addChild(createRandomTree(counter, depth + 1), -1, nullptr);

    return t;
}

動作

動作したアプリの様子は次のようになります。Item2をドラッグアンドドロップでItem1と同階層にもってきた後の様子です。

よかったらシェアしてね!
目次
閉じる