Skip to content

Basic Layout

SlayerDharok edited this page Jun 6, 2023 · 19 revisions

This is a tutorial to get you started with arranging your UI. If you're already familiar with WPF, you probably already know most of what this tutorial covers, since MGUI is heavily inspired by WPF and has many similar properties and functionality.


Getting Started

UI's with MGUI are defined hierarchically. The MGWindow is the outermost node of the visual tree. Most controls have a Content property, allowing you to specify exactly 1 child node, and that child node could then have its own Content, resulting in a tree structure (visual tree).

XAML:

<Window xmlns="clr-namespace:MGUI.Core.UI.XAML;assembly=MGUI.Core"
        Width="220" Height="100" Background="White">
    <CheckBox IsChecked="true">
        <TextBlock FontSize="9" Foreground="Black" Text="The CheckBox is the Window's Content, and this TextBlock is the CheckBox's Content" />
    </CheckBox>
</Window>

c#:

MGWindow Window1 = new(Desktop, 0, 0, 220, 100);
Window1.BackgroundBrush.NormalValue = SolidFillBrushes.White;
MGCheckBox CheckBox = new(Window1, true);
MGTextBlock Text = new(Window1, "The CheckBox is the Window's Content, and this TextBlock is the CheckBox's Content", Color.Black, 9);
CheckBox.SetContent(Text);
Window1.SetContent(CheckBox);

window1

If you just want to use the window as a blank slate for your UI content, then set MGWindow.WindowStyle=WindowStyle.None. This will hide several of the window's graphics, such as hiding its title bar, setting its Padding and BorderThickness to 0, setting its Background to Transparent etc.

XAML:

<Window xmlns="clr-namespace:MGUI.Core.UI.XAML;assembly=MGUI.Core"
        Width="220" SizeToContent="Height" WindowStyle="None">
    <CheckBox IsChecked="True">
        <TextBlock FontSize="9" Foreground="Black" Text="The CheckBox is the Window's Content, and this TextBlock is the CheckBox's Content" />
    </CheckBox>
</Window>

c#:

MGWindow Window1 = new(Desktop, 0, 0, 220, 100);
MGCheckBox CheckBox = new(Window1, true);
MGTextBlock Text = new(Window1, "The CheckBox is the Window's Content, and this TextBlock is the CheckBox's Content", Color.Black, 9);
CheckBox.SetContent(Text);
Window1.SetContent(CheckBox);
Window1.WindowStyle = WindowStyle.None;
Window1.ApplySizeToContent(SizeToContent.Height, 0, 0);

window2

(The background of the Window is Transparent. In this example it's just using Color.CornflowerBlue as that's what the screen was cleared with before drawing the UI)

Common Controls

A control (also commonly referred to as an 'element') generally just refers to any class that represents a visible object in your user interface.

The most commonly-used controls are:

Control Example Purpose
Border controls2 Acts as an outline for arbitrary content
Button controls3 Rectangular clickable shape that invokes some Action when clicked
CheckBox controls4 A 2-state button that cycles through checked and unchecked states when clicked
Unlike ToggleButton, the Content of a CheckBox is placed outside of the checkable button
ComboBox controls5 Sometimes called a 'Dropdown', 'Dropdown Box' Dropdown List' etc,
allows user to choose 1 value from a predefined list of values.
Value choices are displayed in a floating window that is contextually visible.
Image controls6 Draws a Texture2D
RadioButton controls6 A 2-state button (like a CheckBox) that allows mutual exclusion.
Several RadioButtons are added to a RadioButtonGroup so that only 1 RadioButton may be checked at a time
ScrollViewer controls7 Enables vertical and/or horizontal scrollbars around content that might require more space than is available
Slider controls8 Draggable number-line to allow choosing a numeric value
TabControl controls9 Hosts 0 to many TabItems
TabItem A single tab within a TabControl
TextBlock controls10 Renders Text content
Supported markdown can be found here
TextBox controls11 Allows user to input a text value
ToggleButton controls12 A 2-state button that cycles through checked and unchecked states when clicked
Unlike CheckBox, the Content of a ToggleButton is placed directly inside the checkable button
ToolTip controls13 Content that is attached to a parent, and is contextually visible when the parent is hovered by the mouse
ToolTips typically follow the mouse cursor (I.E. the top-left corner of the ToolTip is positioned where the mouse cursor is)
Window controls1 The outermost control that other content is placed upon, like a canvas to paint with your UI

