03 - LayoutPanels例子 - SimpleInkCanvas

C# maui暂时没有官方支持InkCanvas,但是不影响,自己实现一个就行了。目前支持,画图,选择,移动和删除。同时支持自定义橡皮擦形状,也支持绑定自定义的形状列表。

实现一个Converter类,以后所有的绑定类型转换都在这个类中实现。

复制代码
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Shares.Utility
{
    public class Converter : IValueConverter
    {
        public object? Convert(object? value, Type targetType, object? parameter, System.Globalization.CultureInfo culture)
        {
            // Implement conversion logic here
            if (value is List<string> list)
            {
                return string.Join(", ", list); // 自定义分隔符
            }
            else if (value is int intValue && targetType.IsEnum)
            {
                return Enum.ToObject(targetType, intValue); // 将整数转换为枚举类型
            }
            return value;
        }
        public object? ConvertBack(object? value, Type targetType, object? parameter, System.Globalization.CultureInfo culture)
        {
            // Implement conversion back logic here
            return value;
        }
    }
}

然后在MyStyles.xaml中添加Converter类的引用,这样以后所有项目都可以使用了,local是

xmlns:local="clr-namespace:Shares.Utility;assembly=Shares"

复制代码
    <!--converter定义-->
    <local:Converter x:Key="Converter"/>

InkCanvas重写GraphicsView

