Creating a UI Element Directly on a Canvas

When you create one of the default UI objects from Unity, something slightly different happens from the other (non-UI) objects; the UI object will be automatically created under a Canvas. If a canvas doesn’t exist in the scene, a Canvas will first be created, then the UI object will be created under that new Canvas.

The built-in UI objects will get automatically placed under a Canvas

If you’ve used the Unity UI system before, you’ve probably noticed this, but maybe never given it much thought. I know I never did, until recently when I wanted that same behavior for a custom UI object I was working on. This was a generic object that I planned to re-use, and wanted it to be easily accessible from the menu.

So I created a function on and Editor script and added the MenuItem attribute. I then wrote the function to create my UI object and add it to the scene. Not thinking this through at all, I think I was expecting that it would just work and end up under a Canvas object. I was slightly surprised when that didn’t happen, until I stopped to think it through. Well duh was my next reaction. If I wanted to add it under a Canvas, that was the responsibility of my function.

So now I needed to figure out how the default UI elements did it, and just do the same thing. I figured there must be a function some where I could call. PlaceUIElementOnCanvas or something like that. However searches didn’t reveal anything like that, and I couldn’t find anything in the docs describing what I wanted to accomplish. So I turned to my good friend, dotPeek, to look at how the default UI elements do it (of course after doing all this, I discovered the Unity UI code is open source and available here )

The first step was figuring out where the menu options for these default objects lived. I poked around the C:\Program Files\Unity\Editor\Data directory for a while, looking for things that looked promising. Before long I found UnityEditor.UI.dll under UnityExtensions\Unity\GUISystem\Editor. Bingo.

Opening that up I quickly found the class where all these default objects are defined; MenuOptions. Here there are MenuItems for Image, Button, Toggle, Slider, etc. Picking one as an example, lets look at the implementation for the Button:

[MenuItem("GameObject/UI/Button", false, 2030)]
static public void AddButton(MenuCommand menuCommand)
{
    GameObject go = DefaultControls.CreateButton(GetStandardResources());
    PlaceUIElementRoot(go, menuCommand);
}

Pretty straightforward actually. If we were poke around, we’d see that DefaultControls.CreateButton contains all the logic for creating the Button object and adding all the required components. MenuOptions.PlaceUIElementRoot then does exactly what I was looking for by placing the newly created object on Canvas. Unfortunately the MenuOptions class is Internal so I can’t call it directly from my code. Lets look at PlaceUIElementRoot method, maybe that calls something else I can use.

private static void PlaceUIElementRoot(GameObject element, MenuCommand menuCommand)
{
    GameObject parent = menuCommand.context as GameObject;
    if (parent == null || parent.GetComponentInParent<Canvas>() == null)
    {
        parent = GetOrCreateCanvasGameObject();
    }

    string uniqueName = GameObjectUtility.GetUniqueNameForSibling(parent.transform, element.name);
    element.name = uniqueName;
    Undo.RegisterCreatedObjectUndo(element, "Create " + element.name);
    Undo.SetTransformParent(element.transform, parent.transform, "Parent " + element.name);
    GameObjectUtility.SetParentAndAlign(element, parent);
    if (parent != menuCommand.context) // not a context click, so center in sceneview
        SetPositionVisibleinSceneView(parent.GetComponent<RectTransform>(), element.GetComponent<RectTransform>());

    Selection.activeGameObject = element;
}

The first 3 lines are the most relevant to what I want to do, as those contain the logic for selecting the parent for the newly created GameObjet. It first checks the context for the command, given to the function by MenuCommand.context. If there is a context object, it then checks if it has a Canvas anywhere in it’s hierarchy. If so, it will parent the new object to the selected object.

However, if no Canvas is found it calls another method, MenuOptions.GetOrCreateCanvasGameObject. Lets see what that does:

static public GameObject GetOrCreateCanvasGameObject()
{
    GameObject selectedGo = Selection.activeGameObject;

    // Try to find a gameobject that is the selected GO or one if its parents.
    Canvas canvas = (selectedGo != null) ? selectedGo.GetComponentInParent<Canvas>() : null;
    if (canvas != null && canvas.gameObject.activeInHierarchy)
        return canvas.gameObject;

    // No canvas in selection or its parents? Then use just any canvas..
    canvas = Object.FindObjectOfType(typeof(Canvas)) as Canvas;
    if (canvas != null && canvas.gameObject.activeInHierarchy)
        return canvas.gameObject;

    // No canvas in the scene at all? Then create a new one.
    return MenuOptions.CreateNewUI();
}

This contains additional logic for trying to find a Canvas to place the new UI object on. The previous method plus this one define several rules for finding the Canvas to use

  1. If the context for the MenuCommand contains a Canvas anywhere in the hierarchy, the new object will be put under the context
  2. If the selected object in the Hierarchy has a Canvas anywhere in it’s hierarchy, that Canvas will be used (NOT the selction, but the object containing the Canvas)
  3. If there is a Canvas anywhere in the scene, that Canvas will be used. If there are multiple Canvases, the first one found will be used (this may be random)
  4. Otherwise a new Canvas will be created (this is done by MenuOptions.CreateNewUI)

Still nothing Public though, so nothing I can use from my code. The leaves me 2 options to get the behavior I want

  1. Call PlaceUIElementRoot with reflection
  2. Copy the PlaceUIElementRoot method into my code, along with any method it calls

I ultimately decided to go with #2 since I didn’t want to deal with reflection for this use case. Once I copied everything over, I just had to call PlaceUIElementRoot with my new UI object, and bam everything worked as I wanted. It would have been easier if Unity had an official API for doing this, but copying what the default objects did wasn’t too much work.

For reference, in case anyone else wants to enable this same behavior for their code, here’s the last remaining functions needed to make this work fully with the functions above.

static public GameObject CreateNewUI()
{
    // Root for the UI
    var root = new GameObject("Canvas");
    root.layer = LayerMask.NameToLayer(kUILayerName);
    Canvas canvas = root.AddComponent<Canvas>();
    canvas.renderMode = RenderMode.ScreenSpaceOverlay;
    root.AddComponent<CanvasScaler>();
    root.AddComponent<GraphicRaycaster>();
    Undo.RegisterCreatedObjectUndo(root, "Create " + root.name);

    // if there is no event system add one...
    CreateEventSystem(false);
    return root;
}

[MenuItem("GameObject/UI/Event System", false, 2100)]
public static void CreateEventSystem(MenuCommand menuCommand)
{
    GameObject parent = menuCommand.context as GameObject;
    CreateEventSystem(true, parent);
}

private static void CreateEventSystem(bool select)
{
    CreateEventSystem(select, null);
}

private static void CreateEventSystem(bool select, GameObject parent)
{
    var esys = Object.FindObjectOfType<EventSystem>();
    if (esys == null)
    {
        var eventSystem = new GameObject("EventSystem");
        GameObjectUtility.SetParentAndAlign(eventSystem, parent);
        esys = eventSystem.AddComponent<EventSystem>();
        eventSystem.AddComponent<StandaloneInputModule>();
        eventSystem.AddComponent<TouchInputModule>();

        Undo.RegisterCreatedObjectUndo(eventSystem, "Create " + eventSystem.name);
    }

    if (select && esys != null)
    {
        Selection.activeGameObject = esys.gameObject;
    }
}

You can also view all these methods on the official Unity UI repository here