Box Model

MGUI controls mostly adhere to the Box Model (except not all elements have a Border)

box model1

  • Margin is empty space reserved outside of the bounds an element draws itself to
  • Padding is empty space reserved inside the bounds an element draws itself to, but outside the bounds of the element's Content

For layout purposes (I.E. measuring how much space an element requires), an element's bounds is Margin+Border+Padding+Content. For rendering purposes (I.E. actually drawing the element), an element's bounds is Border+Padding+Content. The Background of an element spans Padding+Content.

Most elements, but not all, have a Border built-in to them. If you wish to have a Border around an element that doesn't have a built-in Border, just wrap it inside of a Border:

<Window xmlns="clr-namespace:MGUI.Core.UI.XAML;assembly=MGUI.Core"
        MinHeight="0" SizeToContent="WidthAndHeight" WindowStyle="None">
    <Border BorderBrush="Turquoise" BorderThickness="2" Background="Gray">
        <CheckBox Content="CheckBoxes don't have a built-in Border" />
    </Border>
</Window>

border1

Margin, Padding, and BorderThickness are all of type: Thickness.
Thicknesses are commonly defined in XAML as a comma delimited string: "{Left}, {Top}, {Right}, {Bottom}", or "{Left+Right}, {Top+Bottom}", or "{All}"

<Window xmlns="clr-namespace:MGUI.Core.UI.XAML;assembly=MGUI.Core"
        MinHeight="0" SizeToContent="WidthAndHeight" WindowStyle="None">
    <Border BorderBrush="Green" BorderThickness="4, 7, 2, 10" Background="White" Width="50" Height="50" />
</Window>

border2 Left is 4px, Top is 7px, Right is 2px, Bottom is 10px

Size and Alignment

All elements expose these common properties for sizing:

Type Property Description
int? MinWidth / MinHeight Min Width/Height in pixels, does not include Margin. 0 if null
int? MaxWidth / MaxHeight Max Width/Height in pixels, does not include Margin. int.MaxValue if null
int? PreferredWidth / PreferredHeight Desired Width/Height in pixels, does not include Margin.
If null, element is dynamically sized to be just big enough to draw itself and its Content
This value is clamped to the range [MinWidth, MaxWidth] or [MinHeight, MaxHeight] when possible
int ActualWidth / ActualHeight Readonly. The actual Width/Height in pixels, does not include Margin.
This value isn't updated immediately when changing size-related properties.
It's updated the next time the layout of the element is recalculated, during an Update tick

All elements expose these common properties for alignment:

Type Property
HorizontalAlignment HorizontalAlignment
HorizontalAlignment HorizontalContentAlignment
VerticalAlignment VerticalAlignment
VerticalAlignment VerticalContentAlignment

All alignment properties default to Stretch, meaning they will attempt to give as much space as possible to their child, and attempt to take up as much space as their parent offers.

Understanding Alignments

Content alignments (HorizontalContentAlignment / VerticalContentAlignment) determine what space an element offers to its children.
Regular alignments (HorizontalAlignment / VerticalAlignment) determine what space an element consumes from what its parent offers.
Alignments are processed top-down, starting from the root-level Window element.

To understand how space is allocated, let's walk through a simple example. Suppose you have the following Window:

<Window xmlns="clr-namespace:MGUI.Core.UI.XAML;assembly=MGUI.Core"
        Width="200" Height="100" 
        Background="Cyan" Padding="6" IsUserResizable="False" IsTitleBarVisible="False"
        BorderBrush="Gray" BorderThickness="2">
    <Border BorderBrush="Red" BorderThickness="2" Background="Green" Padding="12">
        <TextBlock Background="Orange" Text="Hello World" />
    </Border>
</Window>

alignment1

The Window is explicitly sized with Width="200", Height="100".
The Window reserves 6px of Padding on each side, and 2px of BorderThickness on each side, leaving 184x84 leftover space.
Since the Window's VerticalContentAlignment and HorizontalContentAlignment both default to Stretch, the Window attempts to give all this remaining space to its Content (The Border).

