“The ValueTree is JUCE’s secret weapon.”
– (from the ValueTree tutorial on juce.com)
The juce::ValueTree
class is a super-flexible data structure that can hold a list of (key, value)
pairs (referred to as properties), where:
key
is of typejuce::Identifier
, which you can think of as a string, but brings along some other capabilities and constraints that make it better for this use case.value
can be of (more or less) any data type, stored internally as a variant typejuce::var
that’s part of JUCE.- There’s no compile-time schema for a ValueTree, properties and children can be added and removed at runtime, and the value of any property can be of any arbitrary type.
A ValueTree can also contain a list of child ValueTree objects (which is obviously required to implement the ‘tree’ part of the name).
ValueTrees may be round-tripped through XML without loss; it might be useful to think of them as a live, runtime XML document. There are some obvious differences between them (no namespacing, no schema support, and ValueTrees cannot contain text elements), but it can be a useful comparison to keep in mind.
Application code can listen for changes to a ValueTree, including events like
- changes to property values
- addition of child trees
- removal of child trees
- re-ordering of child trees
ValueTrees can also manage undo/redo operations, simplifying a frequently error-prone part of application development.
Internally, a ValueTree object points at a shared object that actually contains the storage for the tree. When you first create a ValueTree valueTree1
, that shared object is automatically created. Creating a second ValueTree valueTree2
that’s a copy of valueTree1
increases the reference count on the shared object and points valueTree2
to the shared object as well. Any changes made to valueTree1
are visible to code that has access to valueTree2
and vice-versa.
This code:
juce::ValueTree valueTree1 { "typeName" };
juce::ValueTree valueTree2 { valueTree1 };
Creates a graph of objects that looks like:
graph LR A[valueTree1] --> B(Shared Object) C[valueTree2] --> B
Thanks to reference counting, the shared object containing the data of interest will stay alive until the last ValueTree that points at it is destroyed.
Because copying a ValueTree is implemented as a simple pointer assignment and increment of a reference count, ValueTrees are very lightweight and can be passed around by value.