C# Avalonia 03 – LayoutPanels – SimpleInkCanvas

这次继承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;         }     } } 

运行效果

C# Avalonia 03 - LayoutPanels - SimpleInkCanvas

 

发表评论

评论已关闭。

相关文章