这次继承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;
}
}
}
运行效果