Search Unity

  1. Welcome to the Unity Forums! Please take the time to read our Code of Conduct to familiarize yourself with the forum rules and how to post constructively.
  2. Dismiss Notice

Question UIToolkit Custom Controls Runtime Constructible Without Loosing uxml uss references.

Discussion in 'UI Toolkit' started by LarsLundh, Sep 7, 2022.

  1. LarsLundh

    LarsLundh

    Joined:
    Sep 6, 2022
    Posts:
    20
    I am trying to use a constructor to create a custom control named Popup.
    I want to set the text of the popup by passing a string via the constructor.
    exactly in the same way that a Toggle VisualElement lets you do.

    Example.
    Unity's Custom Control:

    Toggle myToggle = new Toggle("Text for toggle via a constructor");
    My Custom Control:
    Popup myPopup = new Popup("Text in the centre of the red box from c#");

    I want to be able to construct VisualElements this way so that I can create X number of VisualElements depending on data received from SQL all with different member variable data.

    But what happens is that the myPopup instance has no Uxml or USS references at all.
    Unity gives this error:
    upload_2022-9-7_15-27-41.png
    Note: This error is on line 22 of popup c# example below.

    Please someone explain what I need to do.

    Below is an example project created to try to solve this.
    • The Main window has three visual elements, Header, Body, Footer.
    • The Header has a button that when pressed should place a Popup within the Body.
    • The Footer has a Toggle as an example of unity being able to do what I want to do.
    MainWindow: Uxml
    Code (Uxml):
    1.  
    2. <ui:UXML xmlns:ui="UnityEngine.UIElements" xmlns:uie="UnityEditor.UIElements" xsi="http://www.w3.org/2001/XMLSchema-instance" engine="UnityEngine.UIElements" editor="UnityEditor.UIElements" noNamespaceSchemaLocation="../../UIElementsSchema/UIElements.xsd" editor-extension-mode="False">
    3.     <Style src="project://database/Assets/UI/MainWindow.uss?fileID=7433441132597879392&amp;guid=c91c9754ad6b2de4fbf95507110cc6a7&amp;type=3#MainWindow" />
    4.     <MainWindow class="screen">
    5.         <ui:VisualElement name="Container" class="screen">
    6.             <ui:VisualElement name="Header" class="header">
    7.                 <ui:Button text="Spawn Popup" display-tooltip-when-elided="true" name="HeaderButton" style="top: 25%; width: 50%; left: 25%; height: 75px; font-size: 32px;" />
    8.             </ui:VisualElement>
    9.             <ui:VisualElement name="Body" class="body" style="background-color: rgb(77, 64, 64);" />
    10.             <ui:VisualElement name="Footer" class="footer" />
    11.         </ui:VisualElement>
    12.     </MainWindow>
    13. </ui:UXML>
    MainWindow: Picture
    upload_2022-9-7_14-47-28.png

    Popup: UXML
    Code (Uxml):
    1.  
    2. <ui:UXML xmlns:ui="UnityEngine.UIElements" xmlns:uie="UnityEditor.UIElements" xsi="http://www.w3.org/2001/XMLSchema-instance" engine="UnityEngine.UIElements" editor="UnityEditor.UIElements" noNamespaceSchemaLocation="../../../UIElementsSchema/UIElements.xsd" editor-extension-mode="False">
    3.     <Style src="project://database/Assets/UI/Popup/Popup.uss?fileID=7433441132597879392&amp;guid=fb7fb0e47e1b69e4e9637badbc4e54de&amp;type=3#Popup" />
    4.     <Popup int-attr="5" class="size">
    5.         <ui:VisualElement name="container" class="size container">
    6.             <ui:Label text="Change Me" display-tooltip-when-elided="true" name="PopupLabel" class="label" />
    7.         </ui:VisualElement>
    8.     </Popup>
    9. </ui:UXML>
    Popup: Picture
    upload_2022-9-7_14-50-41.png

    MainWindow:
    Code (CSharp):
    1.  
    2. public class MainWindow : VisualElement
    3. {
    4.     Button headerButton;
    5.     public new class UxmlFactory : UxmlFactory<MainWindow, UxmlTraits> {}
    6.  
    7.     public MainWindow()
    8.     {
    9.         this.RegisterCallback<GeometryChangedEvent>(OnGeometryChange);
    10.     }
    11.     void OnGeometryChange(GeometryChangedEvent evt)
    12.     {
    13.         headerButton = this.Q<Button>("HeaderButton");
    14.         headerButton.RegisterCallback<ClickEvent>(ev => OnHeaderButtonClicked());
    15.  
    16.         LookAtUnityBeingAbleToDoWhatIWantToDo();
    17.  
    18.         this.UnregisterCallback<GeometryChangedEvent>(OnGeometryChange);
    19.     }
    20.  
    21.     private void OnHeaderButtonClicked()
    22.     {
    23.         // This should work the same way as a toggle.
    24.         Popup popup = new Popup("Text in the middle of Red Box"); // losses its uxml and uss references.
    25.         popup.style.flexGrow = 1;
    26.         this.Q("Body").Add(popup);
    27.         Debug.Log("Pressed But nothing spawned T.T ");
    28.     }
    29.  
    30.     private void LookAtUnityBeingAbleToDoWhatIWantToDo()
    31.     {
    32.         // This is the goal... but I want the looks to be defined in uxml + uss
    33.         Toggle myToggle = new Toggle("Setting a label via a constructor");
    34.         this.Q("Footer").Add(myToggle);
    35.     }
    Popup:
    Code (CSharp):
    1.  
    2. public class Popup : VisualElement
    3. {
    4.     Label popupLabel;
    5.     string labelText = "Some Default Text";
    6.  
    7.     public new class UxmlFactory : UxmlFactory<Popup, UxmlTraits> {}
    8.     public Popup()
    9.     {
    10.         Debug.Log("Default Constructor");
    11.         this.RegisterCallback<GeometryChangedEvent>(OnGeometryChange);
    12.     }
    13.     public Popup(string text) : this()
    14.     {
    15.         Debug.Log("The Constructor Being Used");
    16.         labelText = text;
    17.     }
    18.  
    19.     void OnGeometryChange(GeometryChangedEvent evt)
    20.     {
    21.         popupLabel = this.Q<Label>("PopupLabel");
    22.         popupLabel.text = labelText;
    23.         this.UnregisterCallback<GeometryChangedEvent>(OnGeometryChange);
    24.     }
    25. }
     
    Last edited: Sep 8, 2022
  2. mcoted3d

    mcoted3d

    Unity Technologies

    Joined:
    Feb 3, 2016
    Posts:
    970
    When you create your instance manually in code, you'll also have to hook the UXML/USS styles manually. For example, our Toggle class does this internally in its constructor:
    labelElement.AddToClassList(labelUssClassName)


    In your situation, instead of instantiating the popup with
    new Popup()
    , I would instantiate it from the UXML
    VisualTreeAsset.Instantiate()
    . Then you can query the Label to change it's string.
     
  3. LarsLundh

    LarsLundh

    Joined:
    Sep 6, 2022
    Posts:
    20
    Project Details
    I am making a UI only application, it is a ROS2 robot control application. (Not a game)
    The goal is to always utilize uxml and uss files and to never need to programmatically build out a "Custom Control"

    Question
    To save your time, skip to - To Instantiate a Custom Control section
    Please could you fact check that section?

    Creating a Custom Control - Example
    upload_2022-9-10_11-55-16.png

    • Create a C# script, a uxml file a and a uss file. (Green1, Blue2, Yellow3)
    • Open UI Builder.
    • Make the C# Custom Control a child element of the uxml file (Drag Green1.2 into Blue2)
    • Add your design as a Child to the C# Custom control (The tree of children inside Green 1.1)
    • Add the uss file and apply your styles to each child element. (Yellow 3)
    Notes
    Look at 1.3, This is a preview of what 1.2 C#CustomControl Script will look like. Notice that it is empty.
    This is because 1.2 does not have any reference to the tree from the uxml (Blue2)
    In other words.
    "Only Visual Elements created via C# code within the constructor of 1.2 will appear in the preview of 1.3"
    try it by adding:
    Code (CSharp):
    1. this.Add(new Toggle("Toggle added via c#"));
    In the c# file constructor and then preview the CustomControl 1.3

    To Instantiate a Custom Control
    The Only ways to instantiate a C#CustomControl that will keep its reference to a uxml/uss file is to get a reference to the uxml parent that has a C#CustomControl as a child element:
    1. Using Direct Path
    Code (CSharp):
    1. VisualTreeAsset uxml = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>("Assets/UI/Example.uxml");
    2. VisualElement tree = uxml.Instantiate();
    2. Using Resources Api - Requires a folder named "Resources" that contains the uxml file.
    Code (CSharp):
    1. var uxml = Resources.Load<VisualTreeAsset("Example.uxml");
    2. VisualElement tree = uxml.Instantiate();
    Grumble
    Would be nice if there was a way to Instantiate Custom Controls without Direct Paths or Resources folder.

    The answer to my original question
    Refer to MainWindow.cs on line 21 at the top of this thread.
    Example code to spawn five popups and set the Label text:
    Code (CSharp):
    1.  
    2. private void OnHeaderButtonClicked()
    3.     {
    4.         // Get a reference to uxml file
    5.         VisualTreeAsset uxml = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>("Assets/UI/Popup.uxml");
    6.         int numberOfPopupToSpawn = 5;
    7.  
    8.         for (int i = 0; i < numberOfPopupToSpawn; i++)
    9.         {
    10.             // Create a new tree with references to uxml/uss
    11.             var popup = uxml.Instantiate();
    12.             popup.Q<Label>("PopupLabel").text = "Method that returns a title by ID";
    13.             // In this example "this" = another custom control
    14.             this.Q("Body").Add(popup);
    15.         }
    16.     }
    Further Reading
    If you are wondering how to programmatically create Custom Controls with uss references, this series is for you.
     
    Last edited: Sep 12, 2022
  4. LarsLundh

    LarsLundh

    Joined:
    Sep 6, 2022
    Posts:
    20
    CAREFUL!
    AssetDatabase.LoadAssetAtPath does not work during runtime.