背景
WeChatの【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/4の内側円
- 右上角も内側円(比率に応じて描画可能)
- 右辺は傾きのある斜線
- 左下角と右下角には外側の円弧を付けることが可能
枠線描画コード:
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