这次继承C# Avalonia官方自带的Canvas,扩展一个InkCanvas,兼容Canvas的所有功能。为了简化自定义命名控件,建议把自定义控件加入到默认空间。
AssemblyInfo.cs代码如下
using System.Runtime.CompilerServices;
using System.Resources;
using Avalonia.Metadata;
[assembly: NeutralResourcesLanguage("zh-CN")]
[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Shares.Avalonia")]
Canvas类有几点需要注意。
1. 自定义内容区域,是通过[Content]属性来描述Controls类。
[Content] public Controls Children { get; } = new Controls();
2. Render是sealed,所以不支持重写Render。
public sealed override void Render(DrawingContext context)
现在,我们在Shares.Avalonia共享项目中,创建一个ControlExtensions.cs,实现InkCanvas类。代码如下
using Avalonia; using Avalonia.Controls; using Avalonia.Input; using Avalonia.Media; using System; using System.Collections.Generic; using System.Linq; namespace Shares.Avalonia { public enum InkEditingMode { Ink, Erase, Select } public class InkStroke { public List<Point> Points { get; set; } = new(); public Color Color { get; set; } = Colors.Black; public double Thickness { get; set; } = 1.0; } public class InkCanvasLayer : Control { public List<InkStroke> Strokes { get; set; } = new(); public InkStroke? CurrentStroke { get; set; } public List<InkStroke> SelectedStrokes { get; set; } = new(); public Rect? SelectionRect { get; set; } public Rect? SelectionBox { get; set; } public override void Render(DrawingContext context) { base.Render(context); foreach (var stroke in Strokes) { var isSelected = SelectedStrokes.Contains(stroke); DrawStroke(context, stroke, isSelected); } if (CurrentStroke != null) DrawStroke(context, CurrentStroke); if (SelectionRect.HasValue) { context.DrawRectangle(null, new Pen(Brushes.DarkOliveGreen, 1, dashStyle: DashStyle.Dash), SelectionRect.Value); } if (SelectionBox.HasValue) { var pen = new Pen(Brushes.DarkGray, 1, dashStyle: DashStyle.Dash); context.DrawRectangle(null, pen, SelectionBox.Value); } } private void DrawStroke(DrawingContext context, InkStroke stroke, bool isSelected = false) { if (stroke.Points.Count < 2) return; var color = isSelected ? Colors.Black : stroke.Color; var thickness = isSelected ? stroke.Thickness * 2 : stroke.Thickness; var pen = new Pen(new SolidColorBrush(color), thickness); for (int i = 1; i < stroke.Points.Count; i++) { context.DrawLine(pen, stroke.Points[i - 1], stroke.Points[i]); } } } public class InkCanvas : Canvas { private readonly InkCanvasLayer layer; private List<InkStroke> strokes = new(); private InkStroke? currentStroke; private Stack<List<InkStroke>> undoStack = new(); private Stack<List<InkStroke>> redoStack = new(); private bool isSelecting = false; private Rect selectionRect; private List<InkStroke> selectedStrokes = new(); private Point selectionStart; private bool isDraggingSelection = false; private Point lastDragPoint; public static readonly StyledProperty<Color> StrokeColorProperty = AvaloniaProperty.Register<InkCanvas, Color>(nameof(StrokeColor), Colors.Black); public Color StrokeColor { get => GetValue(StrokeColorProperty); set => SetValue(StrokeColorProperty, value); } public static readonly StyledProperty<double> StrokeThicknessProperty = AvaloniaProperty.Register<InkCanvas, double>(nameof(StrokeThickness), 2.0); public double StrokeThickness { get => GetValue(StrokeThicknessProperty); set => SetValue(StrokeThicknessProperty, value); } public static readonly StyledProperty<InkEditingMode> EditingModeProperty = AvaloniaProperty.Register<InkCanvas, InkEditingMode>(nameof(EditingMode), InkEditingMode.Ink); public InkEditingMode EditingMode { get => GetValue(EditingModeProperty); set => SetValue(EditingModeProperty, value); } public InkCanvas() { layer = new InkCanvasLayer(); Children.Add(layer); PointerPressed += OnPointerPressed; PointerMoved += OnPointerMoved; PointerReleased += OnPointerReleased; this.GetObservable(EditingModeProperty).Subscribe(mode => { selectedStrokes.Clear(); layer.SelectedStrokes = selectedStrokes; layer.SelectionBox = null; layer.InvalidateVisual(); }); Background = Brushes.White; } protected override Size ArrangeOverride(Size finalSize) { layer.Arrange(new Rect(finalSize)); return base.ArrangeOverride(finalSize); } private void OnPointerPressed(object? sender, PointerPressedEventArgs e) { var point = e.GetPosition(this); if (EditingMode == InkEditingMode.Erase) { EraseAtPoint(point); return; } if (EditingMode == InkEditingMode.Select) { selectionStart = point; selectionRect = new Rect(point, point); if (selectedStrokes.Any(s => s.Points.Any(p => Distance(p, point) < 5))) { isDraggingSelection = true; lastDragPoint = point; return; } isSelecting = true; return; } currentStroke = new InkStroke { Color = StrokeColor, Thickness = StrokeThickness }; currentStroke.Points.Add(point); layer.CurrentStroke = currentStroke; e.Pointer.Capture(this); } private void OnPointerMoved(object? sender, PointerEventArgs e) { var point = e.GetPosition(this); if (currentStroke != null && e.GetCurrentPoint(this).Properties.IsLeftButtonPressed) { currentStroke.Points.Add(point); layer.InvalidateVisual(); } if (isDraggingSelection && e.GetCurrentPoint(this).Properties.IsLeftButtonPressed) { var delta = point - lastDragPoint; MoveSelected(delta); lastDragPoint = point; UpdateSelectionBox(); } if (isSelecting && e.GetCurrentPoint(this).Properties.IsLeftButtonPressed) { selectionRect = new Rect(selectionStart, point).Normalize(); layer.SelectionRect = selectionRect; layer.InvalidateVisual(); } } private void OnPointerReleased(object? sender, PointerReleasedEventArgs e) { if (currentStroke != null) { SaveUndoState(); strokes.Add(currentStroke); currentStroke = null; layer.CurrentStroke = null; } if (isDraggingSelection) { isDraggingSelection = false; } if (isSelecting) { isSelecting = false; layer.SelectionRect = null; SelectStrokesInRect(selectionRect); } layer.InvalidateVisual(); e.Pointer.Capture(null); } private void SelectStrokesInRect(Rect rect) { selectedStrokes.Clear(); foreach (var stroke in strokes) { if (stroke.Points.Any(p => rect.Contains(p))) { selectedStrokes.Add(stroke); } } layer.SelectedStrokes = selectedStrokes; UpdateSelectionBox(); } private void UpdateSelectionBox() { if (selectedStrokes.Count == 0) { layer.SelectionBox = null; return; } double minX = double.MaxValue, minY = double.MaxValue; double maxX = double.MinValue, maxY = double.MinValue; foreach (var stroke in selectedStrokes) { foreach (var p in stroke.Points) { minX = Math.Min(minX, p.X); minY = Math.Min(minY, p.Y); maxX = Math.Max(maxX, p.X); maxY = Math.Max(maxY, p.Y); } } layer.SelectionBox = new Rect(minX, minY, maxX - minX, maxY - minY); } private void EraseAtPoint(Point point) { const double hitRadius = 5; SaveUndoState(); strokes.RemoveAll(s => s.Points.Exists(p => Distance(p, point) < hitRadius)); layer.Strokes = strokes; layer.InvalidateVisual(); } private double Distance(Point a, Point b) { var dx = a.X - b.X; var dy = a.Y - b.Y; return Math.Sqrt(dx * dx + dy * dy); } public void MoveSelected(Vector delta) { foreach (var stroke in selectedStrokes) { for (int i = 0; i < stroke.Points.Count; i++) stroke.Points[i] += delta; } UpdateSelectionBox(); layer.InvalidateVisual(); } private void SaveUndoState() { undoStack.Push(strokes.Select(s => new InkStroke { Points = new List<Point>(s.Points), Color = s.Color, Thickness = s.Thickness }).ToList()); redoStack.Clear(); layer.Strokes = strokes; } public void Undo() { if (undoStack.Count == 0) return; redoStack.Push(strokes); strokes = undoStack.Pop(); layer.Strokes = strokes; layer.InvalidateVisual(); } public void Redo() { if (redoStack.Count == 0) return; undoStack.Push(strokes); strokes = redoStack.Pop(); layer.Strokes = strokes; layer.InvalidateVisual(); } public IReadOnlyList<InkStroke> Strokes => strokes.AsReadOnly(); } }
SimpleInkCanvas.axaml代码,其中office.jpg要把属性设置为AvaloniaResource。目前AvaloniaResource除了对axaml有bug外,其他资源是没问题。
<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" Height="300" Width="300" x:Class="AvaloniaUI.SimpleInkCanvas" Title="SimpleInkCanvas"> <Grid RowDefinitions="auto,*"> <StackPanel Margin="5" Orientation="Horizontal"> <TextBlock Margin="5">EditingMode: </TextBlock> <ComboBox Name="lstEditingMode" VerticalAlignment="Center"> </ComboBox> </StackPanel> <InkCanvas Name="inkCanvas" Grid.Row="1" Background="LightYellow" EditingMode="{Binding ElementName=lstEditingMode,Path=SelectedItem}"> <Button Canvas.Top="10" Canvas.Left="10">Hello</Button> <Image Source="avares://AvaloniaUI/Resources/Images/office.jpg" Canvas.Top="10" Canvas.Left="50" Width="100" Height="100"/> </InkCanvas> </Grid> </Window>
SimpleInkCanvas.axaml.cs代码
using Avalonia; using Avalonia.Controls; using Avalonia.Markup.Xaml; using Shares.Avalonia; using System; namespace AvaloniaUI; public partial class SimpleInkCanvas : Window { public SimpleInkCanvas() { InitializeComponent(); foreach (InkEditingMode mode in Enum.GetValues(typeof(InkEditingMode))) { lstEditingMode.Items.Add(mode); lstEditingMode.SelectedItem = inkCanvas.EditingMode; } } }
运行效果