The Border has VerticalAlignment and HorizontalAlignment set to the default of Stretch, so the Border decides to consume all 184x84 space that its parent offers.
The Border reserves 12px Padding on each side, and 2px of BorderThickness on each side, leaving 156x56 leftover space.
Since the Border's VerticalContentAlignment and HorizontalContentAlignment both default to Stretch, the Border attempts to give all this remaining space to its Content (The TextBlock).

The TextBlock has VerticalAlignment and HorizontalAlignment set to the default of Stretch, so the TextBlock decides to consume all 156x56 space that its parent offers.


Suppose we set the Border's VerticalAlignment=Center:

<Window xmlns="clr-namespace:MGUI.Core.UI.XAML;assembly=MGUI.Core"
        Width="200" Height="100" 
        Background="Cyan" Padding="6" IsUserResizable="False" IsTitleBarVisible="False"
        BorderBrush="Gray" BorderThickness="2">
    <Border VerticalAlignment="Center" BorderBrush="Red" BorderThickness="2" Background="Green" Padding="12">
        <TextBlock Background="Orange" Text="Hello World" />
    </Border>
</Window>

alignment2

Now the Border is still offered 184x84 by its parent (the Window), but decides to only consume 47px of Height because that's the minimum Height it needs to draw itself and its Content. Those 47px are taken from the center of the Rectangular bounding box it was offered.


Now try setting the TextBlock's HorizontalAlignment=Right:

<Window xmlns="clr-namespace:MGUI.Core.UI.XAML;assembly=MGUI.Core"
        Left="200" Top="200" Width="200" Height="100" 
        Background="Cyan" Padding="6" IsUserResizable="False" IsTitleBarVisible="False"
        BorderBrush="Gray" BorderThickness="2">
    <Border VerticalAlignment="Center" BorderBrush="Red" BorderThickness="2" Background="Green" Padding="12">
        <TextBlock HorizontalAlignment="Right" Background="Orange" Text="Hello World" />
    </Border>
</Window>

alignment3

What if we also set the Border's HorizontalContentAlignment=Left?:

<Window xmlns="clr-namespace:MGUI.Core.UI.XAML;assembly=MGUI.Core"
        Left="200" Top="200" Width="200" Height="100" 
        Background="Cyan" Padding="6" IsUserResizable="False" IsTitleBarVisible="False"
        BorderBrush="Gray" BorderThickness="2">
    <Border VerticalAlignment="Center" HorizontalContentAlignment="Left" BorderBrush="Red" BorderThickness="2" Background="Green" Padding="12">
        <TextBlock HorizontalAlignment="Right" Background="Orange" Text="Hello World" />
    </Border>
</Window>

alignment4

The HorizontalContentAlignment of the parent (Border) took precedence over the HorizontalAlignment of the child (TextBlock), so the child ends up aligned Left. In other words, the Horizontal positioning of the innermost child (TextBlock) is dependent on these properties, in this order:

  1. Window's HorizontalContentAlignment
  2. Border's HorizontalAlignment
  3. Border's HorizontalContentAlignment
  4. TextBlock's HorizontalAlignment

Because alignments are processed in top-down order.

Containers

What if you wanted to put 2 CheckBoxes inside a Window? A Window can only have 1 element as its Content, so you'd need to wrap the CheckBoxes inside a container that supports multiple children.

Each container defines its own rules for how it arranges its children.

StackPanel

StackPanels arrange their children in order, either from Left to Right (Orientation=Horizontal) or Top to Bottom (Orientation=Vertical).

XAML:

<Window xmlns="clr-namespace:MGUI.Core.UI.XAML;assembly=MGUI.Core"
        MinHeight="0" SizeToContent="WidthAndHeight" WindowStyle="None">
    <StackPanel Orientation="Vertical" Background="MediumPurple">
        <CheckBox IsChecked="False">
            <TextBlock FontSize="9" Foreground="Black" Text="This CheckBox is unchecked" />
        </CheckBox>
        <CheckBox IsChecked="True">
            <TextBlock FontSize="9" Foreground="Black" Text="This CheckBox is checked" />
        </CheckBox>
    </StackPanel>
