18. Desktop targets: Windows, macOS, Linux

Goal

Why this matters

Prerequisites

1. Desktop backends at a glance

Avalonia ships multiple desktop backends; AppBuilder.UsePlatformDetect() selects the correct platform at runtime. Understanding the differences helps when you tweak options or debug native interop.

Platform Backend type Namespace Notes
Windows Win32Platform Avalonia.Win32 Win32 windowing with optional WinUI composition, ANGLE/OpenGL bridges, tray icon helpers.
Windows/macOS AvaloniaNativePlatform Avalonia.Native Shared native host (AppKit on macOS). Used for windowless scenarios and for macOS desktop builds.
Linux (X11) X11Platform Avalonia.X11 Traditional X11 windowing; integrates with FreeDesktop protocols.
Linux portals FreeDesktopPlatform Avalonia.FreeDesktop Supplements X11/Wayland with portal services (dialogs, notifications).

Startup options customize each backend:

AppBuilder.Configure<App>()
    .UsePlatformDetect()
    .With(new Win32PlatformOptions
    {
        RenderingMode = new[] { Win32RenderingMode.AngleEgl, Win32RenderingMode.Software },
        CompositionMode = new[] { Win32CompositionMode.WinUIComposition, Win32CompositionMode.RedirectionSurface },
        OverlayPopups = true
    })
    .With(new MacOSPlatformOptions
    {
        DisableDefaultApplicationMenuItems = false,
        ShowInDock = true
    })
    .With(new X11PlatformOptions
    {
        RenderingMode = new[] { X11RenderingMode.Glx, X11RenderingMode.Software },
        UseDBusMenu = true,
        WmClass = "MyAvaloniaApp"
    });

These options map to platform implementations in Avalonia.Win32, Avalonia.Native, and Avalonia.X11. Tune them when enabling extended client area, portals, or GPU interop.

2. Window fundamentals

<Window xmlns="https://github.com/avaloniaui"
        x:Class="MyApp.MainWindow"
        Width="1024" Height="720"
        CanResize="True"
        SizeToContent="Manual"
        WindowStartupLocation="CenterScreen"
        ShowInTaskbar="True"
        Topmost="False"
        Title="My App">

</Window>

Properties:

Persist position/size between runs:

protected override void OnOpened(EventArgs e)
{
    base.OnOpened(e);
    if (LocalSettings.TryReadWindowPlacement(out var placement))
    {
        Position = placement.Position;
        Width = placement.Width;
        Height = placement.Height;
        WindowState = placement.State;
    }
}

protected override void OnClosing(WindowClosingEventArgs e)
{
    base.OnClosing(e);
    LocalSettings.WriteWindowPlacement(new WindowPlacement
    {
        Position = Position,
        Width = Width,
        Height = Height,
        State = WindowState
    });
}

3. Custom title bars and chrome

SystemDecorations="None" removes native chrome; use extend-client-area hints for custom title bars.

<Window SystemDecorations="None"
        ExtendClientAreaToDecorationsHint="True"
        ExtendClientAreaChromeHints="PreferSystemChrome"
        ExtendClientAreaTitleBarHeightHint="32">
  <Grid>
    <Border Background="#1F2937" Height="32" VerticalAlignment="Top"
            PointerPressed="TitleBar_PointerPressed">
      <StackPanel Orientation="Horizontal" Margin="12,0" VerticalAlignment="Center" Spacing="12">
        <TextBlock Text="My App" Foreground="White"/>

        <Border x:Name="CloseButton" Width="32" Height="24" Background="Transparent"
                PointerPressed="CloseButton_PointerPressed">
          <Path Stroke="White" StrokeThickness="2" Data="M2,2 L10,10 M10,2 L2,10" HorizontalAlignment="Center" VerticalAlignment="Center"/>
        </Border>
      </StackPanel>
    </Border>

  </Grid>