复制代码
    public class InkCanvas : GraphicsView, IDrawable
    {
        public class DrawingPath
        {
            private RectF? cachedBounds;
            private bool isDirty = true;

            public Guid Id { get; } = Guid.NewGuid();
            public PathF Path { get; set; } = new PathF();
            public Color? StrokeColor { get; set; }
            public float StrokeThickness { get; set; }
            public bool IsSelected { get; set; }
            public PointF Pos { get; set; }

            public RectF Bounds
            {
                get
                {
                    if (!isDirty && cachedBounds.HasValue)
                        return cachedBounds.Value;

                    if (Path.Count == 0)
                    {
                        cachedBounds = RectF.Zero;
                        return RectF.Zero;
                    }

                    var points = Path.Points;
                    float minX = float.MaxValue, minY = float.MaxValue;
                    float maxX = float.MinValue, maxY = float.MinValue;

                    foreach (var point in points)
                    {
                        float x = point.X + Pos.X;
                        float y = point.Y + Pos.Y;
                        minX = Math.Min(minX, x);
                        minY = Math.Min(minY, y);
                        maxX = Math.Max(maxX, x);
                        maxY = Math.Max(maxY, y);
                    }

                    cachedBounds = new RectF(minX, minY, maxX - minX, maxY - minY);
                    isDirty = false;
                    return cachedBounds.Value;
                }
            }

            public void InvalidateBounds() => isDirty = true;

            public void LineTo(float x, float y)
            {
                Path.LineTo(x, y);
                InvalidateBounds();
            }

            public bool IntersectAt(PointF eraserPos, float eraserRadius)
            {
                if (Path.Count == 0)
                    return false;

                // 优化点接触检查
                foreach (var point in Path.Points)
                {
                    float dx = point.X + Pos.X - eraserPos.X;
                    float dy = point.Y + Pos.Y - eraserPos.Y;
                    if (dx * dx + dy * dy <= eraserRadius * eraserRadius)
                    {
                        return true;
                    }
                }

                // 优化线段接触检查
                if (Path.Count >= 2)
                {
                    var points = Path.Points;
                    for (int i = 1; i < points.Count(); i++)
                    {
                        var start = new PointF(points.ElementAt(i - 1).X + Pos.X, points.ElementAt(i - 1).Y + Pos.Y);
                        var end = new PointF(points.ElementAt(i).X + Pos.X, points.ElementAt(i).Y + Pos.Y);

                        if (PointToLineDistance(start, end, eraserPos) <= eraserRadius)
                        {
                            return true;
                        }
                    }
                }

                return false;
            }

            public List<DrawingPath> SplitAt(PointF eraserPos, float eraserRadius)
            {
                var newPaths = new List<DrawingPath>();
                if (Path.Count < 2) return newPaths;

                var points = Path.Points;
                int bestIndex = -1;
                float minDistance = float.MaxValue;

                // 1. 检查点接触
                for (int i = 0; i < points.Count(); i++)
                {
                    float dx = points.ElementAt(i).X + Pos.X - eraserPos.X;
                    float dy = points.ElementAt(i).Y + Pos.Y - eraserPos.Y;
                    float distance = dx * dx + dy * dy;

                    if (distance < minDistance)
                    {
                        minDistance = distance;
                        bestIndex = i;
                    }
                }

                // 点接触处理
                if (bestIndex >= 0 && minDistance <= eraserRadius * eraserRadius)
                {
                    // 起点处理
                    if (bestIndex == 0)
                    {
                        if (points.Count() > 1)
                        {
                            var newPath = new DrawingPath
                            {
                                StrokeColor = StrokeColor,
                                StrokeThickness = StrokeThickness,
                                Pos = Pos
                            };
                            newPath.Path.MoveTo(points.ElementAt(1));
                            for (int i = 2; i < points.Count(); i++)
                            {
                                newPath.Path.LineTo(points.ElementAt(i));
                            }
                            newPaths.Add(newPath);
                        }
                        return newPaths;
                    }

                    // 终点处理
                    if (bestIndex == points.Count() - 1)
                    {
                        if (points.Count() > 1)
                        {
                            var newPath = new DrawingPath
                            {
                                StrokeColor = StrokeColor,
                                StrokeThickness = StrokeThickness,
                                Pos = Pos
                            };
                            newPath.Path.MoveTo(points.ElementAt(0));
                            for (int i = 1; i < points.Count() - 1; i++)
                            {
                                newPath.Path.LineTo(points.ElementAt(i));
                            }
                            newPaths.Add(newPath);
                        }
                        return newPaths;
                    }

                    // 中间点处理
                    if (bestIndex > 0 && bestIndex < points.Count() - 1)
                    {
                        // 第一段路径
                        var path1 = new DrawingPath
                        {
                            StrokeColor = StrokeColor,
                            StrokeThickness = StrokeThickness,
                            Pos = Pos
                        };
                        path1.Path.MoveTo(points.ElementAt(0));
                        for (int i = 1; i <= bestIndex; i++)
                        {
                            path1.Path.LineTo(points.ElementAt(i));
                        }
                        newPaths.Add(path1);

                        // 第二段路径
                        var path2 = new DrawingPath
                        {
                            StrokeColor = StrokeColor,
                            StrokeThickness = StrokeThickness,
                            Pos = Pos
                        };
                        path2.Path.MoveTo(points.ElementAt(bestIndex));
                        for (int i = bestIndex + 1; i < points.Count(); i++)
                        {
                            path2.Path.LineTo(points.ElementAt(i));
                        }
                        newPaths.Add(path2);

                        return newPaths;
                    }
                }

                // 2. 线段接触处理
                bestIndex = -1;
                minDistance = float.MaxValue;

                for (int i = 1; i < points.Count(); i++)
                {
                    var start = new PointF(points.ElementAt(i - 1).X + Pos.X, points.ElementAt(i - 1).Y + Pos.Y);
                    var end = new PointF(points.ElementAt(i).X + Pos.X, points.ElementAt(i).Y + Pos.Y);

                    float distance = PointToLineDistance(start, end, eraserPos);
                    if (distance < minDistance)
                    {
                        minDistance = distance;
                        bestIndex = i;
                    }
                }

                if (bestIndex > 0 && minDistance <= eraserRadius)
                {
                    // 第一段路径
                    if (bestIndex > 1)
                    {
                        var path1 = new DrawingPath
                        {
                            StrokeColor = StrokeColor,
                            StrokeThickness = StrokeThickness,
                            Pos = Pos
                        };
                        path1.Path.MoveTo(points.ElementAt(0));
                        for (int i = 1; i < bestIndex; i++)
                        {
                            path1.Path.LineTo(points.ElementAt(i));
                        }
                        newPaths.Add(path1);
                    }

                    // 第二段路径
                    if (bestIndex < points.Count() - 1)
                    {
                        var path2 = new DrawingPath
                        {
                            StrokeColor = StrokeColor,
                            StrokeThickness = StrokeThickness,
                            Pos = Pos
                        };
                        path2.Path.MoveTo(points.ElementAt(bestIndex));
                        for (int i = bestIndex + 1; i < points.Count(); i++)
                        {
                            path2.Path.LineTo(points.ElementAt(i));
                        }
                        newPaths.Add(path2);
                    }
                }

                return newPaths;
            }
        }

        public enum InkCanvasEditingMode { Ink, Select, Erase }

        public static readonly BindableProperty EditingModeProperty =
            BindableProperty.Create(nameof(EditingMode), typeof(InkCanvasEditingMode), typeof(InkCanvas),
                InkCanvasEditingMode.Ink, BindingMode.TwoWay, propertyChanged: OnEditingModeChanged);

        private static void OnEditingModeChanged(BindableObject bindable, object oldValue, object newValue)
        {
            if (bindable is InkCanvas canvas)
            {
                canvas.ClearSelection();
                canvas.Invalidate();
            }
        }

        public InkCanvasEditingMode EditingMode
        {
            get => (InkCanvasEditingMode)GetValue(EditingModeProperty);
            set => SetValue(EditingModeProperty, value);
        }

        public ObservableCollection<DrawingPath> Paths { get; set; } = new ObservableCollection<DrawingPath>();
        public DrawingPath Eraser { get; }
        public float EraserRadius { get; set; } = 15f; // 增大橡皮擦半径
        private DrawingPath? currentPath;
        private RectF? selectionRect;
        private PointF lastTouchPoint;
        private bool isMovingSelection;

        // 橡皮擦轨迹跟踪
        private readonly List<PointF> eraserTrail = new List<PointF>();
        private const int MaxEraserTrailPoints = 5;

        public Color StrokeColor { get; set; } = Colors.Black;
        public Color SelectionColor { get; set; } = Colors.Red;
        public float SelectionStrokeThickness { get; set; } = 1f;
        public float StrokeThickness { get; set; } = 1f;

        public InkCanvas()
        {
            Drawable = this;
            BackgroundColor = Colors.Transparent;
            Eraser = CreateEraserPath();

            StartInteraction += OnTouchStarted;
            DragInteraction += OnTouchMoved;
            EndInteraction += OnTouchEnded;
        }

        private DrawingPath CreateEraserPath()
        {
            var path = new PathF();
            var points = new[]
            {
            new PointF(107.4f, 13), new PointF(113.7f, 28.8f),
            new PointF(127.9f, 31.3f), new PointF(117.6f, 43.5f),
            new PointF(120.1f, 60.8f), new PointF(107.4f, 52.6f),
            new PointF(94.6f, 60.8f), new PointF(97.1f, 43.5f),
            new PointF(86.8f, 31.3f), new PointF(101f, 28.8f)
        };

            path.MoveTo(points[0]);
            for (int i = 1; i < points.Length; i++)
            {
                path.LineTo(points[i]);
            }
            path.Close();

            return new DrawingPath { Path = path, StrokeColor = Colors.Black, StrokeThickness = 1f };
        }

        private void OnTouchStarted(object? sender, TouchEventArgs e)
        {
            if (e.Touches.Length == 0) return;

            var point = e.Touches[0];
            lastTouchPoint = new PointF(point.X, point.Y);
            eraserTrail.Clear(); // 清除历史轨迹

            switch (EditingMode)
            {
                case InkCanvasEditingMode.Ink:
                    StartInking(lastTouchPoint);
                    break;

                case InkCanvasEditingMode.Select:
                    StartSelection(lastTouchPoint);
                    break;

                case InkCanvasEditingMode.Erase:
                    StartErase(lastTouchPoint);
                    eraserTrail.Add(lastTouchPoint); // 添加起始点
                    break;
            }
            Invalidate();
        }

        private void StartInking(PointF startPoint)
        {
            currentPath = new DrawingPath
            {
                StrokeColor = StrokeColor,
                StrokeThickness = StrokeThickness,
                Pos = PointF.Zero
            };
            currentPath.Path.MoveTo(startPoint.X, startPoint.Y);
            Paths.Add(currentPath);
        }

        private void StartSelection(PointF startPoint)
        {
            isMovingSelection = Paths.Any(p => p.IsSelected && p.Bounds.Contains(startPoint));

            if (!isMovingSelection)
            {
                ClearSelection();
                var clickedPath = Paths.LastOrDefault(p => p.Bounds.Contains(startPoint));

                if (clickedPath != null)
                {
                    clickedPath.IsSelected = true;
                    isMovingSelection = true;
                }
                else
                {
                    selectionRect = new RectF(startPoint, SizeF.Zero);
                }
            }
        }

        private void StartErase(PointF startPoint)
        {
            Eraser.Pos = new PointF(startPoint.X - Eraser.Path.Bounds.Width / 2,
                                    startPoint.Y - Eraser.Path.Bounds.Height / 4);
            Eraser.IsSelected = true;
        }

        private void OnTouchMoved(object? sender, TouchEventArgs e)
        {
            if (e.Touches.Length == 0) return;

            var currentPoint = new PointF(e.Touches[0].X, e.Touches[0].Y);

            switch (EditingMode)
            {
                case InkCanvasEditingMode.Ink:
                    ContinueInking(currentPoint);
                    break;

                case InkCanvasEditingMode.Select:
                    UpdateSelection(currentPoint);
                    break;

                case InkCanvasEditingMode.Erase:
                    UpdateEraser(currentPoint);
                    ErasePaths();
                    break;
            }
            Invalidate();
        }

        private void ContinueInking(PointF currentPoint)
        {
            if (currentPath == null) return;

            const float minDistance = 1.0f;
            float dx = currentPoint.X - lastTouchPoint.X;
            float dy = currentPoint.Y - lastTouchPoint.Y;

            if (dx * dx + dy * dy > minDistance * minDistance)
            {
                currentPath.LineTo(currentPoint.X, currentPoint.Y);
                lastTouchPoint = currentPoint;
            }
        }

        private void UpdateSelection(PointF currentPoint)
        {
            if (isMovingSelection)
            {
                MoveSelectedPaths(currentPoint);
            }
            else if (selectionRect.HasValue)
            {
                UpdateSelectionRect(currentPoint);
            }
        }

        private void UpdateEraser(PointF currentPoint)
        {
            Eraser.Pos = new PointF(currentPoint.X - Eraser.Path.Bounds.Width / 2,
                                    currentPoint.Y - Eraser.Path.Bounds.Height / 4);

            // 添加到橡皮擦轨迹
            eraserTrail.Add(Eraser.Pos);
            if (eraserTrail.Count > MaxEraserTrailPoints)
            {
                eraserTrail.RemoveAt(0);
            }

            lastTouchPoint = currentPoint;
        }

        // 优化擦除逻辑
        private void ErasePaths()
        {
            // 倒序遍历所有路径
            for (int i = Paths.Count - 1; i >= 0; i--)
            {
                var path = Paths[i];

                // 检查橡皮擦轨迹上的所有点
                foreach (var trailPoint in eraserTrail)
                {
                    if (path.IntersectAt(trailPoint, EraserRadius))
                    {
                        var newPaths = path.SplitAt(trailPoint, EraserRadius);

                        if (newPaths.Count > 0)
                        {
                            Paths.RemoveAt(i);
                            foreach (var newPath in newPaths)
                            {
                                if (newPath.Path.Count >= 2) // 只添加有效路径
                                {
                                    Paths.Add(newPath);
                                }
                            }
                            break; // 路径已被处理,跳出循环
                        }
                        else
                        {
                            // 没有新路径表示整个路径应被删除
                            Paths.RemoveAt(i);
                            break;
                        }
                    }
                }
            }
        }

        private void MoveSelectedPaths(PointF currentPoint)
        {
            float deltaX = currentPoint.X - lastTouchPoint.X;
            float deltaY = currentPoint.Y - lastTouchPoint.Y;

            foreach (var path in Paths)
            {
                if (path.IsSelected)
                {
                    path.Pos = new PointF(path.Pos.X + deltaX, path.Pos.Y + deltaY);
                    path.InvalidateBounds();
                }
            }
            lastTouchPoint = currentPoint;
        }

        private void UpdateSelectionRect(PointF currentPoint)
        {
            float x = Math.Min(lastTouchPoint.X, currentPoint.X);
            float y = Math.Min(lastTouchPoint.Y, currentPoint.Y);
            float width = Math.Abs(currentPoint.X - lastTouchPoint.X);
            float height = Math.Abs(currentPoint.Y - lastTouchPoint.Y);

            selectionRect = new RectF(x, y, width, height);
        }

        private void OnTouchEnded(object? sender, TouchEventArgs e)
        {
            switch (EditingMode)
            {
                case InkCanvasEditingMode.Select when selectionRect.HasValue:
                    FinalizeSelection();
                    break;
            }

            currentPath = null;
            selectionRect = null;
            isMovingSelection = false;
            Eraser.IsSelected = false;
            eraserTrail.Clear(); // 清除橡皮擦轨迹
            Invalidate();
        }

        private void FinalizeSelection()
        {
            var selection = selectionRect!.Value;

            foreach (var path in Paths)
            {
                if (!selection.IntersectsWith(path.Bounds)) continue;

                if (selection.Contains(path.Bounds))
                {
                    path.IsSelected = true;
                    continue;
                }

                foreach (var point in path.Path.Points)
                {
                    var absolutePoint = new PointF(point.X + path.Pos.X, point.Y + path.Pos.Y);
                    if (selection.Contains(absolutePoint))
                    {
                        path.IsSelected = true;
                        break;
                    }
                }
            }
        }

        public void ClearSelection()
        {
            foreach (var path in Paths)
            {
                path.IsSelected = false;
            }
        }

        public void Draw(ICanvas canvas, RectF dirtyRect)
        {
            canvas.FillColor = BackgroundColor;
            canvas.FillRectangle(dirtyRect);

            canvas.StrokeLineCap = LineCap.Round;
            canvas.StrokeLineJoin = LineJoin.Round;

            // 绘制所有路径
            foreach (var path in Paths)
            {
                // 跳过无效路径(少于2个点)
                if (path.Path.Count < 2) continue;

                DrawPath(canvas, path);
            }

            // 绘制橡皮擦(如果被选中)
            if (Eraser.IsSelected)
            {
                DrawEraser(canvas);
            }

            // 绘制选择框
            if (selectionRect.HasValue)
            {
                DrawSelectionRect(canvas, selectionRect.Value);
            }
        }

        private void DrawPath(ICanvas canvas, DrawingPath path)
        {
            var strokeColor = path.StrokeColor ?? Colors.Black;
            float strokeSize = path.IsSelected ? path.StrokeThickness * 1.5f : path.StrokeThickness;

            if (!path.IsSelected)
            {
                strokeColor = strokeColor.WithAlpha(0.5f);
            }

            canvas.StrokeColor = strokeColor;
            canvas.StrokeSize = strokeSize;

            canvas.SaveState();
            canvas.Translate(path.Pos.X, path.Pos.Y);
            canvas.DrawPath(path.Path);
            canvas.RestoreState();
        }

        private void DrawEraser(ICanvas canvas)
        {
            canvas.SaveState();
            canvas.Translate(Eraser.Pos.X, Eraser.Pos.Y);
            canvas.Scale(0.2f, 0.2f);
            canvas.StrokeColor = Eraser.StrokeColor ?? Colors.Black;
            canvas.StrokeSize = Eraser.StrokeThickness;
            canvas.FillColor = Color.FromArgb("#FFD700");
            canvas.FillPath(Eraser.Path);
            canvas.DrawPath(Eraser.Path);
            canvas.RestoreState();
        }

        private void DrawSelectionRect(ICanvas canvas, RectF rect)
        {
            canvas.SaveState();
            canvas.StrokeColor = SelectionColor;
            canvas.StrokeSize = SelectionStrokeThickness;
            canvas.StrokeDashPattern = new float[] { 5, 3 };
            canvas.DrawRectangle(rect);
            canvas.RestoreState();
        }

        // 静态工具方法
        public static float Distance(PointF a, PointF b)
            => (float)Math.Sqrt(Math.Pow(a.X - b.X, 2) + Math.Pow(a.Y - b.Y, 2));

        public static float DistanceSquared(PointF a, PointF b)
            => (a.X - b.X) * (a.X - b.X) + (a.Y - b.Y) * (a.Y - b.Y);

        public static float PointToLineDistance(PointF lineStart, PointF lineEnd, PointF point)
        {
            float l2 = DistanceSquared(lineStart, lineEnd);
            if (l2 == 0) return Distance(point, lineStart);

            float t = Math.Max(0, Math.Min(1, Vector2.Dot(
                new Vector2(point.X - lineStart.X, point.Y - lineStart.Y),
                new Vector2(lineEnd.X - lineStart.X, lineEnd.Y - lineStart.Y)) / l2));

            PointF projection = new PointF(
                lineStart.X + t * (lineEnd.X - lineStart.X),
                lineStart.Y + t * (lineEnd.Y - lineStart.Y)
            );

            return Distance(point, projection);
        }
    }

