Unity UI Toolkit: Reusable Panel
Introduction
UI Toolkit is the newer UI system for Unity.
Advantages over the older UI system:
ease of laying out the UI, with the help of UI Builder
ease of changing theme or style, with the unity style sheet (uss)
it can be used for the editor drawer as well, which reduces the headache of calculating the rect position for each property
Disadvantages
Documentation is not quite yet helpful at the moment, most of the problems that I encountered, were resolved only after reading from the forum where other users may have encountered similar problems.
Some functionality can only be known how to be done, after digging the unity source code at the github.
Disclaimer
Problem
Create a reusable panel that can assign additional UI in the intended content container.
ScrollView is one such panel that can be reusable and add additional UI.
Note that, the child elements are all greyed out; no editing can be done on them. If you want to add a new UI element to it, it will have to be added to the root visual element, which in this case, would be the ScrollView. It has an internal logic that will automatically allocate the newly added UI element to the visual element, "unity-content-container".
If you try to create a UI Document and mimic the structure, and then try to use it on another UI Document, you will notice that when you try to add a new UI element, it will just be added directly under its root visual element.
Solution
I'm using Unity 2022.3.4f1.
At this point, there doesn't seem to be a way to do with it UI Document. The only way to redirect the newly added UI element is to create a new C# class that derives from VisualElement.
Steps:
Create a visual element that can be shown in the Library
Generate child elements via code
Add styles
Create a visual element that can be shown in the Library
It is very simple to set this up.
Create a class that derived from
VisualElement
Create an inner
UxmlFactory
class that is derived fromUnityEngine.UIElements.UxmlFactory
public class PanelView : VisualElement
{
public new class UxmlFactory : UnityEngine.UIElements.UxmlFactory<PanelView>
{
}
public PanelView()
{
}
}
This is the minimum code that is needed to have it appear in the Library.
Note that the inner class has to be UxmlFactory
, with the new
keyword. This allows the UI Builder to know this factory is associated with PanelView in this case.
A parameterless constructor is needed as well, as new()
is used when the factory tries to create it.
Go back to UI Builder, and you will see it appear under Custom Controls, from the Project tab.
Generate child elements via code
public class PanelView : VisualElement
{
private Label _titleLabel;
private VisualElement _contentContainer;
// this is the important property to override
public override VisualElement contentContainer => _contentContainer;
public new class UxmlFactory : UnityEngine.UIElements.UxmlFactory<PanelView>
{
}
public PanelView() : this("Label")
{
}
public PanelView(string title)
{
_titleLabel = string.IsNullOrEmpty(title) ? new Label() : new Label(title);
_titleLabel.name = "title";
_contentContainer = new VisualElement();
_contentContainer.name = "content-container";
// add via base.hierachy, instead of the usual VisualElement.Add
base.hierarchy.Add(_titleLabel);
base.hierarchy.Add(_contentContainer);
}
}
contentContainer
is the property that is used when the Add
function is called.
By overriding it, it can be changed to the intended visual element to be used as the parent to be added to.
And since contentContainer
has been overridden, it is no longer safe to use the Add
function in this class. As it may try to add itself to itself. The hierarchy
is what is used to manage the direct children. The Add
function uses it as well. By caliing hierarchy.Add
directly, we can avoid adding contentContainer
to contentContainer
.
Now we can have the new UI element be added to the content container that we wanted.
Add styles
To prevent locking the style class name in the code, it is deliberately not to add any uss class name in the code.
As with ScrollView, the child elements will not be editable, so style cannot be added to the child elements directly. The workaround is to apply the style via the root visual element.
I'm no pro with CSS, so this is what I have tried and it seems to work well. Use ">" to redirect it with the child name, or child classes.
With this method, I can separate the logic and visuals.
Extra
Custom attributes can be added to the visual element. Attributes can be used for additional settings or information that you may need for your custom visual element.
Document for the attributes.
public class PanelView : VisualElement
{
private Label _titleLabel { get; }
private VisualElement _contentContainer;
public override VisualElement contentContainer => _contentContainer;
// this is link to the _titleText in UxmlTraits class
// note: the naming is important, it has to be a property
// the name has to match the name in the attribute
public string TitleText
{
get => _titleLabel.text;
set => _titleLabel.text = value;
}
public new class UxmlFactory : UnityEngine.UIElements.UxmlFactory<PanelView, UxmlTraits>
{
}
public new class UxmlTraits : VisualElement.UxmlTraits
{
// this is how you can add custom attribute to the inspector in UI Builder
protected UxmlStringAttributeDescription _titleText = new UxmlStringAttributeDescription
{
name = "title-text",
defaultValue = "Title"
};
public override void Init(VisualElement ve, IUxmlAttributes bag, CreationContext cc)
{
base.Init(ve, bag, cc);
PanelView panelView = (PanelView)ve;
// get the value from the bag and assigned it to the class's property
panelView.TitleText = _titleText.GetValueFromBag(bag, cc);
}
}
public PanelView() : this("Title")
{
}
public PanelView(string title)
{
_titleLabel = string.IsNullOrEmpty(title) ? new Label() : new Label(title);
_titleLabel.name = "title";
_contentContainer = new VisualElement();
_contentContainer.name = "content-container";
base.hierarchy.Add(_titleLabel);
base.hierarchy.Add(_contentContainer);
}
}
A new inner UxmlTraits
class, derived from VisualElement.UxmlTraits
, is needed to enable adding new attributes.
Within the UxmlTraits
class, any amount of attributes can be added, but limited to the types given from the document.
Override the Init
function, to retrieve the attributes. Once you have the attributes, you can do whatever you want with it.
There's an important note, which is that the property variable from the visual element must exist and match the name given to the UxmlTraits
's attribute (not the variable name). The reason is, that Unity uses reflection to serialize and deserialize the attribute value to the property variable. If the property variable cannot be found, the attribute value cannot be saved.
the UI Builder relies on the existence of C# attributes (ie. { get; }) to be defined on your custom element that have the same name as the corresponding attribute (same name as in, uxml uses dashes "my-value" and corresponding C# attribute uses no dashes "myValue", case insensitive).
Source
With the custom attribute, I can set my title text directly.
Conclusion
With some c# code, to create the reusable panel, I can now reuse it for different purposes.