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();
}
}
运行效果

浙公网安备 33010602011771号