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

运行效果

03 - LayoutPanels例子 - SimpleInkCanvas

 

发表评论

评论已关闭。

相关文章