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; set;} 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) { 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; } } }
运行效果