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