Avalonia自訂TabItem邊框

Avalonia自訂TabItem邊框

可作為參考,實現其他形式的TabItem邊框

最後更新 2025/7/7 下午10:36
沙漠尽头的狼
預計閱讀 7 分鐘
分類
Avalonia UI
標籤
.NET C# Avalonia UI Avalonia TabItem

背景

感謝微信【Avalonia開發交流群】的 @kankankan 大佬提供的程式碼範例:

下圖是按個人化要求修改效果:

為了相容Semi.Avalonia主題風格,我們的TabControl控制項主題從參考Semi的Card風格控制項主題開始,Semi的效果如下:

我們修改後,在各主題切換時,展示效果如下:

使用

建議複製本文控制項程式碼自行維護,本控制項不一定更新及時。

本控制項是在Semi的基礎上二次開發,所以需要安裝以下NuGet套件:

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>

使用參考,效果在前面已經展示,程式碼如下:

<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="數據管理" />
            <TabItem Header="系統設定" />
            <TabItem Header="用戶中心" />
            <TabItem Header="日誌記錄" />
            <TabItem Header="幫助文檔" />
        </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="數據管理" />
            <TabItem Header="系統設定" />
            <TabItem Header="用戶中心" />
            <TabItem Header="日誌記錄" />
            <TabItem Header="幫助文檔" />
        </TabControl>
    </Grid>
</UserControl>

實作

講程式碼總是枯燥的,講個大概吧。

這是Semi的TabControl的ControlTheme程式碼:

我們主要是修改TabItem的邊框風格,所以將Semi的這段程式碼直接複製貼上,給ControlTheme換個Key,再把圖中框選部分ItemContainerTheme的Value指向另一個TabItem的控制項主題,其他部分程式碼按需修改,我這沒動其他程式碼,下圖是修改後程式碼截圖:

TabItem的控制項主題程式碼如下,關鍵程式碼是自定義的邊框程式碼位置:

其中TrapezoidShapedTabItemBorder繼承自Control,主要是覆寫它的Render方法:

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;

        // 取得控制項的尺寸
        var rect = new Rect(Bounds.Size);
        var borderThickness = BorderThickness;
        // 偏移路徑使線條對齊像素網格
        var halfBorder = borderThickness / 2.0;
        var adjustedRect = rect.Deflate(halfBorder);

        // 設定邊框路徑
        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);
                }
            }

            // 底邊消失(不繪製)
            // 這裡直接跳過底邊路徑,確保底邊消失
            ctx.EndFigure(isClosed: true);
        }

        // 繪製邊框
        context.DrawGeometry(Background, new Pen(BorderBrush, BorderThickness)
        {
            Thickness = BorderThickness,
            LineJoin = PenLineJoin.Round, // 圓角連接
            LineCap = PenLineCap.Round // 圓角端點
        }, pathGeometry);
    }
}

Render中,根據目前TabItem是TabControl的第一個、最後一個、中間部分,呼叫不同的方法繪製邊框,比如繪製第一個TabItem,效果圖如下:

先分析:

  1. 這是一個直角梯形
  2. 左邊線是豎直直線
  3. 左上角是一個1/4內圓
  4. 右上角又是一個內圓(可按比例繪製)
  5. 右邊線是帶斜率的斜線
  6. 左下角和右下角可帶外圓弧

邊框繪製程式碼如下:

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

    // 左下角開始
    ctx.BeginFigure(new Point(x, y), isFilled: true);

    // 左下角外圓
    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);
    }

    // 左邊直線
    y = adjustedRect.Top + radius.TopLeft;
    ctx.LineTo(new Point(x, y));

    // 左上角內圓角
    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);
    }

    // 上邊直線
    x = adjustedRect.Right - radius.TopRight * 2 - radius.BottomRight * 2;
    ctx.LineTo(new Point(x, y));

    // 右上角內圓角
    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);
    }

    // 右邊斜線
    x = adjustedRect.Right - radius.BottomRight;
    y = adjustedRect.Bottom - radius.BottomRight;
    ctx.LineTo(new Point(x, y));

    // 右下角外圓
    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);
    }
}

透過StreamGeometryContext呼叫LineTo方法繪製直線,呼叫ArcTo方法繪製圓弧(內圓、外圓),各種風格的邊框都能畫了,最後一個TabItem、中間的TabItem繪製方法類似了。

Edge的TabItem效果好實現了吧?

總結

本文只說個大概,大家可以看具體的實作程式碼,舉一反三,其他控制項效果可以使用類似的方式實現。

倉庫:

  • CodeWF.AvaloniaControls:https://github.com/dotnet9/CodeWF.AvaloniaControls

  • CodeF.ToolBox:https://github.com/dotnet9/CodeWF.Toolbox

繼續探索

延伸閱讀

更多文章