Learn how to use the ValueTree class to manage data effectively in your applications.
Level: Intermediate
Platforms: Windows , macOS , Linux , iOS , Android
Classes: ValueTree, var, Identifier
The ValueTree class is JUCE's secret weapon. It has the power to massively streamline the internal complexity of your applications, turning the traditionally passive data model into an active participant in runtime operation. It takes care of some of the fiddlier aspects of development, such as interfacing data with the GUI, and providing automatic undo and redo of data modifications, as well as providing a universal container for your data. It is also inherently serialisable, making the export and import process very straightforward.
You may not know about them, or perhaps you may have simply not realised their potential. This guide is intended to cover everything you need to know in order to use them.
The ValueTree class is really an ensemble and, when working with them, you will always be interacting with at least three important classes. These are the ValueTree class itself, the var class, and the Identifier class. It is impossible to do anything meaningful with ValueTree objects without encountering these, so it is useful to appreciate how (and why) you should be using them. Therefore, before we discuss the actual practicalities of manipulating such data, you should familiarise yourself with the following:
The ValueTree class is the ultimate container class, capable of holding any kind of information. Whilst this may be the obvious role of the type, there are many other facets to it, taking it beyond mere structural duties.
A ValueTree object is a flexible, multi-purpose data object. It has a basic type name, and can hold an arbitrary set of named properties. It is like a kind of universal struct; you don’t need to define what (or how much) data it should hold, you can just use it for whatever you like at runtime.
For illustrative purposes, one object (shown here as pseudo-data) might hold the following information:
As the name suggests, a ValueTree object may also function as a node in a tree structure. Along with its named properties, it can contain any number of child nodes (and can in turn itself be added to a parent node).
These structures are very similar to those formed in the XML format; an individual ValueTree node is very much like an XmlElement object, having a name, properties, and children. The big difference is that the properties are stored as actual typed data, rather than the text-for-everything representation used by the XML format . This means they can hold more complex types of data, access it more efficiently, and be generally better suited to use in a data model (and not just serialisation). In fact, it is possible to automatically generate XML text (or a special binary format) from any given ValueTree node, and later restore that structure.
Each node is reference counted, so their lifetimes are easily managed. The data itself is actually stored in a hidden shared instance, for which the ValueTree class is just a light, reference-keeping wrapper interface.
You can pass them around quickly by value, and not have to use pointers directly; returning a ValueTree object from a function does not copy any data, only a reference, so it makes your interfaces both simple and safe.
You never have to worry about deleting them yourself; as soon as you are no longer using one anywhere, it will be automatically destroyed. This also means that the node data will never be accidentally deleted whilst you might be still using it, which is particularly handy for ensuring an asynchronous UI will never encounter dangling pointers.
It has a simple, universal interface for manipulating its properties and children. By being generic, it can have a single interface to access the content, regardless of its type or organisation. Using the universal var class as the common property type, it can take a variety of inputs. Using the Identifier class to store and retrieve these properties by name, it presents an intuitive way to organise your data.
By having such a concise set of controls, it is able to provide built-in support for undo; all of the functions which modify content incorporate predefined undoable actions, so you need only supply an UndoManager object to provide such functionality in your application. In case you weren’t already convinced, this is a very compelling reason to use ValueTree objects in a data model.
Another very powerful feature is the ability for ValueTrees to send notifications when their contents change; this offers huge practical simplifications, particularly in keeping the UI up to date. For example, a Component object being used to display the contents of a node can simply refresh itself whenever it sees that the data has been edited—you need only implement it as a ValueTree::Listener subclass.
The ValueTree class is like some kind of miracle class for the application data model, effortlessly providing a wealth of functionality to simplify the way the insides of your application slot together.
The var class is a universal variant class, for holding data of various types. Its functionality makes it suitable for representing JSON data structures.
Traditionally, you would have to decide in advance what type of data any given variable in your code may store (for example, if you want to hold a whole number, you would choose an int
, and that is all that variable is allowed to be used for). However, if you were to use a var, such a decision does not have to be made, as it is compatible with a variety of types.
It is like a generic chameleon variable, capable of representing basic numeric values (int
or double
), text (a String object), and bool
values, as well as a void
state (because 0
or false
can be conceptually different to nothing). They can also hold a pointer to any class derived from the ReferenceCountedObject class (which could be comprised of any kind of complex data you can imagine). As if that weren’t enough, a single var object can also be used as an array of multiple var objects!
This versatility makes it ideal for use in a generic container (such as the ValueTree class), allowing its interface the luxury of not having to care what type you’re giving to it (or expecting from it), as long as it can be held in a var. There is implicit casting (and overloaded assignment operators) for the basic types, making interaction in code simple. You can even automatically return a string representation of its current value (including non-text types). The only place these luxuries don’t all apply is when a var object holds a ReferenceCountedObject object. As these are unknown types, you have to cast them yourself (you can do this safely with a dynamic_cast
, as this returns nullptr
if it is not of the correct type).
In a ValueTree object, all properties are held as var objects. By choosing this multi-purpose class as a property type, it is possible to have a single set of functions to access them, regardless of what they may be. You don’t need to use one function for int
values and another for text; all such functions can be unified by dealing with var objects.
This class is intended as a human-readable key, used to identify data.
Essentially, an Identifier object is a string. You can assign one from a String object, and you can also retrieve their contents as a String object. In the context of the ValueTree class, it is used both to specify a type name for the node, and also to uniquely label each of its properties.
There are two main reasons for using a specialised class instead of the general purpose String class.
_-:#$%
. This might sound a bit rubbish, but it makes it possible to ensure compatibility with other systems with the same limitation (for example, the XML format and scripts).By using a special class, we can ensure that it is optimised to allow for quick comparisons. Because of this optimisation (and the limited character set), they are not intended to be used for general text handling, but they are perfectly suited to using string-like data as a key.
It is useful to know about the optimisation of the Identifier class. For one thing, it will reassure you that the code is not secretly checking all the characters in each test. More importantly, however, as with many optimisations, there is a cost involved, and it helps to know where that is so you can avoid it being an issue.
It is perfectly possible to use the Identifier without knowing this stuff, but you will have a better understanding of where the trade-offs occur if you do.
To know that two String objects are the same, what you actually have to do is prove that they are not different. As it happens, the String class holds reference counted text, so there is a chance that two String objects actually point to the same data; if that is the case, we can spot it straight away (as they will both hold the same address).
These special cases are much quicker, but the String class can only take advantage of them if they happen. When the addresses are not the same, it does not prove that their content is not equivalent, and so we must still check the characters.
The Identifier class exploits this behaviour, and makes it so that we can actually guarantee that all equivalent Identifier objects will always be pointing to the same piece of data. Because of the way they are optimised, a character-for-character match of different Identifier objects will never exist for different memory addresses. This means that the special case of identical strings is now the only way to be equivalent, and thus proving that they are different is as simple as spotting that they hold different addresses.
This is not magic, however, as the cost is simply moved elsewhere.
In order to enforce this behaviour, all Identifier objects share a single, concealed, global pool of unique strings. The strings held by every Identifier object used at runtime are stored in this pool. When assigning an Identifier object from a String object, the pool is checked to see if it already contains an equivalent copy. If it does, that will be used instead; if not, the new String object is added, ready to be found by the next Identifier object to look for it.
Thus, instantiating an Identifier object from a String object is the most expensive part of their operation. Once one exists, if you always copy from it, you will never have to pay that price for it again. Sometimes it is unavoidable (specifically if you are acquiring an Identifier object from input, or data held in a normal String object), but this is far less regular an occurrence than comparing them. Every time you assign one from a bare String object (either from data, or as a literal in code), a pool check is required to enforce the optimisation. If you instead assign from another existing Identifier object, it is guaranteed that such a check has already been done.
A good strategy is to initialise some easily-accessible Identifier instances at startup. From your code, you can then use these instead of literal strings, and you will never incur any further lookup penalties from them. You could put them in a globally accessible namespace, use file static instances, or even static class members to help organise them.
In addition to the var and Identifier classes, there are actually many more that you will see used in conjunction with ValueTree objects, as we shall see later. These, however, are required for most of the fundamental operations in the ValueTree class.
Once you've learned about the three essential classes, you should be in a good position to proceed to pick up the basics.
This is a basic guide introducing all of the essential core functions of the ValueTree class. It covers all of the functionality you need to be able to get started using them in your code.
You can easily use ValueTree objects by simply creating an object on the stack (or as a member of a class).
A ValueTree object is, by default, invalid. That is, a ValueTree object initialised with the default constructor will have no data at all. Unless you explicitly initialise it with an Identifier (or an existing, valid, ValueTree object), it will hold nothing; it is an empty shell.
This is similar to a null pointer (in fact, internally, that's exactly what it is), but without any of the inherent dangers. Until it is assigned a valid node, it refers to nothing. The main difference here is that the interface functions can still be used, albeit largely without any effect (as there is nothing to be manipulated).
Whilst it is safe to 'accidentally' call the access functions on an invalid node, it doesn't get you very far!
In your code you can call the member function ValueTree::isValid() to find out if the ValueTree is empty:
The primary means of creating a valid ValueTree object is by initialising with a valid Identifier object. This is used to name the new node, and represents the node’s type.
By requiring that a valid node has a type, we have a mechanism of indicating what sort of data it should contain. Conversely, when faced with a ValueTree object (for example, as a parameter supplied to a function), you can check its type—using the ValueTree::getType() function—to determine what you might expect to find inside it.
The third way in which a ValueTree can be initialised is via the copy constructor, by providing an existing ValueTree object.
This results in an object which refers to the same node data as the existing object.
It is important to understand that nothing is copied here except for a reference; both objects are attached to the same underlying instance of the original node data. Changes made to either (if the shared node is valid) will be found upon inspection of the other.
You can achieve the same result (a sharing of node data) by using the assignment operator.
Here is a question:
What happens to the node instance created in line [3] when we replace it in line [4] ?
By the end, all three variables point to the same initial instance. There are no longer any ValueTree objects referring to the second instance, so it is destroyed.
Whenever a ValueTree object is reassigned (or goes out of scope), the underlying data it was pointing to loses a reference. If that data is not held elsewhere, it is automatically destroyed. This may sound scary, as if you might be operating in a world where your data could self-destruct at any moment; in reality, it is very unlikely that you will inadvertently lose your nodes! It is a very robust system. Besides, if the data is important, you’ll probably find that you have in fact already stored it somewhere; it is as intuitive as you could wish for.
delete
operator if there are no longer references to the previously assigned data.All of the access functions in the ValueTree class understand how to use UndoManager objects. They come ready-prepared with suitable UndoableAction objects; all you have to do is provide a pointer to an UndoManager object with your call, and an operational, undoable record of the modification will be added to it automatically.
There's not a lot you need to know to manage this, but we cover it in more detail in Tutorial: Using an UndoManager with a ValueTree.
For now, we will make use of the functions without bothering to store a history, by providing a nullptr
value. We will take it as read that any such modification functions must be given at least a null pointer to an UndoManager object when describing them; there will of course be nullptr
values visible in the examples.
Registering as a listener to the ValueTree allows us to be notified synchronously when the data has changed. The pointer to the listener is held in the ValueTree instance and therefore it is usually best to take a copy of the ValueTree before registering like so:
We can then implement the following callback functions depending on the behaviour we wish to be notified for:
Callbacks are propagated upwards in the tree hierarchy so listeners to parent nodes will also receive property change callbacks from children and therefore types and property names should be checked.
The ValueTree::setProperty() function is one direct way to set a property on a valid node. You must provide an Identifier object to specify the name of the property you wish to set, and a var value for it to take:
There are two basic ways to retrieve a property. For each approach, you must provide an Identifier to specify which named property you are requesting.
Using the ValueTree::getProperty() function:
Or using the subscript operator (that is, the ValueTree::operator[] function):
Both of the above lines store the same result into the name
variable. Remember that these properties can hold any supported var type. Thus, the following code is also valid:
Here, a property is set in the same way using a double
value, and is easily read back in as such. It is also possible to replace an existing property with a different type of value.
If all you want to do is store individual values in a ValueTree node (and nothing more complex), then you already know enough to get started!
If you were using structs, you would only have the ability to set or get these member variables directly. With a ValueTree node, you can also (at runtime) find out what variables there are.
The ValueTree::getNumProperties() function tells you how many properties a node has:
The ValueTree::getPropertyName() function returns an Identifier object giving the name of the property at the specified position. By using this in combination with the ValueTree::getNumProperties() function, it is possible to iterate over the properties without previously knowing what they are.
The ValueTree::hasProperty() function simply tells you whether or not a particular named property has been set for a node:
These functions allow for what is called reflection, whereby your program is able to inspect the nature of your objects. When faced with an object to work with, your code doesn't necessarily need a definition (for example, class header) to be able to make use of it; you can check to see what members it has, and use them if they are appropriate.
If you want to create more complex structures, you will want to start using these objects as nodes in a hierarchy.
Whilst a node can have a bunch of properties attached to it, it can also contain a number of child nodes, much like an array.
If we have a node we wish to add as a child, we simply call the ValueTree::addChild () function on the node we wish to add it to. We naturally pass in the new child node, but we must also specify where it should go. If you don't need it to be inserted at any particular position, you can specify -1
to simply place it on the end.
Here, the data held in childNode
node belongs to the data held in myNode
.
Note that this belonging is also an additional reference. If we reassign the childNode
variable, the existing data will not be lost.
Whilst we may no longer have any ValueTree objects in our immediate scope pointing directly to that instance, it is kept alive within the original node.
There are a number of ways in which you can retrieve a child node. Which you should choose for any given task depends upon how your chosen structures are organised.
With all of them, any requests which don't correspond to a child will return an invalid object.
The ValueTree::getChild() function returns the child currently sitting in the specified position within the node's internal list. If you're always adding them to the end, 0
would correspond to the first one you put in:
The ValueTree::getChildWithName() function returns the first child with the specified name identifier as its node type:
The ValueTree::getChildWithProperty() function returns the first child which has the named property set to the specified value:
The last two, along with any other such methods you require, can of course be written using the first one; it's easy to write your own functions to retrieve a child by some other criteria (for example, a combination of matching node type and a property value).
You can also retrieve the current owner of any given node using the ValueTree::getParent() function:
This tutorial has given a detailed overview of the ValueTree class and related classes. In particular, you should: