實現Avalonia平臺下低配版的Dock控制元件:實現TabControl的可關閉

wzwyc發表於2024-05-29

在弄一個專案,在WPF下用Dock控制元件,在Avalonia平臺下實現也有一個Dock控制元件,但用起來有點複雜。

Install-Package Dock.Avalonia
Install-Package Dock.Model.Mvvm

其實本身用的比較簡單,所以就想著,用TabControl來改一下。實現最基本的功能。
需要對TabControl和TabItem進行改造

TabControlEx

using Avalonia;
using Avalonia.Controls;

namespace CommandTerminal.Controls;

public class TabControlEx : TabControl
{
    private ICommand _closeItemCommand;

    public ICommand CloseItemCommand
    {
        get => _closeItemCommand;
        private set => SetAndRaise(CloseItemCommandProperty, ref _closeItemCommand, value);
    }

    public static readonly DirectProperty<TabControlEx, ICommand> CloseItemCommandProperty =
    AvaloniaProperty.RegisterDirect<TabControlEx, ICommand>(
        nameof(CloseItemCommand),
        o => o.CloseItemCommand,
        (o, v) => o.CloseItemCommand = v);

    public TabControlEx()
    {
        _closeItemCommand = new SimpleParamActionCommand(CloseItem);
    }

    private void CloseItem(object? tabItemSource)
    {
        ArgumentNullException.ThrowIfNull(tabItemSource);

        if (tabItemSource is not TabItemEx tabItem)
            return;

        RemoveItem(tabItem);
    }

    private void RemoveItem(TabItemEx container)
    {
        if (container == null || Items == null)
        {
            Debug.Assert(false);
            return;
        }

        Items.Remove(container);
    }

    private void SetSelectedNewTab(IList items, int removedItemIndex) =>
    SelectedItem = removedItemIndex == items.Count ? items[^1] : items[removedItemIndex];
}

public class SimpleParamActionCommand(Action<object?> action) : ICommand
{
    public bool CanExecute(object? parameter) => true;

    public void Execute(object? parameter) => action.Invoke(parameter);

    public event EventHandler? CanExecuteChanged;
}

TabItemEx

using Avalonia;
using Avalonia.Controls;

namespace CommandTerminal.Controls;

public class TabItemEx : TabItem
{
    public string ContentId { get; set; }

    public bool CanClose
    {
        get => GetValue(CanCloseProperty);
        set => SetValue(CanCloseProperty, value);
    }

    public static readonly StyledProperty<bool> CanCloseProperty =
        AvaloniaProperty.Register<TabItemEx, bool>(nameof(CanClose), defaultValue: true);
}

載入控制元件樣式

在App.axaml檔案裡新增:

<Application.Resources>
    <Thickness x:Key="TabControlTopPlacementItemMargin">0 0 0 2</Thickness>

    <x:Double x:Key="TabItemMinHeight">48</x:Double>
    <x:Double x:Key="TabItemVerticalPipeHeight">24</x:Double>
    <x:Double x:Key="TabItemPipeThickness">2</x:Double>

    <ControlTheme x:Key="CloseItemCommandButton" TargetType="Button">
        <Setter Property="Content">
            <Template>
                <Viewbox
                    Width="16"
                    Height="16"
                    Stretch="Uniform">
                    <Canvas Width="24" Height="24">
                        <Path Data="M13.46,12L19,17.54V19H17.54L12,13.46L6.46,19H5V17.54L10.54,12L5,6.46V5H6.46L12,10.54L17.54,5H19V6.46L13.46,12Z" Fill="{Binding $parent[Button].Foreground}" />
                    </Canvas>
                </Viewbox>
            </Template>
        </Setter>
        <!--<Setter Property="Background" Value="{DynamicResource ButtonBackground}" />-->
        <Setter Property="Background" Value="Transparent" />
        <Setter Property="Foreground" Value="{DynamicResource ButtonForeground}" />
        <Setter Property="BorderBrush" Value="{DynamicResource ButtonBorderBrush}" />
        <Setter Property="BorderThickness" Value="{DynamicResource ButtonBorderThemeThickness}" />
        <Setter Property="CornerRadius" Value="{DynamicResource ControlCornerRadius}" />
        <Setter Property="Padding" Value="2" />
        <Setter Property="HorizontalAlignment" Value="Left" />
        <Setter Property="VerticalAlignment" Value="Center" />
        <Setter Property="RenderTransform" Value="none" />
        <Setter Property="Transitions">
            <Transitions>
                <TransformOperationsTransition Property="RenderTransform" Duration="0:0:.075" />
            </Transitions>
        </Setter>

        <Setter Property="Template">
            <ControlTemplate>
                <ContentPresenter
                    x:Name="PART_ContentPresenter"
                    Padding="{TemplateBinding Padding}"
                    HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}"
                    VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}"
                    Background="{TemplateBinding Background}"
                    BorderBrush="{TemplateBinding BorderBrush}"
                    BorderThickness="{TemplateBinding BorderThickness}"
                    Content="{TemplateBinding Content}"
                    ContentTemplate="{TemplateBinding ContentTemplate}"
                    CornerRadius="{TemplateBinding CornerRadius}"
                    Foreground="{TemplateBinding Foreground}"
                    RecognizesAccessKey="True" />
            </ControlTemplate>
        </Setter>

        <Style Selector="^:pointerover /template/ ContentPresenter#PART_ContentPresenter">
            <Setter Property="Background" Value="{DynamicResource ButtonBackgroundPointerOver}" />
            <Setter Property="BorderBrush" Value="{DynamicResource ButtonBorderBrushPointerOver}" />
            <Setter Property="Foreground" Value="{DynamicResource ButtonForegroundPointerOver}" />
        </Style>

        <Style Selector="^:pressed">
            <Setter Property="RenderTransform" Value="scale(0.98)" />
        </Style>

        <Style Selector="^:pressed  /template/ ContentPresenter#PART_ContentPresenter">
            <Setter Property="Background" Value="{DynamicResource ButtonBackgroundPressed}" />
            <Setter Property="BorderBrush" Value="{DynamicResource ButtonBorderBrushPressed}" />
            <Setter Property="Foreground" Value="{DynamicResource ButtonForegroundPressed}" />
        </Style>

        <Style Selector="^:disabled /template/ ContentPresenter#PART_ContentPresenter">
            <Setter Property="Background" Value="{DynamicResource ButtonBackgroundDisabled}" />
            <Setter Property="BorderBrush" Value="{DynamicResource ButtonBorderBrushDisabled}" />
            <Setter Property="Foreground" Value="{DynamicResource ButtonForegroundDisabled}" />
        </Style>

        <Style Selector="^.accent">
            <Style Selector="^ /template/ ContentPresenter#PART_ContentPresenter">
                <Setter Property="Background" Value="{DynamicResource AccentButtonBackground}" />
                <Setter Property="BorderBrush" Value="{DynamicResource AccentButtonBorderBrush}" />
                <Setter Property="Foreground" Value="{DynamicResource AccentButtonForeground}" />
            </Style>

            <Style Selector="^:pointerover /template/ ContentPresenter#PART_ContentPresenter">
                <Setter Property="Background" Value="{DynamicResource AccentButtonBackgroundPointerOver}" />
                <Setter Property="BorderBrush" Value="{DynamicResource AccentButtonBorderBrushPointerOver}" />
                <Setter Property="Foreground" Value="{DynamicResource AccentButtonForegroundPointerOver}" />
            </Style>

            <Style Selector="^:pressed  /template/ ContentPresenter#PART_ContentPresenter">
                <Setter Property="Background" Value="{DynamicResource AccentButtonBackgroundPressed}" />
                <Setter Property="BorderBrush" Value="{DynamicResource AccentButtonBorderBrushPressed}" />
                <Setter Property="Foreground" Value="{DynamicResource AccentButtonForegroundPressed}" />
            </Style>

            <Style Selector="^:disabled /template/ ContentPresenter#PART_ContentPresenter">
                <Setter Property="Background" Value="{DynamicResource AccentButtonBackgroundDisabled}" />
                <Setter Property="BorderBrush" Value="{DynamicResource AccentButtonBorderBrushDisabled}" />
                <Setter Property="Foreground" Value="{DynamicResource AccentButtonForegroundDisabled}" />
            </Style>
        </Style>
    </ControlTheme>

    <ControlTheme x:Key="{x:Type ctl:TabControlEx}" TargetType="ctl:TabControlEx">
        <Setter Property="Margin" Value="0" />
        <Setter Property="Padding" Value="{DynamicResource TabItemMargin}" />
        <Setter Property="Background" Value="{DynamicResource TabControlBackground}" />
        <Setter Property="Template">
            <ControlTemplate>
                <Border
                    HorizontalAlignment="{TemplateBinding HorizontalAlignment}"
                    VerticalAlignment="{TemplateBinding VerticalAlignment}"
                    Background="{TemplateBinding Background}"
                    BorderBrush="{TemplateBinding BorderBrush}"
                    BorderThickness="{TemplateBinding BorderThickness}"
                    CornerRadius="{TemplateBinding CornerRadius}">
                    <DockPanel>
                        <ItemsPresenter
                            Name="PART_ItemsPresenter"
                            DockPanel.Dock="{TemplateBinding TabStripPlacement}"
                            ItemsPanel="{TemplateBinding ItemsPanel}" />
                        <ContentPresenter
                            Name="PART_SelectedContentHost"
                            Margin="{TemplateBinding Padding}"
                            HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}"
                            VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}"
                            Content="{TemplateBinding SelectedContent}"
                            ContentTemplate="{TemplateBinding SelectedContentTemplate}" />
                    </DockPanel>
                </Border>
            </ControlTemplate>
        </Setter>

        <Style Selector="^[TabStripPlacement=Left] /template/ ItemsPresenter#PART_ItemsPresenter > WrapPanel">
            <Setter Property="Orientation" Value="Vertical" />
        </Style>
        <Style Selector="^[TabStripPlacement=Right] /template/ ItemsPresenter#PART_ItemsPresenter > WrapPanel">
            <Setter Property="Orientation" Value="Vertical" />
        </Style>
        <Style Selector="^[TabStripPlacement=Top] /template/ ItemsPresenter#PART_ItemsPresenter">
            <Setter Property="Margin" Value="{DynamicResource TabControlTopPlacementItemMargin}" />
        </Style>
    </ControlTheme>

    <ControlTheme x:Key="{x:Type ctl:TabItemEx}" TargetType="ctl:TabItemEx">
        <Setter Property="FontSize" Value="{DynamicResource TabItemHeaderFontSize}" />
        <Setter Property="FontWeight" Value="{DynamicResource TabItemHeaderThemeFontWeight}" />
        <Setter Property="Background" Value="{DynamicResource TabItemHeaderBackgroundUnselected}" />
        <Setter Property="Foreground" Value="{DynamicResource TabItemHeaderForegroundUnselected}" />
        <Setter Property="Padding" Value="{DynamicResource TabItemHeaderMargin}" />
        <Setter Property="Margin" Value="0" />
        <Setter Property="MinHeight" Value="{DynamicResource TabItemMinHeight}" />
        <Setter Property="VerticalContentAlignment" Value="Center" />
        <Setter Property="Template">
            <ControlTemplate>
                <Border
                    Name="PART_LayoutRoot"
                    Padding="{TemplateBinding Padding}"
                    Background="{TemplateBinding Background}"
                    BorderBrush="{TemplateBinding BorderBrush}"
                    BorderThickness="{TemplateBinding BorderThickness}"
                    CornerRadius="{TemplateBinding CornerRadius}">
                    <Panel>
                        <Grid ColumnDefinitions="* Auto">
                            <ContentPresenter
                                Name="PART_ContentPresenter"
                                Grid.Column="0"
                                HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
                                VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
                                Content="{TemplateBinding Header}"
                                ContentTemplate="{TemplateBinding HeaderTemplate}"
                                RecognizesAccessKey="True" />
                            <Button
                                Grid.Column="1"
                                Margin="2,0,0,0"
                                Command="{Binding $parent[ctl:TabControlEx].CloseItemCommand}"
                                CommandParameter="{Binding RelativeSource={RelativeSource TemplatedParent}}"
                                IsVisible="{TemplateBinding CanClose}"
                                Theme="{StaticResource CloseItemCommandButton}" />
                        </Grid>
                        <Border
                            Name="PART_SelectedPipe"
                            Background="{DynamicResource TabItemHeaderSelectedPipeFill}"
                            CornerRadius="{DynamicResource ControlCornerRadius}"
                            IsVisible="False" />
                    </Panel>
                </Border>
            </ControlTemplate>
        </Setter>

        <!--  Selected state  -->
        <!--  We don't use selector to PART_LayoutRoot, so developer can override selected item background with TabStripItem.Background  -->
        <Style Selector="^:selected">
            <Setter Property="Background" Value="{DynamicResource TabItemHeaderBackgroundSelected}" />
            <Setter Property="Foreground" Value="{DynamicResource TabItemHeaderForegroundSelected}" />
        </Style>
        <Style Selector="^:selected /template/ Border#PART_SelectedPipe">
            <Setter Property="IsVisible" Value="True" />
        </Style>

        <!--  PointerOver state  -->
        <Style Selector="^:pointerover /template/ Border#PART_LayoutRoot">
            <Setter Property="Background" Value="{DynamicResource TabItemHeaderBackgroundUnselectedPointerOver}" />
            <Setter Property="TextElement.Foreground" Value="{DynamicResource TabItemHeaderForegroundUnselectedPointerOver}" />
        </Style>

        <!--  Selected PointerOver state  -->
        <Style Selector="^:selected:pointerover /template/ Border#PART_LayoutRoot">
            <Setter Property="Background" Value="{DynamicResource TabItemHeaderBackgroundSelectedPointerOver}" />
            <Setter Property="TextElement.Foreground" Value="{DynamicResource TabItemHeaderForegroundSelectedPointerOver}" />
        </Style>

        <!--  Pressed state  -->
        <Style Selector="^:pressed /template/ Border#PART_LayoutRoot">
            <Setter Property="Background" Value="{DynamicResource TabItemHeaderBackgroundUnselectedPressed}" />
            <Setter Property="TextElement.Foreground" Value="{DynamicResource TabItemHeaderForegroundUnselectedPressed}" />
        </Style>

        <!--  Selected Pressed state  -->
        <Style Selector="^:selected:pressed /template/ Border#PART_LayoutRoot">
            <Setter Property="Background" Value="{DynamicResource TabItemHeaderBackgroundSelectedPressed}" />
            <Setter Property="TextElement.Foreground" Value="{DynamicResource TabItemHeaderForegroundSelectedPressed}" />
        </Style>

        <!--  Disabled state  -->
        <Style Selector="^:disabled /template/ Border#PART_LayoutRoot">
            <Setter Property="Background" Value="{DynamicResource TabItemHeaderBackgroundDisabled}" />
            <Setter Property="TextElement.Foreground" Value="{DynamicResource TabItemHeaderForegroundDisabled}" />
        </Style>

        <!--  TabStripPlacement States Group  -->
        <Style Selector="^[TabStripPlacement=Left] /template/ Border#PART_SelectedPipe">
            <Setter Property="Width" Value="{DynamicResource TabItemPipeThickness}" />
            <Setter Property="Height" Value="{DynamicResource TabItemVerticalPipeHeight}" />
            <Setter Property="Margin" Value="0,0,2,0" />
            <Setter Property="HorizontalAlignment" Value="Left" />
            <Setter Property="VerticalAlignment" Value="Center" />
        </Style>
        <Style Selector="^[TabStripPlacement=Left] /template/ ContentPresenter#PART_ContentPresenter">
            <Setter Property="Margin" Value="8,0,0,0" />
        </Style>

        <Style Selector="^[TabStripPlacement=Top] /template/ Border#PART_SelectedPipe, ^[TabStripPlacement=Bottom] /template/ Border#PART_SelectedPipe">
            <Setter Property="Height" Value="{DynamicResource TabItemPipeThickness}" />
            <Setter Property="Margin" Value="0,0,0,2" />
            <Setter Property="HorizontalAlignment" Value="Stretch" />
            <Setter Property="VerticalAlignment" Value="Bottom" />
        </Style>

        <Style Selector="^[TabStripPlacement=Right] /template/ Border#PART_SelectedPipe">
            <Setter Property="Width" Value="{DynamicResource TabItemPipeThickness}" />
            <Setter Property="Height" Value="{DynamicResource TabItemVerticalPipeHeight}" />
            <Setter Property="Margin" Value="2,0,0,0" />
            <Setter Property="HorizontalAlignment" Value="Right" />
            <Setter Property="VerticalAlignment" Value="Center" />
        </Style>
        <Style Selector="^[TabStripPlacement=Right] /template/ ContentPresenter#PART_ContentPresenter">
            <Setter Property="Margin" Value="0,0,8,0" />
        </Style>
        <Style Selector="^[TabStripPlacement=Right]">
            <Setter Property="HorizontalContentAlignment" Value="Right" />
        </Style>
    </ControlTheme>