SimpleInkCanvas.xaml

复制代码
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:local="clr-namespace:Shares.Utility;assembly=Shares"
             x:Class="MauiViews.MauiDemos.Book._03.SimpleInkCanvas"
             Title="SimpleInkCanvas" HeightRequest="300" WidthRequest="300">
    <Grid RowDefinitions="auto,*">
        <StackLayout Margin="5" Orientation="Horizontal">
            <Label Text="EditingMode:" Margin="5" VerticalOptions="Center" FontSize="16"/>
            <Picker x:Name="lstEditingMode" VerticalOptions="Center"/>
        </StackLayout>
        <local:InkCanvas Grid.Row="1" BackgroundColor="LightYellow" 
                         EditingMode="{Binding Path=SelectedIndex, 
                         Source={x:Reference lstEditingMode}, Converter={StaticResource Converter}}"/>
        <Button Text="Hello" Grid.Row="1" WidthRequest="78" HeightRequest="16" 
                HorizontalOptions="Start" VerticalOptions="Start"/>
    </Grid>
</ContentPage>

对应的cs代码

复制代码
using static Shares.Utility.InkCanvas;

namespace MauiViews.MauiDemos.Book._03;

public partial class SimpleInkCanvas : ContentPage
{
	public SimpleInkCanvas()
	{
		InitializeComponent();
		foreach (InkCanvasEditingMode mode in Enum.GetValues(typeof(InkCanvasEditingMode)))
		{
            lstEditingMode.Items.Add(mode.ToString());
			lstEditingMode.SelectedIndex = 0;
        }
    }
}

运行效果

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