</Window>

window3

StackPanels only allocate as much space as is requested to the children. They make no guarantee that the children will fill all of the StackPanel's available space.

<Window xmlns="clr-namespace:MGUI.Core.UI.XAML;assembly=MGUI.Core"
        MinHeight="0" SizeToContent="WidthAndHeight" WindowStyle="None">
    <StackPanel Orientation="Horizontal" Background="Red" Width="300" Height="100">
        <Border Background="Green" Content="This element requests 150px" Width="150" />
        <Border Background="Orange" Content="This element requests 100px" Width="100" />
    </StackPanel>
</Window>

window5

StackPanel Width=300
StackPanel Remaining Width to allocate=300
1st child requests 150, receives 150
StackPanel Remaining Width to allocate=300-150=150
2nd child requests 100, receives 100
StackPanel Remaining Width to allocate=150-100=50, no children receive this Width

If there isn't enough space for all the children, space is allocated first-come first-serve until it runs out.

<Window xmlns="clr-namespace:MGUI.Core.UI.XAML;assembly=MGUI.Core"
        MinHeight="0" SizeToContent="WidthAndHeight" WindowStyle="None">
    <StackPanel Orientation="Horizontal" Background="Red" Width="220" Height="100">
        <Border Background="Green" Content="This element requests 150px" Width="150" />
        <Border Background="Orange" Content="This element requests 100px" Width="100" />
    </StackPanel>
</Window>

window4

StackPanel Width=220
StackPanel Remaining Width to allocate=220
1st child requests 150, receives 150
StackPanel Remaining Width to allocate=220-150=70
2nd child requests 100, receives 70

If you want a uniform padding between children, set StackPanel.Spacing to a non-zero value.

<Window xmlns="clr-namespace:MGUI.Core.UI.XAML;assembly=MGUI.Core"
        MinHeight="0" SizeToContent="WidthAndHeight" WindowStyle="None">
    <StackPanel Orientation="Horizontal" Background="Red" Height="50" Spacing="20">
        <Border Background="Green" Content="1" Width="40" />
        <Border Background="Purple" Content="2" Width="40" />
        <Border Background="Brown" Content="3" Width="40" />
        <Border Background="Magenta" Content="4" Width="40" />
    </StackPanel>
</Window>

window6

DockPanel

DockPanels arrange their children by 'docking' them to an edge (Left, Top, Right, or Bottom). Use the Dock property to specify which edge the child should be anchored to. The last child is given all the remaining space, effectively ignoring its Dock value.

<Window xmlns="clr-namespace:MGUI.Core.UI.XAML;assembly=MGUI.Core"
        MinHeight="0" SizeToContent="WidthAndHeight" WindowStyle="None">
    <DockPanel Background="Red" Width="250" Height="120">
        <Border Dock="Left" Background="Green" Content="Docked Left" />
        <Border Dock="Top" Background="Purple" Content="Docked Top" />
        <Border Dock="Bottom" Background="Orange" Content="Docked Bottom but is last child, spans all remaining space" />
    </DockPanel>
</Window>

dockpanel1

If you don't want the last child to fill all remaining space, then set LastChildFill=false:

<Window xmlns="clr-namespace:MGUI.Core.UI.XAML;assembly=MGUI.Core"
        MinHeight="0" SizeToContent="WidthAndHeight" WindowStyle="None">
    <DockPanel Background="Red" Width="250" Height="120" LastChildFill="false">
        <Border Dock="Left" Background="Green" Content="Docked Left" />
        <Border Dock="Top" Background="Purple" Content="Docked Top" />
        <Border Dock="Bottom" Background="Orange" Content="Docked Bottom" />
    </DockPanel>
</Window>

dockpanel4

You can dock multiple children to the same edge:

<Window xmlns="clr-namespace:MGUI.Core.UI.XAML;assembly=MGUI.Core"
        MinHeight="0" SizeToContent="WidthAndHeight" WindowStyle="None">
    <DockPanel Background="Red" Width="250" Height="120">
        <Border Dock="Left" Background="Green" Content="Docked Left" />
        <Border Dock="Top" Background="Purple" Content="Docked Top #1" />
        <Border Dock="Top" Background="Magenta" Content="Docked Top #2" />
        <Border Dock="Bottom" Background="Orange" Content="Last child" />
    </DockPanel>
