UI builder and custom elements

Hi

I want to make a custom UI element just to make my workflow a bit easier. A collection of UI elements with some variables to change, just like any button, progress bar, vector3 field, etc. Is this something we can do and it will work with the UI builder? I know you can place in other UXML files into your current one but I don’t see a way to use custom code along with that custom UXML.

I basically want to have some exposed variables like this but for my own custom elements:

Yep, you can do that by inheriting from VisualElement. You can then define a UxmlFactory inside your element class to expose it to UXML (so you can create it with a tag with same name as you C# type). And you can define a UxmlTraits class to add custom attributes that you can then set in UXML. These custom attributes will then be exposed in the UI Builder inspector, like Text, Binding Path, Name, etc. (in your screenshot). Here’s a full example of a custom element and some UXML attribute declarations:

class BuilderAttributesTestElement : VisualElement
{
    public enum Existance
    {
        None,
        Good,
        Bad
    }

    public new class UxmlFactory : UxmlFactory<BuilderAttributesTestElement, UxmlTraits> { }

    public new class UxmlTraits : VisualElement.UxmlTraits
    {
        UxmlStringAttributeDescription m_String = new UxmlStringAttributeDescription { name = "string-attr", defaultValue = "default_value" };
        UxmlFloatAttributeDescription m_Float = new UxmlFloatAttributeDescription { name = "float-attr", defaultValue = 0.1f };
        UxmlDoubleAttributeDescription m_Double = new UxmlDoubleAttributeDescription { name = "double-attr", defaultValue = 0.1 };
        UxmlIntAttributeDescription m_Int = new UxmlIntAttributeDescription { name = "int-attr", defaultValue = 2 };
        UxmlLongAttributeDescription m_Long = new UxmlLongAttributeDescription { name = "long-attr", defaultValue = 3 };
        UxmlBoolAttributeDescription m_Bool = new UxmlBoolAttributeDescription { name = "bool-attr", defaultValue = false };
        UxmlColorAttributeDescription m_Color = new UxmlColorAttributeDescription { name = "color-attr", defaultValue = Color.red };
        UxmlEnumAttributeDescription<Existance> m_Enum = new UxmlEnumAttributeDescription<Existance> { name = "enum-attr", defaultValue = Existance.Bad };

        public override IEnumerable<UxmlChildElementDescription> uxmlChildElementsDescription
        {
            get { yield break; }
        }

        public override void Init(VisualElement ve, IUxmlAttributes bag, CreationContext cc)
        {
            base.Init(ve, bag, cc);
            var ate = ve as BuilderAttributesTestElement;

            ate.Clear();

            ate.stringAttr = m_String.GetValueFromBag(bag, cc);
            ate.Add(new TextField("String") { value = ate.stringAttr });

            ate.floatAttr = m_Float.GetValueFromBag(bag, cc);
            ate.Add(new FloatField("Float") { value = ate.floatAttr });

            ate.doubleAttr = m_Double.GetValueFromBag(bag, cc);
            ate.Add(new DoubleField("Double") { value = ate.doubleAttr });

            ate.intAttr = m_Int.GetValueFromBag(bag, cc);
            ate.Add(new IntegerField("Integer") { value = ate.intAttr });

            ate.longAttr = m_Long.GetValueFromBag(bag, cc);
            ate.Add(new LongField("Long") { value = ate.longAttr });

            ate.boolAttr = m_Bool.GetValueFromBag(bag, cc);
            ate.Add(new Toggle("Toggle") { value = ate.boolAttr });

            ate.colorAttr = m_Color.GetValueFromBag(bag, cc);
            ate.Add(new ColorField("Color") { value = ate.colorAttr });

            ate.enumAttr = m_Enum.GetValueFromBag(bag, cc);
            var en = new EnumField("Enum");
            en.Init(m_Enum.defaultValue);
            en.value = ate.enumAttr;
            ate.Add(en);
        }
    }

    public string stringAttr { get; set; }
    public float floatAttr { get; set; }
    public double doubleAttr { get; set; }
    public int intAttr { get; set; }
    public long longAttr { get; set; }
    public bool boolAttr { get; set; }
    public Color colorAttr { get; set; }
    public Existance enumAttr { get; set; }
}

UXML usage:

<BuilderAttributesTestElement
    string-attr="my-string"
    float-attr="4.2"
    double-attr="4.3"
    int-attr="4"
    long-attr="423"
    bool-attr="true"
    color-attr="#CA7C03FF"
    enum-attr="Good" />

And this is how its UI Builder inspector looks like:

25 Likes

Oh wow, thanks for the detailed and quick response! Very much appreciated! I just got one more question. In the UI builder my element appears in an empty group (as it has no name). Is there a way to fix that so I can easily see it in the builder?

I found another thing that I need help with. How do I make so the UI elements actually update when I use them? Right now the elements just stay the way they are when updating them through a script, in my case, at runtime.

I’m not sure what you mean. Might help to see a screenshot.

It really depends what you mean by “update”. Like, change style? Change value (in case of field)? Change shape/size when setting myelement.style.width/height? All of these should result in immediate updating of the game view UI so something must be going wrong if you don’t see any updates.

That is, if you mean the UI in the Game view (or used in a custom EditorWindow). If you are still referring to the UI inside the UI Builder’s Canvas, this UI is always going to be static. You can turn on Preview mode from the top right corner of the Viewport to be able to interact with the UI (like, see hover states and expand Foldouts), but any code not inside a custom C# element (inheriting from VisualElement) will not run. Like, the Panel Renderer code does not run inside the Builder.

Here’s what I mean with the empty group:

And what I meant with not updating is like I have a string property, like a label, and when I change it, it does not update the text on the element. It works in the UI builder but not while in-game. I used your example and got that result. But I then realized I could peek into the other built-in UI elements to see how they were built and I’ve started making it find the right element and update it based on the property.
Like this:

public string Label
{
    get { return this.Q<Label>("progress-label").text; }
    set { this.Q<Label>("progress-label").text = value; }
}

Is that the wrong way and has something just gone wrong with the example you provided?

This is a bug. Everything below “Packages” should be custom C# elements the UI Builder finds via reflection that have a UxmlFactory defined. It just uses the C# type name and namespace to generate the tree items in that tree, where each namespace is a “folder”. Is GameProgressBar inside a namespace? We might just not handle no-namespace elements properly.

The example element I posted is really just a test for the custom UXML attribute declaration. You’ll notice that only the Init() function of the UxmlTraits implementation completely recreates all children inside the element. This Init() is what is called when element is first initialized from UXML and never again. The UI Builder cheats a bit by calling this Init() after the element is initialized every time you change the attribute values in the UI Builder’s Inspector. But this does not happen at runtime.

What you did with the public string Label property above is exactly what you need to do. The only suggestion I have is to cache the result of this.Q<Label>("progress-label") - can do this in the element’s constructor - so you don’t have to find it every time you change its text value.

1 Like

It is not in a namespace so I guess it’s the bug showing.

But now I have a new issue that has popped up. When I build my game I get the error “Element ‘’ has no registered factory method.” Quickly looking at some other built-in UI elements I can’t seem to find anything that relates to that. It only happens in a build too (Mono, no stripping). What could be causing this and is there a fix?

Make sure your element is not defined inside an Editor assembly (script lives inside an Editor folder). By extension, it also doesn’t use anything from UnityEditor namespace.

My element is not in the Editor assembly and does not use any UnityEditor namespace things… Now what?

Make sure your UxmlFactory inherits correctly:

class MyElement : VisualElement
{
    public new class UxmlFactory : UxmlFactory<MyElement, UxmlTraits> { }
    ...
}
1 Like

It is correct and the problem persists. Just to be 100% sure, I checked the generated code and all the code is there, so absolutely nothing has been stripped out (still haven’t used any stripping).
Here is the error message if it helps:

Element 'GameProgressBar' has no registered factory method.
UnityEngine.DebugLogHandler:Internal_Log(LogType, LogOption, String, Object)
UnityEngine.DebugLogHandler:LogFormat(LogType, Object, String, Object[ ])
UnityEngine.Logger:LogFormat(LogType, String, Object[ ])
UnityEngine.Debug:LogErrorFormat(String, Object[ ])
UnityEngine.UIElements.VisualTreeAsset:Create(VisualElementAsset, CreationContext) (at C:\buildslave\unity\build\Modules\UIElements\UXML\VisualTreeAsset.cs:403)
UnityEngine.UIElements.VisualTreeAsset:CloneSetupRecursively(VisualElementAsset, Dictionary`2, CreationContext) (at C:\buildslave\unity\build\Modules\UIElements\UXML\VisualTreeAsset.cs:195)
UnityEngine.UIElements.VisualTreeAsset:CloneSetupRecursively(VisualElementAsset, Dictionary`2, CreationContext) (at C:\buildslave\unity\build\Modules\UIElements\UXML\VisualTreeAsset.cs:246)
UnityEngine.UIElements.VisualTreeAsset:CloneTree(VisualElement, Dictionary`2, List`1) (at C:\buildslave\unity\build\Modules\UIElements\UXML\VisualTreeAsset.cs:182)
UnityEngine.UIElements.VisualTreeAsset:CloneTree(VisualElement, Dictionary`2) (at C:\buildslave\unity\build\Modules\UIElements\UXML\VisualTreeAsset.cs:145)
UnityEngine.UIElements.VisualTreeAsset:CloneTree(VisualElement) (at C:\buildslave\unity\build\Modules\UIElements\UXML\VisualTreeAsset.cs:134)
Unity.UIElements.Runtime.PanelRenderer:RecreateUIFromUxml() (at Project\Packages\UIERuntime\Runtime\PanelRenderer.cs:235)
Unity.UIElements.Runtime.PanelRenderer:Start() (at Project\Packages\UIERuntime\Runtime\PanelRenderer.cs:215)

And here’s a very stripped down version of my code but it still has the important parts:

public class GameProgressBar : TemplateContainer
{
    [Preserve]
    public new class UxmlFactory : UxmlFactory<GameProgressBar, UxmlTraits> { }

    [Preserve]
    public new class UxmlTraits : VisualElement.UxmlTraits
    {
        UxmlStringAttributeDescription label = new UxmlStringAttributeDescription() { name = "label", defaultValue = "Progress" };

        public override IEnumerable<UxmlChildElementDescription> uxmlChildElementsDescription
        {
            get { yield break; }
        }

        public override void Init(VisualElement ve, IUxmlAttributes bag, CreationContext cc)
        {
            base.Init(ve, bag, cc);
            GameProgressBar ate = ve as GameProgressBar;

            ate.Label = label.GetValueFromBag(bag, cc);
        }
    }

    private Label label;

    public string Label
    {
        get { return label.text; }
        set { label.text = value; }
    }

    public GameProgressBar()
    {
        label = new Label() { name = "progress-label" };
        hierarchy.Add(label);
    }
}

Wait, why are you inheriting from TemplateContainer? That may be the issue.

But just to be sure, can you also paste the snippet of UXML that uses GameProgressBar.

I’m actually not sure why I was inheriting from TemplateContainer. I changed it to VisualElement but it didn’t change anything.
Here’s the UXML that use GameProgressBar:

<ui:UXML xmlns:ui="UnityEngine.UIElements" xmlns:uie="UnityEditor.UIElements">
    <ui:VisualElement name="TopHolder" style="align-items: center;">
        <Style src="HUD.uss" />
        <ui:VisualElement class="element-background" style="width: 260px; height: 80px;">
            <ui:Label text="Wave 99" name="WaveLabel" class="label" style="font-size: 28px; -unity-text-align: middle-center; height: 40px;" />
            <GameProgressBar Label="Label" MinValue="0.39" MaxValue="1.74" CurrentValue="4.89" BackgroundColor="#FF0000FF" TopColor="#84FF00FF" background-color="#484848FF" name="WaveProgressBar" top-color="#9A0003FF" direction="LeftToRight" style="height: 24px; margin-left: 10px; margin-right: 10px; margin-top: 4px;" />
        </ui:VisualElement>
    </ui:VisualElement>
</ui:UXML>

Bit of a UI Builder rookie so apologies if this is a dumb question, but I had this working using the tanks example and having a script containing:

    // UI elements to update with variable information
    private Label resourceVolumeLabel;
    private Label resourceEnergyLabel;
    private Label resourceMineralsLabel;

    private void Awake()
    {
        panelRenderer = GetComponent<PanelRenderer>();
    }

    void OnEnable()
    {
        panelRenderer.postUxmlReload = BindLogic;
    }

    private IEnumerable<UnityEngine.Object> BindLogic()
    {
        var root = panelRenderer.visualTree;

        resourceVolumeLabel = root.Q<Label>("resource-label-volume");
        resourceMineralsLabel = root.Q<Label>("resource-label-minerals");
        resourceEnergyLabel = root.Q<Label>("resource-label-energy");

        return null;
    }

This worked a couple of days ago, all values updating as necessary, and now without changing anything and reloading my project I seem to have somehow stopped this working properly. I can confirm that OnEnable is called, so panelRenderer.postUxmlReload gets this binding IEnum. Where my UI actually updated before, I now just get a null ref exception showing that resourceVolumeLabel is never assigned to.

Is there anything missing from this barebones logic to get a panel renderer to work properly?

Thanks

Can you make sure that you have a Label with resource-label-volume name in your UXML?

I’m going to hop on this thread because I am also trying to make a custom VisualElement. I’ll post my test class below. My issue is that in the inspector once a value is given to my “DataType” attribute and then the template is saved it removes the value within the inspector. The UXML looks fine and maintains the entered value. The only issue is in-editor. (The idea is to do something similar to Enums and resolve a type with a given name and assembly).

public class TestElement : VisualElement
    {
        public class TestElementFactory : UxmlFactory<TestElement, TestElementTraits> { }

        public class TestElementTraits : UxmlTraits
        {
            private UxmlStringAttributeDescription m_typeStringAttribute;

            public override void Init(VisualElement ve, IUxmlAttributes bag, CreationContext cc)
            {
                base.Init(ve, bag, cc);
                var te = ve as TestElement;
                try
                {
                    if (te != null)
                    {
                        te.TypeString = m_typeStringAttribute.GetValueFromBag(bag, cc);
                        var test = Type.GetType( te.TypeString);
                        if (test != null)
                        {
                            ((TestElement) ve)._dataType = test;
                        }
                    }
                }
                catch
                {
                    // ignored
                }
            }

            public TestElementTraits()
            {
                m_typeStringAttribute = new UxmlStringAttributeDescription { name = "DataType" };
            }
        }

        private Type _dataType;

        private string TypeString { get; set; }
    }

For the attribute to work properly in the UI Builder, you need to expose a public property on TestElement that has the same name as the UXML attribute name. If TypeString was supposed to be it, change it to DataType (or you might need dataType) and make it public.

See my example above: UI builder and custom elements

1 Like

Hello,

I made a bit o UI for my ability system that using the UI Builder. It works fine but I’ like to make a custom ViualElement as that bit of UI is composed of several child visual element.
I’m currently adding them through script but I wonder if I can make use of a UXML template in that custom visual element (through the UxmlFactory maybe ?).

Do you have any sample on how to do that sort of things ?

Also is there a way to define a style sheet that can be overidden ?

Just get a reference to you UXML asset via the AssetDatabase and call CloneTree(this) on it in your custom C# element’s constructor. This will populate the insides of your custom element with the contents of the UXML. You can then use uQuery (.Q or .Query) to find specific elements and register your desired events.

Even better, you can embed any styles for your custom C# element inside a USS that is directly referenced by the UXML via the tag so there’s no need to do the same AssetDatabase lookup for the USS. If you used the UI Builder, it will embed the USS in your UXML for you.

Not too sure why you mean. StyleSheets (USS) are can be added to any element via element.AddStyleSheetPath(). The order matters, so if you add another USS later, if there are any duplicate selectors between the existing and new USS, the ones in the new USS will win.

3 Likes