Avalonia Custom TabItem Border

Avalonia Custom TabItem Border

Can be used as a reference to implement other forms of TabItem borders

Last updated 7/7/2025 10:36 PM
沙漠尽头的狼
8 min read
Category
Avalonia UI
Tags
.NET C# Avalonia UI Avalonia TabItem

Background

Thanks to @kankankan from the WeChat "Avalonia Development Discussion Group" for providing the code example:

The image below shows the modified effect according to personalized requirements:

To be compatible with the Semi.Avalonia theme style, our TabControl control theme starts by referencing Semi's Card-style control theme. The effect of Semi is as follows:

After our modifications, the display effect when switching between themes is as follows:

Usage

It is recommended to copy the control code from this article and maintain it yourself, as this control may not be updated in a timely manner.

This control is a secondary development based on Semi, so the following NuGet packages need to be installed:

Install-Package Semi.Avalonia -Version 11.2.1.8
Install-Package CodeWF.AvaloniaControls -Version 0.1.1.6
<Application xmlns="https://github.com/avaloniaui"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             x:Class="CodeWF.AvaloniaControls.Demo.App"
             xmlns:semi="https://irihi.tech/semi"
             xmlns:codewf="https://codewf.com">
    <Application.Styles>
        <semi:SemiTheme Locale="zh-CN" />
		<codewf:CodeWFTheme />
    </Application.Styles>
</Application>

Usage reference, the effect has been shown earlier, the code is as follows:

<UserControl xmlns="https://github.com/avaloniaui"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
             mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
             x:Class="CodeWF.AvaloniaControls.Demo.Pages.TabControlDemo">

    <Grid RowDefinitions="20 Auto 20 Auto" ColumnDefinitions="20 * 20">

        <TabControl Grid.Row="1" Grid.Column="1" VerticalAlignment="Top"
                    Theme="{StaticResource TrapezoidShapedTabControl}"
                    CornerRadius="10 10 0 0" TabStripPlacement="Top">
            <TabControl.Styles>
                <Style Selector="TabItem">
                    <Setter Property="CornerRadius" Value="10 10 0 0" />
                    <Setter Property="Padding" Value="12 8" />
                </Style>
            </TabControl.Styles>
            <TabItem Header="Data Management" />
            <TabItem Header="System Settings" />
            <TabItem Header="User Center" />
            <TabItem Header="Log Records" />
            <TabItem Header="Help Documentation" />
        </TabControl>

        <TabControl Grid.Row="3" Grid.Column="1" VerticalAlignment="Top"
                    Theme="{StaticResource TrapezoidShapedTabControl}"
                    CornerRadius="10 10 0 0" TabStripPlacement="Top">
            <TabControl.Styles>
                <Style Selector="TabControl">
                    <Setter Property="Background" Value="#551890FF"></Setter>
                </Style>
                <Style Selector="TabItem">
                    <Setter Property="CornerRadius" Value="10 10 0 0" />
                    <Setter Property="Foreground" Value="#FFFFFF" />
                    <Setter Property="Padding" Value="12 8" />
                    <Setter Property="MinHeight" Value="40" />
                    <Setter Property="BorderThickness" Value="1" />
                    <Setter Property="VerticalContentAlignment" Value="Center" />
                    <Setter Property="Background">
                        <Setter.Value>
                            <LinearGradientBrush StartPoint="50%, 0%"
                                                 EndPoint="50%, 100%">
                                <GradientStops>
                                    <GradientStop Color="#BAE7FF" Offset="0" />
                                    <GradientStop Color="#FFFFFF" Offset="1" />
                                </GradientStops>
                            </LinearGradientBrush>
                        </Setter.Value>
                    </Setter>
                </Style>
            </TabControl.Styles>
            <TabControl.Resources>
                <SolidColorBrush x:Key="TabItemLineHeaderPointeroverForeground">#1890FF</SolidColorBrush>
                <SolidColorBrush x:Key="TabItemLineHeaderSelectedForeground">#1890FF</SolidColorBrush>
            </TabControl.Resources>
            <TabItem Header="Data Management" />
            <TabItem Header="System Settings" />
            <TabItem Header="User Center" />
            <TabItem Header="Log Records" />
            <TabItem Header="Help Documentation" />
        </TabControl>
    </Grid>
</UserControl>

Implementation

Explaining code is always tedious, so I'll give a general overview.

This is the ControlTheme code for Semi's TabControl:

Our main modification is to change the border style of TabItem, so we copy and paste this Semi code directly, give the ControlTheme a different Key, and point the Value of the ItemContainerTheme (the boxed part in the image) to another TabItem control theme. Other parts of the code are modified as needed; I didn't touch the rest. Below is a screenshot of the modified code:

The TabItem control theme code is as follows. The key code is the custom border code location:

Among them, TrapezoidShapedTabItemBorder inherits from Control and mainly overrides its Render method:

public partial class TrapezoidShapedTabItemBorder : Control
{
    public const double DiagonalFilletRatio = 0.8;

    public static readonly StyledProperty<IBrush> BorderBrushProperty =
        AvaloniaProperty.Register<TrapezoidShapedTabItemBorder, IBrush>(nameof(BorderBrush),
            new SolidColorBrush(Color.Parse("#05CCCCCC")));

    public static readonly StyledProperty<double> BorderThicknessProperty =
        AvaloniaProperty.Register<TrapezoidShapedTabItemBorder, double>(nameof(BorderThickness), 1);

    public static readonly StyledProperty<IBrush> BackgroundProperty =
        AvaloniaProperty.Register<TrapezoidShapedTabItemBorder, IBrush>(nameof(Background), Brushes.DarkGreen);