</Window>

dockpanel2

The space is allocated inwards. The first child to be docked to the edge will be closer to the outermost edge of the entire DockPanel. Space is also allocated in first-come first-serve order. Try docking multiple children to the same edge, but don't add those children to the DockPanel consecutively:

<Window xmlns="clr-namespace:MGUI.Core.UI.XAML;assembly=MGUI.Core"
        MinHeight="0" SizeToContent="WidthAndHeight" WindowStyle="None">
    <DockPanel Background="Red" Width="250" Height="120">
        <Border Dock="Top" Background="Purple" Content="Docked Top #1" />
        <Border Dock="Left" Background="Green" Content="Docked Left" />
        <Border Dock="Top" Background="Magenta" Content="Docked Top #2" />
        <Border Dock="Bottom" Background="Orange" Content="Last child" />
    </DockPanel>
</Window>

dockpanel3

The Purple Border is docked to the top first, receiving the remaining unallocated width of the DockPanel, and just as much height as it needed.
Then the Green Border is docked to the left, receiving the remaining unallocated height of the DockPanel, and just as much width as it needed.
Then the Magenta Border is docked to the top, receiving the remaining unallocated width of the DockPanel, and just as much height as it needed.
Then the Orange Border fills all remaining unallocated width and height of the DockPanel, because it is the last child.

OverlayPanel

OverlayPanels arrange their children on top of each other. Children are drawn in ascending order of their ZIndex values, or in the order they were added to the OverlayPanel if no ZIndex is specified.

(Tip: You can easily specify Alpha transparency in XAML by multiplying a color by a decimal value, such as Background="Purple * 0.7")

<Window xmlns="clr-namespace:MGUI.Core.UI.XAML;assembly=MGUI.Core"
        MinHeight="0" SizeToContent="WidthAndHeight" WindowStyle="None">
    <OverlayPanel Background="White" Width="200" Height="60">
        <Border Background="Orange" VerticalAlignment="Bottom" Content="First child" />
        <Border Background="Purple * 0.7" VerticalAlignment="Stretch" Content="Second child" />
    </OverlayPanel>
</Window>

overlay1

If we swapped the order of the children, we'd get:

overlay2

You can also specify an Offset to apply to the children (Default value = "0, 0, 0, 0" (Left, Top, Right, Bottom))

<Window xmlns="clr-namespace:MGUI.Core.UI.XAML;assembly=MGUI.Core"
        MinHeight="0" SizeToContent="WidthAndHeight" WindowStyle="None">
    <OverlayPanel Background="Red" Width="200" Height="50">
        <Border Background="Purple" VerticalAlignment="Stretch" Content="First child" Offset="15, 3, 6, 25" />
        <Border Background="Orange" VerticalAlignment="Bottom" Content="Second child" Offset="5, 0, 12, 10" />
    </OverlayPanel>
</Window>

overlay3

If you want more control over the ordering of the children, you can specify a ZIndex value on each child.

<Window xmlns="clr-namespace:MGUI.Core.UI.XAML;assembly=MGUI.Core"
        MinHeight="0" SizeToContent="WidthAndHeight" WindowStyle="None">
    <OverlayPanel Background="Red" Width="200" Height="50">
        <Border Background="Purple" Content="First child" Offset="15, 3, 62, 25" ZIndex="0.01" />
        <Border Background="Orange" Content="Second child" Offset="5, 0, 50, 10" ZIndex="10" VerticalAlignment="Bottom" />
        <Border Background="Green" Content="Third child" Offset="120, 10, 4, 18" ZIndex="-10" />
    </OverlayPanel>
</Window>

overlay4

Even though the Green Border is added last, it appears underneath the other children because it has the lowest ZIndex value. (Children without a ZIndex are rendered first. If multiple children have the same ZIndex, ordering is based on the order the child was added to the panel)

Grid

Grids arrange their children according to the row+column (cell) they're placed in.
More details available here

TODO: Other containers such as HeaderedContentPresenter and UniformGrid.
More documentation coming soon... probably...

Clone this wiki locally