Goal
Measure then Arrange) and how Layoutable and LayoutManager orchestrate it.GridSplitter, Viewbox, LayoutTransformControl, SharedSizeGroup).MeasureOverride/ArrangeOverride.Panel.ZIndex interact with layout.Why this matters
Prerequisites
Every control inherits from Layoutable (Layoutable.cs). The layout pass runs in two stages:
MeasureOverride in panels to lay out children.ArrangeOverride to position children based on the measured sizes.The LayoutManager (LayoutManager.cs) schedules layout passes when controls invalidate measure or arrange (InvalidateMeasure, InvalidateArrange).
InvalidateMeasure() when a control's desired size changes (for example, text content updates).InvalidateArrange() when position changes but desired size remains the same. Panels do this when children move without resizing.LayoutManager batches these requests; inspect timings via LayoutPassTiming or DevTools -> Layout tab.RendererDebugOverlays.LayoutTimeGraph to profile layout costs.InvalidateMeasure from inside MeasureOverride; schedule work via Dispatcher if you must recalc asynchronously.dotnet new avalonia.app -o LayoutPlayground
cd LayoutPlayground
Replace MainWindow.axaml with an experiment playground that demonstrates the core panels and alignment tools:
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="LayoutPlayground.MainWindow"
Width="880" Height="560"
Title="Layout Playground">
<Grid ColumnDefinitions="*,*" RowDefinitions="Auto,*" Padding="16" RowSpacing="16" ColumnSpacing="16">
<TextBlock Grid.ColumnSpan="2" Classes="h1" Text="Layout system without mystery"/>
<StackPanel Grid.Row="1" Spacing="12">
<TextBlock Classes="h2" Text="StackPanel"/>
<Border BorderBrush="#CCC" BorderThickness="1" Padding="8">
<StackPanel Spacing="6">
<Button Content="Top"/>
<Button Content="Middle"/>
<Button Content="Bottom"/>
<Button Content="Stretch me" HorizontalAlignment="Stretch"/>
</StackPanel>
</Border>
<TextBlock Classes="h2" Text="DockPanel"/>
<Border BorderBrush="#CCC" BorderThickness="1" Padding="8">
<DockPanel LastChildFill="True">
<TextBlock DockPanel.Dock="Top" Text="Top bar"/>
<TextBlock DockPanel.Dock="Left" Text="Left" Margin="0,4,8,0"/>
<Border Background="#F0F6FF" CornerRadius="4" Padding="8">
<TextBlock Text="Last child fills remaining space"/>
</Border>
</DockPanel>
</Border>
</StackPanel>
<StackPanel Grid.Column="1" Grid.Row="1" Spacing="12">
<TextBlock Classes="h2" Text="Grid + WrapPanel"/>
<Border BorderBrush="#CCC" BorderThickness="1" Padding="8">
<Grid ColumnDefinitions="Auto,*" RowDefinitions="Auto,Auto,Auto" ColumnSpacing="8" RowSpacing="8">
<TextBlock Text="Name:"/>
<TextBox Grid.Column="1" MinWidth="200"/>
<TextBlock Grid.Row="1" Text="Email:"/>
<TextBox Grid.Row="1" Grid.Column="1"/>
<TextBlock Grid.Row="2" Text="Notes:" VerticalAlignment="Top"/>
<TextBox Grid.Row="2" Grid.Column="1" Height="80" AcceptsReturn="True" TextWrapping="Wrap"/>
</Grid>
</Border>
<Border BorderBrush="#CCC" BorderThickness="1" Padding="8">
<WrapPanel ItemHeight="32" MinWidth="200" ItemWidth="100" HorizontalAlignment="Left">
<Button Content="One"/>
<Button Content="Two"/>
<Button Content="Three"/>
<Button Content="Four"/>
<Button Content="Five"/>
<Button Content="Six"/>
</WrapPanel>
</Border>
</StackPanel>
</Grid>
</Window>
Run the app and resize the window. Observe how StackPanel, DockPanel, Grid, and WrapPanel distribute space.
Margin vs Padding: Margin adds space around a control; Padding adds space inside a container.HorizontalAlignment/VerticalAlignment: Stretch makes controls fill available space; Center, Start, End align within the assigned slot.Width/Height: fixed sizes; use sparingly. Prefer MinWidth, MaxWidth, MinHeight, MaxHeight for adaptive layouts.Auto (size to content), * (take remaining space), 2* (take twice the share). Column/row definitions can mix Auto, star, and pixel values.SharedSizeGroupSharedSizeGroup lets multiple grids share sizes within a scope. Mark the parent with Grid.IsSharedSizeScope="True":
<Grid ColumnDefinitions="Auto,*" RowDefinitions="Auto,Auto" Grid.IsSharedSizeScope="True">
<Grid.ColumnDefinitions>
<ColumnDefinition SharedSizeGroup="Label"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Grid RowDefinitions="Auto,Auto" ColumnDefinitions="Auto,*">
<TextBlock Text="First" Grid.Column="0"/>
<TextBox Grid.Column="1" MinWidth="200"/>
</Grid>
<Grid Grid.Row="1" ColumnDefinitions="Auto,*">
<TextBlock Text="Second" Grid.Column="0"/>
<TextBox Grid.Column="1" MinWidth="200"/>
</Grid>
</Grid>
All label columns share the same width. Source: Grid.cs and DefinitionBase.cs.
GridSplitter<Grid ColumnDefinitions="3*,Auto,2*">
<StackPanel Grid.Column="0">...</StackPanel>
<GridSplitter Grid.Column="1" Width="6" ShowsPreview="True" Background="#DDD"/>
<StackPanel Grid.Column="2">...</StackPanel>
</Grid>
GridSplitter lets users resize star-sized columns/rows. Implementation: GridSplitter.cs.
Viewbox and LayoutTransformControlViewbox scales its child proportionally to fit the available space.LayoutTransformControl applies transforms (rotate, scale, skew) while preserving layout.<Viewbox Stretch="Uniform" Width="200" Height="200">
<TextBlock Text="Scaled" FontSize="24"/>
</Viewbox>
<LayoutTransformControl>
<LayoutTransformControl.LayoutTransform>
<RotateTransform Angle="-10"/>
</LayoutTransformControl.LayoutTransform>
<Border Padding="12" Background="#E7F1FF">
<TextBlock Text="Rotated layout"/>
</Border>
</LayoutTransformControl>
Sources: Viewbox.cs, LayoutTransformControl.cs.
Panel.ZIndexControls inside the same panel respect Panel.ZIndex for stacking order. Higher ZIndex renders above lower values.
<Canvas>
<Rectangle Width="100" Height="80" Fill="#60FF0000" Panel.ZIndex="1"/>
<Rectangle Width="120" Height="60" Fill="#6000FF00" Panel.ZIndex="2" Margin="20,10,0,0"/>
</Canvas>
ScrollViewer wraps content to provide scrolling. When the child implements ILogicalScrollable (e.g., ItemsPresenter with virtualization), the scrolling is smoother and can skip measurement of offscreen content.
<ScrollViewer HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto">
<StackPanel>
</StackPanel>
</ScrollViewer>
ILogicalScrollable (see LogicalScroll.cs).ScrollViewer triggers layout when viewports change.Derive from Panel and override MeasureOverride/ArrangeOverride to create custom layout logic. Example: a simplified UniformGrid:
using Avalonia;
using Avalonia.Controls;
using Avalonia.Layout;
namespace LayoutPlayground.Controls;
public class UniformGridPanel : Panel
{
public static readonly StyledProperty<int> ColumnsProperty =
AvaloniaProperty.Register<UniformGridPanel, int>(nameof(Columns), 2);
public int Columns
{
get => GetValue(ColumnsProperty);
set => SetValue(ColumnsProperty, value);
}
protected override Size MeasureOverride(Size availableSize)
{
foreach (var child in Children)
{
child.Measure(Size.Infinity);
}
var rows = (int)Math.Ceiling(Children.Count / (double)Columns);
var cellWidth = availableSize.Width / Columns;
var cellHeight = availableSize.Height / rows;
return new Size(cellWidth * Columns, cellHeight * rows);
}
protected override Size ArrangeOverride(Size finalSize)
{
var rows = (int)Math.Ceiling(Children.Count / (double)Columns);
var cellWidth = finalSize.Width / Columns;
var cellHeight = finalSize.Height / rows;
for (var index = 0; index < Children.Count; index++)
{
var child = Children[index];
var row = index / Columns;
var column = index % Columns;
var rect = new Rect(column * cellWidth, row * cellHeight, cellWidth, cellHeight);
child.Arrange(rect);
}
return finalSize;
}
}
child.DesiredSize from Measure.Layoutable and Panel sources to understand helper methods like ArrangeRect.While running the app press F12 -> Layout tab:
RendererDebugOverlays in code: see RendererDebugOverlays.cs).You can also enable layout logging:
AppBuilder.Configure<App>()
.UsePlatformDetect()
.LogToTrace(LogEventLevel.Debug, new[] { LogArea.Layout })
.StartWithClassicDesktopLifetime(args);
LogArea.Layout logs measure/arrange operations to the console.
Grid.IsSharedSizeScope and SharedSizeGroup across multiple form sections so labels align perfectly, even when collapsed sections are toggled.GridSplitter with a two-column layout; ensure minimum sizes keep content readable.LayoutTransformControl to rotate it; evaluate how alignment behaves inside the transform.UniformGridPanel and compare measurement behaviour in DevTools.ScrollViewer, enable DevTools Layout overlay, and observe how viewport size changes the arrange rectangles.LogArea.Layout and capture a trace of Measure/Arrange calls when resizing. Inspect LayoutManager.Instance.LayoutPassTiming.LastLayoutTime to correlate with DevTools overlays.Layoutable.csLayoutManager.csLayoutPassTiming.cs, RendererDebugOverlays.csGrid.cs, DefinitionBase.csLayoutTransformControl.csScrollViewer.cs, LogicalScroll.csVirtualizingStackPanel.csSharedSizeGroup influence multiple grids? What property enables shared sizing?LayoutTransformControl instead of a render transform?Panel.ZIndex for children inside the same panel?What's next