</Window>
private void TitleBar_PointerPressed(object? sender, PointerPressedEventArgs e)
{
    if (e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
        BeginMoveDrag(e);
}

private void CloseButton_PointerPressed(object? sender, PointerPressedEventArgs e)
{
    Close();
}

4. Window transparency & effects

<Window TransparencyLevelHint="Mica, AcrylicBlur, Blur, Transparent">

</Window>
TransparencyLevelHint = new[]
{
    WindowTransparencyLevel.Mica,
    WindowTransparencyLevel.AcrylicBlur,
    WindowTransparencyLevel.Blur,
    WindowTransparencyLevel.Transparent
};

this.GetObservable(TopLevel.ActualTransparencyLevelProperty)
    .Subscribe(level => Debug.WriteLine($"Transparency: {level}"));

Platform support summary (subject to OS version, composition mode):

Design for fallback: ActualTransparencyLevel may be None--ensure backgrounds look good without blur.

5. Screens, DPI, and scaling

Center on active screen:

protected override void OnOpened(EventArgs e)
{
    base.OnOpened(e);
    var currentScreen = Screens?.ScreenFromWindow(this) ?? Screens?.Primary;
    if (currentScreen is null)
        return;

    var frameSize = PixelSize.FromSize(ClientSize, DesktopScaling);
    var target = currentScreen.WorkingArea.CenterRect(frameSize);
    Position = target.Position;
}

Handle scaling changes when moving between monitors:

ScalingChanged += (_, _) =>
{
    // Renderer scaling updated; adjust cached bitmaps if necessary.
};

6. Platform integration

6.1 Windows

6.2 macOS

6.3 Linux

7. Rendering & GPU selection

Avalonia renders through Skia; each backend exposes toggles for GPU acceleration and composition. Tune them to balance visuals versus compatibility.

Platform Rendering options When to change
Windows (Win32PlatformOptions) RenderingMode (AngleEgl, Wgl, Vulkan, Software), CompositionMode (WinUIComposition, etc.), GraphicsAdapterSelectionCallback, WinUICompositionBackdropCornerRadius Choose ANGLE + WinUI for blur effects, fall back to software for remote desktops, pick dedicated GPU in multi-adapter rigs.
macOS (AvaloniaNativePlatformOptions) RenderingMode (Metal, OpenGL, Software) Prefer Metal on modern macOS; include Software as fallback for virtual machines.
Linux (X11PlatformOptions) RenderingMode (Glx, Egl, Vulkan, Software), GlxRendererBlacklist, UseDBusMenu, UseDBusFilePicker Disable GLX on problematic drivers, force software when GPU drivers are unstable.

UseSkia accepts SkiaOptions for further tuning:

AppBuilder.Configure<App>()
    .UsePlatformDetect()
    .With(new SkiaOptions
    {
        MaxGpuResourceSizeBytes = 128 * 1024 * 1024, // cap VRAM usage
        UseOpacitySaveLayer = true
    })
    .UseSkia()
    .LogToTrace();

Inside a window you can inspect the actual implementation for diagnostics:

if (TryGetPlatformHandle() is { Handle: var hwnd, HandleDescriptor: "HWND" })
    Debug.WriteLine($"HWND: 0x{hwnd.ToInt64():X}");

Log area Avalonia.Rendering.Platform reports which backend was selected; capture it during startup when debugging GPU-related issues.

8. Packaging & deployment overview

Reference docs: Avalonia publishing guide (docs/publish.md).

9. Multiple window management tips

10. Troubleshooting

Issue Fix
Window blurry on high DPI Use vector assets; adjust RenderScaling; ensure UseCompositor is default
Transparency ignored Check ActualTransparencyLevel; verify OS support; remove conflicting settings
Custom chrome drag fails Ensure BeginMoveDrag only on left button down; avoid starting drag from interactive controls
Incorrect monitor on startup Set WindowStartupLocation or compute position using Screens before showing window
Linux packaging fails Include libAvaloniaNative.so dependencies; use Avalonia Debian/RPM packaging scripts

11. Practice exercises

  1. Build a window with custom title bar, including minimize, maximize, close, and move/resize handles.
  2. Request Mica/Acrylic, detect fallback, and apply theme-specific backgrounds for each transparency level.
  3. Implement a "Move to Next Monitor" command cycling through available screens.
  4. Persist window placement (position/size/state) to disk and restore on startup.
  5. Log which backend (Win32RenderingMode, X11RenderingMode, etc.) starts under different option combinations and document the impact on transparency and input latency.
  6. Create deployment artifacts: MSIX (Windows), .app (macOS), and AppImage/Flatpak (Linux) for a simple app.

Look under the hood (source bookmarks)

Check yourself

What's next