-
Notifications
You must be signed in to change notification settings - Fork 8
Basic Layout
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.
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);
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);
(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)
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 |
Acts as an outline for arbitrary content | |
Button |
Rectangular clickable shape that invokes some Action when clicked |
|
CheckBox |
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 |
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 |
Draws a Texture2D
|
|
RadioButton |
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 |
Enables vertical and/or horizontal scrollbars around content that might require more space than is available | |
Slider |
Draggable number-line to allow choosing a numeric value | |
TabControl |
Hosts 0 to many TabItems
|
|
TabItem |
A single tab within a TabControl
|
|
TextBlock |
Renders Text content Supported markdown can be found here |
|
TextBox |
Allows user to input a text value | |
ToggleButton |
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 |
Content that is attached to a parent, and is contextually visible when the parent is hovered by the mouseToolTips typically follow the mouse cursor (I.E. the top-left corner of the ToolTip is positioned where the mouse cursor is) |
|
Window |
The outermost control that other content is placed upon, like a canvas to paint with your UI |
MGUI controls mostly adhere to the Box Model (except not all elements have a Border
)
-
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'sContent
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>
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>
Left is 4px, Top is 7px, Right is 2px, Bottom is 10px
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.
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>
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>
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>
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>
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:
-
Window's
HorizontalContentAlignment
-
Border's
HorizontalAlignment
-
Border's
HorizontalContentAlignment
-
TextBlock's
HorizontalAlignment
Because alignments are processed in top-down order.
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.
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>
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>
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>
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>
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>
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>
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>
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>
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.
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>
If we swapped the order of the children, we'd get:
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>
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>
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)
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...