</Application.Resources>

前臺使用

新增引用:

xmlns:ctl="clr-namespace:CommandTerminal.Controls"

程式碼:

<ctl:TabControlEx
    x:Name="TabList"
    Grid.Row="1"
    Grid.Column="1">
    <ctl:TabItemEx CanClose="False" Header="遙測監視">
        <v:UcTelemetryMonitoring />
    </ctl:TabItemEx>
    <ctl:TabItemEx Header="發令記錄">
        <v:UcCommandSendList />
    </ctl:TabItemEx>
    <ctl:TabItemEx Header="發令查詢">
        <v:UcCommandHistory />
    </ctl:TabItemEx>
</ctl:TabControlEx>

後臺使用:

public void ShowLayoutDocument(string contendId, string title, Func<UserControl> func, bool canClose = true)
{
    var items = _tabList.Items.Cast<TabItemEx>().ToArray();
    var info = items.FirstOrDefault(s => s.ContentId == contendId);
    if (info != null)
    {
        _tabList.SelectedItem = info;
        return;
    }

    var item = new TabItemEx();
    item.ContentId = contendId;
    item.Header = title;
    item.CanClose = canClose;
    item.Content = func.Invoke();

    _tabList.Items.Add(item);
    _tabList.SelectedItem = item;
}

相關文章