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;
        }
    }
}

运行效果

相关推荐
dalgleish3 天前
C# Avalonia动态加载xaml和cs实例
跨平台·mvvm·c# avalonia
十五年专注C++开发5 天前
CMake基础:条件判断详解
c++·跨平台·cmake·自动化编译
dalgleish18 天前
03 - LayoutPanels例子 - TextBox
跨平台·mvvm·c# maui
专注VB编程开发20年18 天前
java/.net跨平台UI浏览器SDK,浏览器控件开发包分析
linux·ui·跨平台·浏览器·cef·miniblink
Areslee18 天前
一种通用跨平台实现SEH的解决方案
linux·macos·内核·跨平台·seh
攻城狮7号18 天前
【AI时代速通QT】第二节:Qt SDK 的目录介绍和第一个Qt Creator项目
c语言·c++·qt·跨平台
dalgleish22 天前
03 - LayoutPanels例子 - SimpleInkCanvas
跨平台·mvvm·c# maui
南岩亦凛汀23 天前
在Linux下使用wxWidgets进行跨平台GUI开发(三)
c++·跨平台·gui·开源框架·工程实战教程
南岩亦凛汀1 个月前
使用wxWidgets进行跨平台GUI开发(附1)
跨平台·gui·开源框架·工程实战教程