    public IBrush BorderBrush
    {
        get => GetValue(BorderBrushProperty);
        set => SetValue(BorderBrushProperty, value);
    }

    public double BorderThickness
    {
        get => GetValue(BorderThicknessProperty);
        set => SetValue(BorderThicknessProperty, value);
    }

    public IBrush Background
    {
        get => GetValue(BackgroundProperty);
        set => SetValue(BackgroundProperty, value);
    }

    public override void Render(DrawingContext context)
    {
        base.Render(context);
        if (BorderThickness < 1)
        {
            return;
        }

        if (Parent?.Parent?.Parent is not TabControl tabControl ||
            Parent?.Parent is not TabItem currentTabItem)
        {
            return;
        }

        var index = tabControl.Items.IndexOf(currentTabItem);
        var isFirst = index == 0;
        var isLast = index == tabControl.Items.Count - 1;
        var radius = currentTabItem.CornerRadius;

        // Get the control's dimensions
        var rect = new Rect(Bounds.Size);
        var borderThickness = BorderThickness;
        // Offset the path to align lines with the pixel grid
        var halfBorder = borderThickness / 2.0;
        var adjustedRect = rect.Deflate(halfBorder);

        // Set border path
        var pathGeometry = new StreamGeometry();
        using (var ctx = pathGeometry.Open())
        {
            if (isFirst & !isLast)
            {
                if (tabControl.TabStripPlacement == Dock.Top)
                {
                    DrawTopFirstTabItemBorder(ctx, adjustedRect, radius, rect);
                }
            }
            else if (!isFirst && isLast)
            {
                if (tabControl.TabStripPlacement == Dock.Top)
                {
                    DrawTopLastTabItemBorder(ctx, adjustedRect, radius, rect);
                }
            }
            else
            {
                if (tabControl.TabStripPlacement == Dock.Top)
                {
                    DrawTopOtherTabItemBorder(ctx, adjustedRect, radius, rect);
                }
            }

            // bottom edge disappears (not drawn)
            // Here we skip the bottom edge path directly to ensure the bottom edge disappears
            ctx.EndFigure(isClosed: true);
        }

        // Draw the border
        context.DrawGeometry(Background, new Pen(BorderBrush, BorderThickness)
        {
            Thickness = BorderThickness,
            LineJoin = PenLineJoin.Round, // rounded join
            LineCap = PenLineCap.Round // rounded line cap
        }, pathGeometry);
    }
}

In Render, depending on whether the current TabItem is the first, last, or middle one in the TabControl, different methods are called to draw the border. For example, drawing the first TabItem:

Analysis:

  1. This is a right-angled trapezoid.
  2. The left side is a vertical straight line.
  3. The top-left corner is a 1/4 inner arc.
  4. The top-right corner is also an inner arc (can be drawn proportionally).
  5. The right side is a slanted line with a slope.
  6. The bottom-left and bottom-right corners can have outer arcs.

The border drawing code is as follows:

private static void DrawTopFirstTabItemBorder(StreamGeometryContext ctx, Rect adjustedRect, CornerRadius radius,
    Rect rect)
{
    var x = adjustedRect.Left;
    var y = adjustedRect.Bottom;

    // Start at bottom-left
    ctx.BeginFigure(new Point(x, y), isFilled: true);

    // bottom-left outer arc
    if (radius.BottomLeft > 0)
    {
        x = rect.Left + radius.BottomLeft;
        y = adjustedRect.Bottom - radius.BottomLeft;
        ctx.ArcTo(
            new Point(x, y),
            new Size(radius.BottomLeft, radius.BottomLeft),
            0,
            false,
            SweepDirection.CounterClockwise);
    }

    // left straight line
    y = adjustedRect.Top + radius.TopLeft;
    ctx.LineTo(new Point(x, y));

    // top-left inner arc
    if (radius.TopLeft > 0)
    {
        x += radius.TopLeft;
        y = adjustedRect.Top;
        ctx.ArcTo(
            new Point(x, y),
            new Size(radius.TopLeft, radius.TopLeft),
            0,
            false,
            SweepDirection.Clockwise);
    }

    // top straight line
    x = adjustedRect.Right - radius.TopRight * 2 - radius.BottomRight * 2;
    ctx.LineTo(new Point(x, y));

    // top-right inner arc
    if (radius.TopRight > 0)
    {
        x += radius.TopRight;
        y += radius.TopRight * DiagonalFilletRatio;
        ctx.ArcTo(
            new Point(x, y),
            new Size(radius.TopRight, radius.TopRight),
            0,
            false,
            SweepDirection.Clockwise);
    }

    // right slanted line
    x = adjustedRect.Right - radius.BottomRight;
    y = adjustedRect.Bottom - radius.BottomRight;
    ctx.LineTo(new Point(x, y));

    // bottom-right outer arc
    if (radius.BottomRight > 0)
    {
        x = rect.Right;
        y = adjustedRect.Bottom;
        ctx.ArcTo(
            new Point(x, y),
            new Size(radius.BottomRight, radius.BottomRight),
            0,
            false,
            SweepDirection.CounterClockwise);
    }
}

By calling the LineTo method of StreamGeometryContext to draw straight lines and the ArcTo method to draw arcs (inner and outer arcs), various border styles can be drawn. The drawing methods for the last TabItem and middle TabItems are similar.

The Edge TabItem effect is easy to achieve now, right?

Summary

This article only gives a rough idea. You can look at the specific implementation code and draw inferences; other control effects can be achieved in a similar way.

Repositories:

Keep Exploring

Related Reading

More Articles