C# avalonia没有内置Mdi,继承Canvas实现一个。增加了选中虚线框功能。Fluent主题中,OnApplyTemplate获取的副本closeButton,无法像Simple主题那样,直接修改属性,因为Fluent主题样式会覆盖。所以,我们可以通过css或者代码产生css样式来控制closeButton。

Mdi类

    public class Mdi : Canvas
    {
        public static readonly StyledProperty<MdiChild> SelectedItemProperty =
          AvaloniaProperty.Register<Mdi, MdiChild>(nameof(SelectedItem));
        public MdiChild SelectedItem
        {
            get => GetValue(SelectedItemProperty);
            set => SetValue(SelectedItemProperty, value);
        }
        private MdiChild? draggingChild;
        private Point dragStartPointer;
        private Point dragStartPosition;
        private bool resizing;
        private Size dragStartSize;
        private ResizeDirection resizeDirection;
        private const double grip = 15;

        private enum ResizeDirection
        {
            None,
            TopLeft,
            TopRight,
            BottomLeft,
            BottomRight
        }
        public Mdi()
        {
            Background = Brushes.Transparent;
            this.PointerPressed += OnPointerPressed;
            this.PointerReleased += OnPointerReleased;
            this.PointerMoved += OnPointerMoved;
            this.Children.CollectionChanged += (s, e) =>
            {
                NormalizeZIndex();
                /*
                switch (e.Action)
                {
                    case NotifyCollectionChangedAction.Add:
                        BringToFront((MdiChild)e.NewItems?[0]!);
                        break;
                }
                */
            };
        }
        private void OnPointerPressed(object? sender, PointerPressedEventArgs e)
        {
            var point = e.GetPosition(this);

            // 找到点击的子窗口(ZIndex最高的优先)
            var child = Children
                .OfType<MdiChild>()
                .OrderByDescending(c => c.GetZIndex())
                .FirstOrDefault(c =>
                {
                    resizeDirection = GetResizeDirection(point, c);
                    if (resizeDirection != ResizeDirection.None)
                        return true;
                    var rect = new Rect(c.X, c.Y, c.Bounds.Width, c.Bounds.Height);
                    return rect.Contains(point);
                });

          
            if (child != null)
            {
                BringToFront(child);
                // 判断是否在四个角
                if (resizeDirection != ResizeDirection.None)
                {
                    resizing = true;
                    draggingChild = child;
                    dragStartPointer = point;
                    dragStartPosition = new Point(child.X, child.Y);
                    dragStartSize = child.Bounds.Size;
                    e.Pointer.Capture(this);
                    this.Cursor = new Cursor(StandardCursorType.Cross);
                    return;
                }

                // 检测是否点击在 TitleBar
                var titleBar = child.GetTemplateChildren().FirstOrDefault(x => x.Name == "PART_TitleBar");
                if (titleBar != null)
                {
                    var relative = e.GetPosition(titleBar);
                    if (relative.Y >= 0 && relative.Y <= titleBar.Bounds.Height)
                    {
                        draggingChild = child;
                        dragStartPointer = point;
                        dragStartPosition = new Point(child.X, child.Y);
                        e.Pointer.Capture(this);
                        this.Cursor = new Cursor(StandardCursorType.Hand); // 拖拽时手型
                    }
                }
            }
            else
            {
                this.Focusable = true;
                if (SelectedItem != null) 
                    SelectedItem.BorderBrush = Brushes.Gray;
            }
        }
        private void OnPointerMoved(object? sender, PointerEventArgs e)
        {
            if (draggingChild == null || !e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
                return;

            var point = e.GetPosition(this);
            var offset = point - dragStartPointer;

            if (resizing)
            {
                double minWidth = 100;
                double minHeight = 60;

                double newX = dragStartPosition.X;
                double newY = dragStartPosition.Y;
                double newWidth = dragStartSize.Width;
                double newHeight = dragStartSize.Height;

                switch (resizeDirection)
                {
                    case ResizeDirection.TopLeft:
                        newX += offset.X;
                        newY += offset.Y;
                        newWidth -= offset.X;
                        newHeight -= offset.Y;

                        // 修正宽度
                        if (newWidth < minWidth)
                        {
                            newX -= (minWidth - newWidth);
                            newWidth = minWidth;
                        }
                        // 修正高度
                        if (newHeight < minHeight)
                        {
                            newY -= (minHeight - newHeight);
                            newHeight = minHeight;
                        }
                        break;

                    case ResizeDirection.TopRight:
                        newY += offset.Y;
                        newWidth += offset.X;
                        newHeight -= offset.Y;

                        // 修正宽度
                        if (newWidth < minWidth)
                        {
                            newWidth = minWidth;
                        }
                        // 修正高度
                        if (newHeight < minHeight)
                        {
                            newY -= (minHeight - newHeight);
                            newHeight = minHeight;
                        }
                        break;

                    case ResizeDirection.BottomLeft:
                        newX += offset.X;
                        newWidth -= offset.X;
                        newHeight += offset.Y;

                        // 修正宽度
                        if (newWidth < minWidth)
                        {
                            newX -= (minWidth - newWidth);
                            newWidth = minWidth;
                        }
                        // 高度只增大,不需要修正
                        if (newHeight < minHeight)
                        {
                            newHeight = minHeight;
                        }
                        break;

                    case ResizeDirection.BottomRight:
                        newWidth += offset.X;
                        newHeight += offset.Y;

                        // 宽高限制(不影响 X/Y)
                        if (newWidth < minWidth)
                            newWidth = minWidth;
                        if (newHeight < minHeight)
                            newHeight = minHeight;
                        break;
                }

                draggingChild.X = newX;
                draggingChild.Y = newY;
                draggingChild.Width = newWidth;
                draggingChild.Height = newHeight;
            }
            else
            {
                // 当前子窗口尺寸(用显式属性,防止 Bounds 计算偏差)
                double childWidth = draggingChild.Width;
                double childHeight = draggingChild.Height;

                double newX = dragStartPosition.X + offset.X;
                double newY = dragStartPosition.Y + offset.Y;

                // 限制左上边界
                if (newX < 0) newX = 0;
                if (newY < 0) newY = 0;

                // 限制右下边界
                double maxX = this.Bounds.Width - childWidth;
                double maxY = this.Bounds.Height - childHeight;

                if (newX > maxX) newX = maxX;
                if (newY > maxY) newY = maxY;

                draggingChild.X = newX;
                draggingChild.Y = newY;
            }
        }
        private void OnPointerReleased(object? sender, PointerReleasedEventArgs e)
        {
            if (draggingChild != null)
            {
                draggingChild = null;
                resizing = false;
                resizeDirection = ResizeDirection.None;
                this.Cursor = Cursor.Default; // 释放时还原
                e.Pointer.Capture(null);
            }
        }
        private ResizeDirection GetResizeDirection(Point globalPoint, MdiChild child)
        {
            double x = child.X;
            double y = child.Y;
            double w = child.Bounds.Width;
            double h = child.Bounds.Height;

            // 四个角的矩形区域
            Rect hotTopLeft = new Rect(x - grip, y - grip, grip * 2, grip * 2);
            Rect hotTopRight = new Rect(x + w - grip, y - grip, grip * 2, grip * 2);
            Rect hotBottomLeft = new Rect(x - grip, y + h - grip, grip * 2, grip * 2);
            Rect hotBottomRight = new Rect(x + w - grip, y + h - grip, grip * 2, grip * 2);

            if (hotTopLeft.Contains(globalPoint)) return ResizeDirection.TopLeft;
            if (hotTopRight.Contains(globalPoint)) return ResizeDirection.TopRight;
            if (hotBottomLeft.Contains(globalPoint)) return ResizeDirection.BottomLeft;
            if (hotBottomRight.Contains(globalPoint)) return ResizeDirection.BottomRight;

            return ResizeDirection.None;
        }

        public void Add(MdiChild child)
        {
            // 1) 记录当前最上面的窗口,用它的 X/Y 做错位
            var prevTop = Children.OfType<MdiChild>()
                                  .OrderByDescending(c => c.GetZIndex())
                                  .FirstOrDefault();     

            // 2) 给新窗口定好位置
            child.X = prevTop != null ? prevTop.X + grip : 0;
            child.Y = prevTop != null ? prevTop.Y + grip : 0;

            // 3) 加入集合
            Children.Add(child);

            // 4) 把它推到最上层但不制造空洞
            child.SetZIndex(int.MaxValue);

            NormalizeZIndex();

            // 5) 未命名则用“最终”的 ZIndex 命名(此时是连续 0..N-1)
            if (string.IsNullOrWhiteSpace(child.Header?.ToString()))
                child.Header = $"未命名{child.GetZIndex()}";
        }

        public void Remove(MdiChild child)
        {
            Children.Remove(child);
        }

        public void BringToFront(MdiChild child)
        {
            child.SetZIndex(int.MaxValue);
            child.Focusable = true;
            if (SelectedItem != null)
            {
                SelectedItem.BorderBrush = Brushes.Gray;
            }
            SelectedItem = child;
            SelectedItem.BorderBrush = Brushes.Transparent;
            SelectedItem.StrokeBrush = Brushes.Gray;
            NormalizeZIndex();
        }

        // 压缩所有 ZIndex,避免无限增长
        private void NormalizeZIndex()
        {
            var sorted = Children.OfType<Control>().OrderBy(c => c.GetZIndex()).ToList();
            for (int i = 0; i < sorted.Count; i++)
            {
                sorted[i].SetZIndex(i);
            }
        }
    }

MdiChild类

    public class MdiChild : HeaderedContentControl
    {
        public event EventHandler? Closed;
        private const string PART_CloseButton = "PART_CloseButton";
        private const string PART_TitleBar = "PART_TitleBar";

        public static readonly StyledProperty<double> XProperty =
          AvaloniaProperty.Register<MdiChild, double>(nameof(X));

        public double X
        {
            get => GetValue(XProperty);
            set => SetValue(XProperty, value);
        }

        public static readonly StyledProperty<double> YProperty =
            AvaloniaProperty.Register<MdiChild, double>(nameof(Y));

        public double Y
        {
            get => GetValue(YProperty);
            set => SetValue(YProperty, value);
        }

        public static readonly StyledProperty<double[]> DashPatternProperty =
            AvaloniaProperty.Register<MdiChild, double[]>(nameof(DashPattern), new[] { 2.0, 2.0 });

        public double[] DashPattern
        {
            get => GetValue(DashPatternProperty);
            set => SetValue(DashPatternProperty, value);
        }

        public static readonly StyledProperty<IBrush> StrokeBrushProperty =
            AvaloniaProperty.Register<MdiChild, IBrush>(nameof(StrokeBrush));

        public IBrush StrokeBrush
        {
            get => GetValue(StrokeBrushProperty);
            set => SetValue(StrokeBrushProperty, value);
        }
        static MdiChild()
        {
            TemplateProperty.OverrideDefaultValue<MdiChild>(CreateControlTemplate());
        }
        protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
        {
            base.OnApplyTemplate(e);
            //初始化绑定
            this[!!Canvas.LeftProperty] = this[!!XProperty];
            this[!!Canvas.TopProperty] = this[!!YProperty];
            AddHandler(PointerPressedEvent, (sender, args) =>
            {
                if (this.Parent is Mdi mdi)
                {
                    mdi.BringToFront(this);
                }
            }, RoutingStrategies.Tunnel | RoutingStrategies.Bubble, handledEventsToo: true); // 即使事件被TextBox标记为Handled也能收到
            //初始化样式
            this.BorderBrush = Brushes.Gray;
            this.BorderThickness = new Thickness(1);
            this.Background = Brushes.White;
            this.CornerRadius = new CornerRadius(4);

            // 获取关闭按钮
            var closeButton = e.NameScope.Find<Button>(PART_CloseButton);

            if (closeButton != null)
            {
                closeButton.Click += Close;

                closeButton.Styles.Add(new Style
                {
                    Selector = Selectors.Class(null, ":pointerover"),
                    Setters =        
                    {
                        new Setter(Button.BackgroundProperty, Brushes.Red),
                        new Setter(Button.ForegroundProperty, Brushes.White)      
                    }
                });

                closeButton.Styles.Add(new Style(s => s.Class(":normal"))
                {
                    Setters = 
                    {      
                        new Setter(Button.BackgroundProperty, Brushes.WhiteSmoke),   
                        new Setter(Button.ForegroundProperty, Brushes.Black)
                    }
                });
                
            }
        }

        private static IControlTemplate CreateControlTemplate()
        {
            return new FuncControlTemplate<MdiChild>((control, scope) =>
            {
                var closeButton = new Button
                {
                    Name = PART_CloseButton, // 命名部件
                    Content = "\ue603",
                    FontFamily = (FontFamily)AvaloniaExtensions.GetResource("IconFont")!,
                    Background = Brushes.Transparent,
                    Width = 30,
                    Height = 24,
                    BorderThickness = new Thickness(0),
                    HorizontalAlignment = HorizontalAlignment.Right,
                    VerticalAlignment = VerticalAlignment.Center,
                    Margin = new Thickness(0, 3, 3, 3),
                };

                var titleBar = new DockPanel
                {
                    Name = PART_TitleBar,
                    Background = Brushes.WhiteSmoke,
                    Height = 30,
                    Children =
                {
                    new TextBlock
                    {
                        [!TextBlock.TextProperty] = control[!HeaderedContentControl.HeaderProperty],
                        VerticalAlignment = VerticalAlignment.Center,
                        Margin = new Thickness(8,0)
                    },
                    closeButton
                }
                };
                DockPanel.SetDock(titleBar, Dock.Top);
                
                scope.Register(PART_CloseButton, closeButton);
                scope.Register(PART_TitleBar, titleBar);

                var root = new Grid
                {
                    Children =
                    {
                        // 虚线边框层
                        new Rectangle
                        {
                            IsHitTestVisible = false,
                            [!Rectangle.StrokeProperty] = control[!MdiChild.StrokeBrushProperty],
                            [!Rectangle.FillProperty] = control[!HeaderedContentControl.BackgroundProperty],
                            [!Rectangle.StrokeThicknessProperty]= control.GetObservable(HeaderedContentControl.BorderThicknessProperty)
                            .Select(thickness => thickness.Left) 
                            .ToBinding(),
                            [!Rectangle.StrokeDashArrayProperty] = control.GetObservable(MdiChild.DashPatternProperty)
                            .Select(dashArray => new AvaloniaList<double>(dashArray))
                            .ToBinding(),
                            [!Rectangle.RadiusXProperty] = control.GetObservable(HeaderedContentControl.CornerRadiusProperty)
                            .Select(cr => cr.TopLeft)
                            .ToBinding(),
                            [!Rectangle.RadiusYProperty] = control.GetObservable(HeaderedContentControl.CornerRadiusProperty)
                            .Select(cr => cr.TopRight)
                            .ToBinding(),
                        },
                        new Border
                        {
                            Child = new DockPanel
                            {
                                LastChildFill = true,
                                Children =
                                {
                                    titleBar,
                                    new ContentPresenter
                                    {
                                        [!ContentPresenter.ContentProperty] = control[!HeaderedContentControl.ContentProperty]
                                    }
                                }
                            },
                            [!Border.BorderBrushProperty] = control[!HeaderedContentControl.BorderBrushProperty],
                            [!Border.BorderThicknessProperty] = control[!HeaderedContentControl.BorderThicknessProperty],
                            [!Border.BackgroundProperty] = control[!HeaderedContentControl.BackgroundProperty],
                            [!Border.CornerRadiusProperty] = control[!HeaderedContentControl.CornerRadiusProperty]
                        }
                    }
                };
                return root;
            });
        }
        private void Close(object? sender, RoutedEventArgs e)
        {
            Closed?.Invoke(this, e);
            if (this.Parent is  Mdi mdi)
            {
                mdi.Remove(this);
            }
        }
    }

WindowTracker.axaml代码

<Window 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="AvaloniaUI.WindowTracker"
        Title="WindowTracker">
    <Mdi>
        <MdiChild X="30" Y="30" Width="200" Height="150" Header="子控件1">
            <TextBlock>Hello</TextBlock>
        </MdiChild>
        <MdiChild X="50" Y="60" Width="200" Height="150" Header="子控件2">
            <TextBlock>Hello 1</TextBlock>
        </MdiChild>
    </Mdi>
</Window>

WindowTracker.axaml.cs代码

using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;

namespace AvaloniaUI;

public partial class WindowTracker : Window
{
    public WindowTracker()
    {
        InitializeComponent();
    }
}

运行效果

image

 

posted on 2025-08-02 10:06  dalgleish  阅读(41)  评论(0)    收藏  举报