文中例子是基于wpf Canvas写的,由于Maui还没有支持Canvas,所以顺手自己写一个。之前写了一个InkCanvas,发现扩展性太差了,这次写这个Canvas,彻底解决扩展性问题,支持自定义碰撞测试等。自己写的碰撞测试,是基于点集碰撞测试,可以处理任何点集,所以大家可以继承Shape类,写自己的Shape类。我抛砖引玉,写了几个常用的。Canvas目前支持的功能,单选,多选,单选移动,多选移动,二指手势缩放,多指手势选中。删除功能很简单,就不实现了。
Shape类以及子类扩展(ImageShape是一个非常有用的子类,里面有如何把ImageSource转换为IImage的代码),利用矩阵完成旋转,位移,缩放。把常见的实现放到了基类,这样子类可以专注StyleDraw的逻辑,而不用担心旋转等影响。
//Shape基类 public abstract class Shape : BindableObject { public static readonly BindableProperty FillColorProperty = BindableProperty.Create(nameof(FillColor), typeof(Color), typeof(Shape), Colors.Transparent); public static readonly BindableProperty StrokeColorProperty = BindableProperty.Create(nameof(StrokeColor), typeof(Color), typeof(Shape), Colors.Black); public static readonly BindableProperty StrokeThicknessProperty = BindableProperty.Create(nameof(StrokeThickness), typeof(float), typeof(Shape), 1f); public static readonly BindableProperty XProperty = BindableProperty.Create(nameof(X), typeof(float), typeof(Shape), 0f); public static readonly BindableProperty YProperty = BindableProperty.Create(nameof(Y), typeof(float), typeof(Shape), 0f); public static readonly BindableProperty WidthProperty = BindableProperty.Create(nameof(Width), typeof(float), typeof(Shape), 100f); public static readonly BindableProperty HeightProperty = BindableProperty.Create(nameof(Height), typeof(float), typeof(Shape), 100f); public static readonly BindableProperty RotationProperty = BindableProperty.Create(nameof(Rotation), typeof(float), typeof(Shape), 0f); public static readonly BindableProperty ScaleXProperty = BindableProperty.Create(nameof(ScaleX), typeof(float), typeof(Shape), 1f); public static readonly BindableProperty ScaleYProperty = BindableProperty.Create(nameof(ScaleY), typeof(float), typeof(Shape), 1f); public static readonly BindableProperty IsSelectedProperty = BindableProperty.Create(nameof(IsSelected), typeof(bool), typeof(Shape), false); public static readonly BindableProperty StrokeDashPatternProperty = BindableProperty.Create(nameof(StrokeDashPattern), typeof(string), typeof(RectangleShape), null); public static readonly BindableProperty StrokeDashOffsetProperty = BindableProperty.Create(nameof(StrokeDashOffset), typeof(float), typeof(RectangleShape), 0f); public static readonly BindableProperty AspectRatioProperty = BindableProperty.Create(nameof(AspectRatio), typeof(float), typeof(Shape), 1f); public Color FillColor { get => (Color)GetValue(FillColorProperty); set => SetValue(FillColorProperty, value); } public Color StrokeColor { get => (Color)GetValue(StrokeColorProperty); set => SetValue(StrokeColorProperty, value); } public float StrokeThickness { get => (float)GetValue(StrokeThicknessProperty); set => SetValue(StrokeThicknessProperty, value); } public float X { get => (float)GetValue(XProperty); set => SetValue(XProperty, value); } public float Y { get => (float)GetValue(YProperty); set => SetValue(YProperty, value); } public float Width { get => (float)GetValue(WidthProperty); set => SetValue(WidthProperty, value); } public float Height { get => (float)GetValue(HeightProperty); set => SetValue(HeightProperty, value); } public float Rotation { get => (float)GetValue(RotationProperty); set => SetValue(RotationProperty, value); } public float ScaleX { get => (float)GetValue(ScaleXProperty); set => SetValue(ScaleXProperty, value); } public float ScaleY { get => (float)GetValue(ScaleYProperty); set => SetValue(ScaleYProperty, value); } public string StrokeDashPattern { get => (string)GetValue(StrokeDashPatternProperty); set => SetValue(StrokeDashPatternProperty, value); } public float StrokeDashOffset { get => (float)GetValue(StrokeDashOffsetProperty); set => SetValue(StrokeDashOffsetProperty, value); } public bool IsSelected { get => (bool)GetValue(IsSelectedProperty); set => SetValue(IsSelectedProperty, value); } public float AspectRatio { get => (float)GetValue(AspectRatioProperty); set => SetValue(AspectRatioProperty, value); } public RectF Bounds { get { // 使用局部坐标系(X, Y)为左上角的顶点 PointF[] points = this.GetPoints(); // 应用当前变换矩阵到所有顶点 Matrix3x2 transform = GetTransformMatrix(); for (int i = 0; i < points.Length; i++) { points[i] = Transform(points[i], transform); } // 计算变换后顶点的边界框 // 计算变换后边界 float minX = points.Min(p => p.X); float minY = points.Min(p => p.Y); float maxX = points.Max(p => p.X); float maxY = points.Max(p => p.Y); return new RectF(minX, minY, maxX - minX, maxY - minY); } } // 获取变换矩 protected Matrix3x2 GetTransformMatrix() { // 计算原始中心点(局部坐标系) float centerX = X + Width / 2; float centerY = Y + Height / 2; // 构建变换矩阵 return Matrix3x2.CreateScale(AspectRatio, AspectRatio) * Matrix3x2.CreateRotation(Rotation * (MathF.PI / 180), new Vector2(centerX, centerY)) * Matrix3x2.CreateScale(ScaleX, ScaleY); } //获取逆矩阵 protected Matrix3x2 GetInverseMatrix() { Matrix3x2.Invert(GetTransformMatrix(), out Matrix3x2 result); return result; } public PointF Transform(PointF point, Matrix3x2 matrix) { return new PointF( point.X * matrix.M11 + point.Y * matrix.M21 + matrix.M31, point.X * matrix.M12 + point.Y * matrix.M22 + matrix.M32 ); } //子类可重写 public virtual bool HitTest(PointF p, float tolerance = 5f) { PointF[] points = GetPoints(); Matrix3x2 transform = GetTransformMatrix(); for (int i = 0; i < points.Length; i++) { points[i] = Transform(points[i], transform); } //简单判断 if (Bounds.Contains(p)) { //检查一个点或者两个点 if (points.Count() == 1) { return Math.Sqrt(DistanceSquare(p, points[0])) <= tolerance; } else if (points.Count() == 2) { return DistanceToSegment(p, points[0], points[1]) <= tolerance; } //点在形状类 return IsPointInPolygon(p, points) || IsPointNearPolygonEdge(p, points, tolerance); } return false; } public virtual bool HitTest(Shape other) { if (this.Bounds.IntersectsWith(other.Bounds)) { // 使用局部坐标系(X, Y)为左上角的顶点 PointF[] pointsA = this.GetPoints(); PointF[] pointsB = other.GetPoints(); // 应用当前变换矩阵到所有顶点 Matrix3x2 transformA = GetTransformMatrix(); Matrix3x2 transformB = other.GetTransformMatrix(); for (int i = 0; i < pointsA.Length; i++) { pointsA[i] = Transform(pointsA[i], transformA); } for (int i = 0; i < pointsB.Length; i++) { pointsB[i] = Transform(pointsB[i], transformB); } return PolygonIntersects(pointsA, pointsB); } return false; } //形状到形状 : 检测两个多边形是否相交 public static bool PolygonIntersects(PointF[] polyA, PointF[] polyB) { // 检测polyA的边是否与polyB相交 for (int i = 0; i < polyA.Length; i++) { int nextI = (i + 1) % polyA.Length; for (int j = 0; j < polyB.Length; j++) { int nextJ = (j + 1) % polyB.Length; if (LinesIntersect(polyA[i], polyA[nextI], polyB[j], polyB[nextJ])) return true; } } // 检测一个多边形是否完全包含在另一个多边形中 if (IsPointInPolygon(polyA[0], polyB) || IsPointInPolygon(polyB[0], polyA)) return true; return false; } //点到点 public static float DistanceSquare(PointF v, PointF w) { return (v.X - w.X) * (v.X - w.X) + (v.Y - w.Y) * (v.Y - w.Y); } //点到线 public static float DistanceToSegment(PointF p, PointF v, PointF w) { float l2 = (v.X - w.X) * (v.X - w.X) + (v.Y - w.Y) * (v.Y - w.Y); if (l2 == 0.0) return (float)Math.Sqrt(DistanceSquare(p, v)); float t = Math.Max(0, Math.Min(1, ((p.X - v.X) * (w.X - v.X) + (p.Y - v.Y) * (w.Y - v.Y)) / l2)); PointF projection = new PointF( v.X + t * (w.X - v.X), v.Y + t * (w.Y - v.Y)); return (float)Math.Sqrt(DistanceSquare(p, projection)); } // 射线法判断点是否在多边形内部,默认是闭合路径 public static bool IsPointInPolygon(PointF p, PointF[] polygon) { if (polygon.Length < 3) return false; bool inside = false; int j = polygon.Length - 1; for (int i = 0; i < polygon.Length; i++) { if ((polygon[i].Y > p.Y) != (polygon[j].Y > p.Y) && p.X < (polygon[j].X - polygon[i].X) * (p.Y - polygon[i].Y) / (polygon[j].Y - polygon[i].Y) + polygon[i].X) { inside = !inside; } j = i; } return inside; } // 判断点是否在多边形边线附近 public static bool IsPointNearPolygonEdge(PointF p, PointF[] points, float tolerance) { if (points.Length < 2) return false; for (int i = 0; i < points.Length; i++) { int next = (i + 1) % points.Length; float distance = DistanceToSegment(p, points[i], points[next]); if (distance <= tolerance) return true; } return false; } //检测两条线段是否相交 public static bool LinesIntersect(PointF a1, PointF a2, PointF b1, PointF b2) { float d = (b2.Y - b1.Y) * (a2.X - a1.X) - (b2.X - b1.X) * (a2.Y - a1.Y); if (d == 0) return false; // 平行线 float uA = ((b2.X - b1.X) * (a1.Y - b1.Y) - (b2.Y - b1.Y) * (a1.X - b1.X)) / d; float uB = ((a2.X - a1.X) * (a1.Y - b1.Y) - (a2.Y - a1.Y) * (a1.X - b1.X)) / d; return uA >= 0 && uA <= 1 && uB >= 0 && uB <= 1; } public void Draw(ICanvas canvas, RectF dirtyRect) { canvas.SaveState(); canvas.FillColor = FillColor; canvas.StrokeColor = StrokeColor; canvas.StrokeSize = StrokeThickness; canvas.StrokeDashPattern = StrokeDashPattern?.Split(" ").Select(s => float.Parse(s)).ToArray(); canvas.StrokeDashOffset = StrokeDashOffset; //测试点击区域 if (IsSelected) { canvas.SaveState(); RectF bounds = this.Bounds; canvas.StrokeColor = Colors.Gray; canvas.StrokeSize = 1f; canvas.StrokeDashPattern = new float[] { 5, 3 }; canvas.DrawRectangle(bounds); canvas.RestoreState(); } canvas.ConcatenateTransform(this.GetTransformMatrix()); StyleDraw(canvas, dirtyRect); canvas.RestoreState(); } //点集到线 public static PathF CreatePathF(PointF[] points, bool closed = true) { if (points.Length == 0) return new PathF(); PathF path = new PathF(); path.MoveTo(points[0]); for (int i = 1; i < points.Length; i++) { path.LineTo(points[i]); } if (closed) path.Close(); return path; } protected abstract void StyleDraw(ICanvas canvas, RectF dirtyRect); protected abstract PointF[] GetPoints(); } //长方形类 public class RectangleShape : Shape { public static readonly BindableProperty CornerRadiusProperty = BindableProperty.Create(nameof(CornerRadius), typeof(float), typeof(RectangleShape), 0f); public float CornerRadius { get => (float)GetValue(CornerRadiusProperty); set => SetValue(CornerRadiusProperty, value); } protected override PointF[] GetPoints() { return new PointF[] { new PointF(X, Y), // 左上 new PointF(X + Width, Y), // 右上 new PointF(X + Width, Y + Height), // 右下 new PointF(X, Y + Height) // 左下 }; } protected override void StyleDraw(ICanvas canvas, RectF dirtyRect) { // 绘制原始矩形(局部坐标) canvas.FillRoundedRectangle(X, Y, Width, Height, CornerRadius); canvas.DrawRoundedRectangle(X, Y, Width, Height, CornerRadius); } } //椭圆形类 public class EllipseShape : Shape { //Length越大,性能要求越高,但是碰撞判断越精确。 public static readonly BindableProperty LengthProperty = BindableProperty.Create(nameof(Length), typeof(int), typeof(EllipseShape), 60); public static readonly BindableProperty RadiusXProperty = BindableProperty.Create(nameof(RadiusX), typeof(float), typeof(EllipseShape), 0f); public static readonly BindableProperty RadiusYProperty = BindableProperty.Create(nameof(RadiusY), typeof(float), typeof(EllipseShape), 0f); public int Length { get => (int)GetValue(LengthProperty); set => SetValue(LengthProperty, value); } public float RadiusX { get => (float)GetValue(RadiusXProperty); set => SetValue(RadiusXProperty, value); } public float RadiusY { get => (float)GetValue(RadiusYProperty); set => SetValue(RadiusYProperty, value); } protected override PointF[] GetPoints() { List<PointF> points = new List<PointF>(); float radiusX = RadiusX == 0 ? Width / 2 : RadiusX; float radiusY = RadiusY == 0 ? Height / 2 : RadiusY; float centerX = X + radiusX; float centerY = Y + radiusY; for (int i = 0; i < Length; i++) { float angle = i * (float)Math.PI / Length * 2f; points.Add(new PointF( centerX + radiusX * (float)Math.Cos(angle), centerY + radiusY * (float)Math.Sin(angle))); } return points.ToArray(); } protected override void StyleDraw(ICanvas canvas, RectF dirtyRect) { float radiusX = RadiusX == 0 ? Width / 2 : RadiusX; float radiusY = RadiusY == 0 ? Height / 2 : RadiusY; canvas.FillEllipse(X, Y, radiusX * 2, radiusY * 2); canvas.DrawEllipse(X, Y, radiusX * 2, radiusY * 2); } } //三角形类 public class TriangleShape : Shape { protected override PointF[] GetPoints() { return new PointF[] { new PointF(X + Width / 2, Y), // 顶点 new PointF(X + Width, Y + Height), // 右下角 new PointF(X, Y + Height) // 左下角 }; } protected override void StyleDraw(ICanvas canvas, RectF dirtyRect) { PathF path = CreatePathF(GetPoints()); canvas.FillPath(path); canvas.DrawPath(path); } } //线段或自定义类,支持SVG等 public class PathShape : Shape { public static readonly BindableProperty DataProperty = BindableProperty.Create(nameof(Data), typeof(string), typeof(PathShape), null); public static readonly BindableProperty IsClosedPathProperty = BindableProperty.Create(nameof(IsClosedPath), typeof(bool), typeof(PathShape), true); public string Data { get => (string)GetValue(DataProperty); set => SetValue(DataProperty, value); } public bool IsClosedPath { get => (bool)GetValue(IsClosedPathProperty); set => SetValue(IsClosedPathProperty, value); } protected override PointF[] GetPoints() { if (Data != null) { PointF[] points = PathBuilder.Build(Data).Points.ToArray(); for (int i = 0; i < points.Length; i++) { points[i].X += X; points[i].Y += Y; } return points; } return Array.Empty<PointF>(); } protected override void StyleDraw(ICanvas canvas, RectF dirtyRect) { PathF path = CreatePathF(GetPoints(), IsClosedPath); canvas.FillPath(path); canvas.DrawPath(path); } //分割线段 public List<PointF[]> SplitAt(PointF point, float tolerance = 5f) { PointF[] points = GetPoints(); // 应用当前变换矩阵到所有顶点 Matrix3x2 transform = GetTransformMatrix(); for (int i = 0; i < points.Length; i++) { points[i] = Transform(points[i], transform); } List<PointF[]> result = new List<PointF[]>(); if (points.Length < 2) { // 点太少无法分割 return result; } // 1. 查找最近的线段和分割点 float minDistance = float.MaxValue; int splitIndex = -1; PointF splitPoint = PointF.Zero; bool isClosingSegment = false; // 检查所有线段(包括可能的闭合线段) for (int i = 0; i < points.Length - 1; i++) { CheckSegment(points[i], points[i + 1], i, ref minDistance, ref splitIndex, ref splitPoint, point); } // 如果是闭合路径,检查最后一段(从最后一个点到第一个点) if (IsClosedPath && points.Length > 1) { isClosingSegment = CheckSegment(points[points.Length - 1], points[0], points.Length - 1, ref minDistance, ref splitIndex, ref splitPoint, point); } // 2. 如果没有找到在容差范围内的分割点 if (minDistance > tolerance || splitIndex == -1) { return result; } // 3. 执行分割 if (isClosingSegment) { // 在闭合线段上分割 SplitClosingSegment(points, splitPoint, result); } else { // 在普通线段上分割 SplitRegularSegment(points, splitIndex, splitPoint, result); } return result; } private bool CheckSegment(PointF a, PointF b, int index, ref float minDistance, ref int splitIndex, ref PointF splitPoint, PointF testPoint) { float distance; PointF projection = GetProjectionOnSegment(testPoint, a, b, out distance); if (distance < minDistance) { minDistance = distance; splitIndex = index; splitPoint = projection; return true; } return false; } private PointF GetProjectionOnSegment(PointF p, PointF a, PointF b, out float distance) { Vector2 ap = new Vector2(p.X - a.X, p.Y - a.Y); Vector2 ab = new Vector2(b.X - a.X, b.Y - a.Y); float magnitude = ab.LengthSquared(); if (magnitude == 0) { distance = (float)Math.Sqrt(DistanceSquare(p, a)); return a; } float t = Math.Clamp(Vector2.Dot(ap, ab) / magnitude, 0, 1); PointF projection = new PointF( a.X + t * ab.X, a.Y + t * ab.Y ); distance = (float)Math.Sqrt(DistanceSquare(p, projection)); return projection; } private void SplitRegularSegment(PointF[] points, int splitIndex, PointF splitPoint, List<PointF[]> result) { // 第一部分:起点到分割点 List<PointF> part1 = new List<PointF>(); for (int i = 0; i <= splitIndex; i++) { part1.Add(points[i]); } part1.Add(splitPoint); // 第二部分:分割点到终点 List<PointF> part2 = new List<PointF>(); part2.Add(splitPoint); for (int i = splitIndex + 1; i < points.Length; i++) { part2.Add(points[i]); } result.Add(part1.ToArray()); result.Add(part2.ToArray()); } private void SplitClosingSegment(PointF[] points, PointF splitPoint, List<PointF[]> result) { // 第一部分:起点到最后一个点 + 分割点 List<PointF> part1 = new List<PointF>(points); part1.Add(splitPoint); // 第二部分:分割点到起点 List<PointF> part2 = new List<PointF>(); part2.Add(splitPoint); part2.Add(points[0]); result.Add(part1.ToArray()); result.Add(part2.ToArray()); } } //图片 public class ImageShape : Shape { public static readonly BindableProperty SourceProperty = BindableProperty.Create(nameof(Source), typeof(ImageSource), typeof(ImageShape), null); public ImageSource Source { get => (ImageSource)GetValue(SourceProperty); set => SetValue(SourceProperty, value); } private IImage? image; public ImageShape() { Dispatcher.Dispatch(() => { image = ConvertImageSourceToIImage(Source); }); } protected override PointF[] GetPoints() { return new PointF[] { new PointF(X, Y), // 左上 new PointF(X + Width, Y), // 右上 new PointF(X + Width, Y + Height), // 右下 new PointF(X, Y + Height) // 左下 }; } protected override void StyleDraw(ICanvas canvas, RectF dirtyRect) { if (image != null) canvas.DrawImage(image, X, Y, Width, Height); } public static IImage? ConvertImageSourceToIImage(ImageSource imageSource) { try { // 1. 将 ImageSource 转换为 Stream Stream? stream = GetStreamFromImageSource(imageSource); // 2. 使用 PlatformImage 加载流 return PlatformImage.FromStream(stream); } catch (Exception ex) { Trace.WriteLine($"转换失败: {ex.Message}"); return null; } } private static Stream? GetStreamFromImageSource(ImageSource imageSource) { if (imageSource is FileImageSource fileSource) { // 资源一定是"嵌入的资源" Assembly assembly = Shell.Current.GetType().GetTypeInfo().Assembly; return assembly.GetManifestResourceStream(assembly.FullName?.Split(',').First() + ".Resources.Images." + fileSource.File); } else if (imageSource is StreamImageSource streamSource) { // 处理流 return streamSource.Stream(CancellationToken.None).Result; } else if (imageSource is UriImageSource uriSource) { // 处理网络图片 using var httpClient = new HttpClient(); var response = httpClient.GetAsync(uriSource.Uri).Result; return response.Content.ReadAsStreamAsync().Result; } Trace.WriteLine("不支持的ImageSource类型"); return null; } } //文字 public class TextShape : Shape { [Flags] public enum TextAttributes { None = 0, Bold = 1 << 0, Italic = 1 << 1, Underline = 1 << 2, Shadow = 1 << 3, } public static readonly BindableProperty TextProperty = BindableProperty.Create(nameof(Text), typeof(string), typeof(TextShape), null); public static readonly BindableProperty FontSizeProperty = BindableProperty.Create(nameof(FontSize), typeof(float), typeof(TextShape), 16f); public static readonly BindableProperty FontColorProperty = BindableProperty.Create(nameof(FontColor), typeof(Color), typeof(TextShape), Colors.Black); public static readonly BindableProperty FontFamilyProperty = BindableProperty.Create(nameof(FontFamily), typeof(string), typeof(TextShape), "Arial"); public static readonly BindableProperty FontAttributesProperty = BindableProperty.Create(nameof(FontAttributes), typeof(TextAttributes), typeof(TextShape), TextAttributes.None); public static readonly BindableProperty HorizontalAlignmentProperty = BindableProperty.Create(nameof(HorizontalAlignment), typeof(HorizontalAlignment), typeof(TextShape), HorizontalAlignment.Left); public static readonly BindableProperty VerticalAlignmentProperty = BindableProperty.Create(nameof(VerticalAlignment), typeof(VerticalAlignment), typeof(TextShape), VerticalAlignment.Center); public string Text { get => (string)GetValue(TextProperty); set => SetValue(TextProperty, value); } public float FontSize { get => (float)GetValue(FontSizeProperty); set => SetValue(FontSizeProperty, value); } public Color FontColor { get => (Color)GetValue(FontColorProperty); set => SetValue(FontColorProperty, value); } public string FontFamily { get => (string)GetValue(FontFamilyProperty); set => SetValue(FontFamilyProperty, value); } public TextAttributes FontAttributes { get => (TextAttributes)GetValue(FontAttributesProperty); set => SetValue(FontAttributesProperty, value); } public HorizontalAlignment HorizontalAlignment { get => (HorizontalAlignment)GetValue(HorizontalAlignmentProperty); set => SetValue(HorizontalAlignmentProperty, value); } public VerticalAlignment VerticalAlignment { get => (VerticalAlignment)GetValue(VerticalAlignmentProperty); set => SetValue(VerticalAlignmentProperty, value); } private SizeF size = SizeF.Zero; private const float shadowOffset = 2; protected override PointF[] GetPoints() { //canvas.GetStringSize存在bug,长宽反了 float w = size.Height, h = size.Width * 1.2f; return new PointF[] { new PointF(X, Y), // 左上 new PointF(X + w, Y), // 右上 new PointF(X + w, Y + h), // 右下 new PointF(X, Y + h) // 左下 }; } protected override void StyleDraw(ICanvas canvas, RectF dirtyRect) { //获取Text大小 Font font = new Font(FontFamily, (int)(FontAttributes.HasFlag(TextAttributes.Bold) ? FontWeight.Bold : FontWeight.Regular), (FontAttributes.HasFlag(TextAttributes.Italic) ? FontStyleType.Italic : FontStyleType.Normal)); size = canvas.GetStringSize(Text, font, FontSize, this.HorizontalAlignment, this.VerticalAlignment); canvas.Font = font; canvas.FontSize = FontSize; SizeF rc = GetSizeF(); // 处理阴影(先绘制) if (FontAttributes.HasFlag(TextAttributes.Shadow)) { canvas.FontColor = new Color(0, 0, 0, 0.5f); canvas.DrawString(Text, X + shadowOffset, Y + shadowOffset / 2, rc.Width, rc.Height, this.HorizontalAlignment, this.VerticalAlignment); } // 主文本 canvas.FontColor = FontColor; canvas.DrawString(Text, X, Y, rc.Width, rc.Height, this.HorizontalAlignment, this.VerticalAlignment); // 处理下划线 if (FontAttributes.HasFlag(TextAttributes.Underline)) { canvas.StrokeColor = StrokeColor; canvas.StrokeSize = StrokeThickness; canvas.DrawLine(X, Y + rc.Height, X + rc.Width, Y + rc.Height); } } private SizeF GetSizeF() { PointF[] points = GetPoints(); return new SizeF() { Width = (float)Math.Sqrt(DistanceSquare(points[0], points[1])), Height = (float)Math.Sqrt(DistanceSquare(points[0], points[3])) }; } }
Canvas类
[ContentProperty(nameof(Shapes))] public class Canvas : GraphicsView, IDrawable { public ObservableCollection<Shape> Shapes { get; set; } = new ObservableCollection<Shape>(); private RectangleShape selection = new RectangleShape() { IsSelected = false, StrokeDashPattern = "5 3", StrokeColor = Colors.Red }; private PointF v = PointF.Zero, w = PointF.Zero;//支持单指或者双指手势 public Canvas() { this.Drawable = this; this.StartInteraction += OnTouchStarted; this.DragInteraction += OnTouchMoved; this.EndInteraction += OnTouchEnded; } private void OnTouchStarted(object? sender, TouchEventArgs e) { if (e.Touches.Length == 0) return; else if (e.Touches.Length ==1) { v = e.Touches[0]; if (Shapes.Any((shape) => shape.HitTest(v) && shape.IsSelected)) return; } else if (e.Touches.Length == 2) { v = e.Touches[0]; w = e.Touches[1]; } foreach (Shape shape in Shapes) { if (e.Touches.Any((p) => shape.HitTest(p))) { shape.IsSelected = true; } else { shape.IsSelected = false; } } //如果没有任何选中且是单点,则启动选择框 if (!Shapes.Any((shape) => shape.IsSelected) && e.Touches.Length == 1) { selection.IsSelected = true; selection.X = e.Touches[0].X; selection.Y = e.Touches[0].Y; } } private void OnTouchMoved(object? sender, TouchEventArgs e) { if (e.Touches.Length == 0) return; else if (e.Touches.Length == 1) { //选择框 if (selection.IsSelected) { selection.Width = e.Touches[0].X - selection.X; selection.Height = e.Touches[0].Y - selection.Y; foreach (var shapre in Shapes) { if (selection.HitTest(shapre)) shapre.IsSelected = true; } } else { var delta = GetOffsetPoint(v, e.Touches[0]); foreach (var shape in Shapes) { if (shape.IsSelected) { shape.X += delta.X; shape.Y += delta.Y; } } v = e.Touches[0]; } } else if (e.Touches.Length == 2) { if (!selection.IsSelected) { PointF p3 = e.Touches[0], p4 = e.Touches[1]; float factor = GetZoomFactor(v, w, p3, p4); foreach (var shape in Shapes) { if (shape.IsSelected) { shape.X *= factor; shape.Y *= factor; } } v = p3; w = p4; } } this.Invalidate(); } private void OnTouchEnded(object? sender, TouchEventArgs e) { v = PointF.Zero; w = PointF.Zero; selection.IsSelected = false; this.Invalidate(); } private PointF GetOffsetPoint(PointF p1, PointF p2) { return new PointF(p2.X - p1.X, p2.Y - p1.Y); } private float GetZoomFactor(PointF p1, PointF p2, PointF p3, PointF p4) { float current = (float)Math.Sqrt(Shape.DistanceSquare(p3, p4)); float previous = (float)Math.Sqrt(Shape.DistanceSquare(p1, p2)); return previous == 0 ? 1 : current / previous; } public void Draw(ICanvas canvas, RectF dirtyRect) { foreach (var shape in Shapes) { // 绘制形状 shape.Draw(canvas, dirtyRect); } if (selection.IsSelected) selection.Draw(canvas, dirtyRect); } }
xmal使用,这里我创建了一个Canvas.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" x:Class="MauiViews.MauiDemos.Book._03.Canvas" Title="Canvas" WidthRequest="800" HeightRequest="800"> <Canvas> <RectangleShape FillColor="Blue" StrokeColor="Red" StrokeThickness="3" CornerRadius="20" X="50" Y="50" Width="150" Rotation="30"/> <EllipseShape X="300" Y="50" FillColor="Blue" StrokeColor="Red" StrokeThickness="3" RadiusX="80" Rotation="45"/> <TriangleShape X="500" Y="50" FillColor="Blue" StrokeColor="Red" StrokeThickness="3" Rotation="15"/> <PathShape X="50" Y="200" FillColor="Blue" StrokeColor="Red" StrokeThickness="3" ScaleX="0.8" Rotation="60" Data="M10 80 C 40 10, 65 10, 95 80 S 150 150, 180 80"/> <ImageShape X="150" Y="200" FillColor="Blue" StrokeColor="Red" StrokeThickness="3" Rotation="30" Width="200" AspectRatio="1.2" Source="dotnet_bot.png"/> <TextShape Text="Hello C# Maui,自定义" X="350" Y="250" FontAttributes="Italic,Bold,Underline,Shadow" Rotation="30" ScaleX="1.2" FontColor="Blue" StrokeColor="Red"/> </Canvas> </ContentPage>
运行效果。选中部分,部分不选中。虚框是外接矩形。