找回密码
 立即注册
首页 业界区 业界 05 - Multitouch/RoutedEvents例子 - 自己实现Canvas ...

05 - Multitouch/RoutedEvents例子 - 自己实现Canvas

啦迩 2025-6-30 14:13:55
文中例子是基于wpf Canvas写的,由于Maui还没有支持Canvas,所以顺手自己写一个。之前写了一个InkCanvas,发现扩展性太差了,这次写这个Canvas,彻底解决扩展性问题,支持自定义碰撞测试等。自己写的碰撞测试,是基于点集碰撞测试,可以处理任何点集,所以大家可以继承Shape类,写自己的Shape类。我抛砖引玉,写了几个常用的。Canvas目前支持的功能,单选,多选,单选移动,多选移动,二指手势缩放,多指手势选中。删除功能很简单,就不实现了。
Shape类以及子类扩展(ImageShape是一个非常有用的子类,里面有如何把ImageSource转换为IImage的代码),利用矩阵完成旋转,位移,缩放。把常见的实现放到了基类,这样子类可以专注StyleDraw的逻辑,而不用担心旋转等影响。
  1. //Shape基类
  2. public abstract class Shape : BindableObject
  3. {
  4.      public static readonly BindableProperty FillColorProperty =
  5.          BindableProperty.Create(nameof(FillColor), typeof(Color), typeof(Shape), Colors.Transparent);
  6.      public static readonly BindableProperty StrokeColorProperty =
  7.          BindableProperty.Create(nameof(StrokeColor), typeof(Color), typeof(Shape), Colors.Black);
  8.      public static readonly BindableProperty StrokeThicknessProperty =
  9.          BindableProperty.Create(nameof(StrokeThickness), typeof(float), typeof(Shape), 1f);
  10.      public static readonly BindableProperty XProperty =
  11.          BindableProperty.Create(nameof(X), typeof(float), typeof(Shape), 0f);
  12.      public static readonly BindableProperty YProperty =
  13.          BindableProperty.Create(nameof(Y), typeof(float), typeof(Shape), 0f);
  14.      public static readonly BindableProperty WidthProperty =
  15.          BindableProperty.Create(nameof(Width), typeof(float), typeof(Shape), 100f);
  16.      public static readonly BindableProperty HeightProperty =
  17.          BindableProperty.Create(nameof(Height), typeof(float), typeof(Shape), 100f);
  18.      public static readonly BindableProperty RotationProperty =
  19.          BindableProperty.Create(nameof(Rotation), typeof(float), typeof(Shape), 0f);
  20.      public static readonly BindableProperty ScaleXProperty =
  21.          BindableProperty.Create(nameof(ScaleX), typeof(float), typeof(Shape), 1f);
  22.      public static readonly BindableProperty ScaleYProperty =
  23.          BindableProperty.Create(nameof(ScaleY), typeof(float), typeof(Shape), 1f);
  24.      
  25.      public static readonly BindableProperty IsSelectedProperty =
  26.                      BindableProperty.Create(nameof(IsSelected), typeof(bool), typeof(Shape), false);
  27.      public static readonly BindableProperty StrokeDashPatternProperty =
  28.          BindableProperty.Create(nameof(StrokeDashPattern), typeof(string), typeof(RectangleShape), null);
  29.      public static readonly BindableProperty StrokeDashOffsetProperty =      
  30.          BindableProperty.Create(nameof(StrokeDashOffset), typeof(float), typeof(RectangleShape), 0f);
  31.      public static readonly BindableProperty AspectRatioProperty =
  32.          BindableProperty.Create(nameof(AspectRatio), typeof(float), typeof(Shape), 1f);
  33.      public Color FillColor
  34.      {
  35.          get => (Color)GetValue(FillColorProperty);
  36.          set => SetValue(FillColorProperty, value);
  37.      }
  38.      public Color StrokeColor
  39.      {
  40.          get => (Color)GetValue(StrokeColorProperty);
  41.          set => SetValue(StrokeColorProperty, value);
  42.      }
  43.      public float StrokeThickness
  44.      {
  45.          get => (float)GetValue(StrokeThicknessProperty);
  46.          set => SetValue(StrokeThicknessProperty, value);
  47.      }
  48.    
  49.      public float X
  50.      {
  51.          get => (float)GetValue(XProperty);
  52.          set => SetValue(XProperty, value);
  53.      }
  54.      public float Y
  55.      {
  56.          get => (float)GetValue(YProperty);
  57.          set => SetValue(YProperty, value);
  58.      }
  59.      public float Width
  60.      {
  61.          get => (float)GetValue(WidthProperty);
  62.          set => SetValue(WidthProperty, value);
  63.      }
  64.      public float Height
  65.      {
  66.          get => (float)GetValue(HeightProperty);
  67.          set => SetValue(HeightProperty, value);
  68.      }
  69.      public float Rotation
  70.      {
  71.          get => (float)GetValue(RotationProperty);
  72.          set => SetValue(RotationProperty, value);
  73.      }
  74.      public float ScaleX
  75.      {
  76.          get => (float)GetValue(ScaleXProperty);
  77.          set => SetValue(ScaleXProperty, value);
  78.      }
  79.      public float ScaleY
  80.      {
  81.          get => (float)GetValue(ScaleYProperty);
  82.          set => SetValue(ScaleYProperty, value);
  83.      }
  84.      public string StrokeDashPattern
  85.      {
  86.          get => (string)GetValue(StrokeDashPatternProperty);
  87.          set => SetValue(StrokeDashPatternProperty, value);
  88.      }
  89.      public float StrokeDashOffset
  90.      {
  91.          get => (float)GetValue(StrokeDashOffsetProperty);
  92.          set => SetValue(StrokeDashOffsetProperty, value);
  93.      }
  94.      public bool IsSelected
  95.      {
  96.          get => (bool)GetValue(IsSelectedProperty);
  97.          set => SetValue(IsSelectedProperty, value);
  98.      }
  99.      public float AspectRatio
  100.      {
  101.          get => (float)GetValue(AspectRatioProperty);
  102.          set => SetValue(AspectRatioProperty, value);
  103.      }
  104.      public RectF Bounds
  105.      {
  106.          get
  107.          {
  108.              // 使用局部坐标系(X, Y)为左上角的顶点
  109.              PointF[] points = this.GetPoints();
  110.              // 应用当前变换矩阵到所有顶点
  111.              Matrix3x2 transform = GetTransformMatrix();
  112.              for (int i = 0; i < points.Length; i++)
  113.              {
  114.                  points[i] = Transform(points[i], transform);
  115.              }
  116.              // 计算变换后顶点的边界框
  117.              // 计算变换后边界
  118.              float minX = points.Min(p => p.X);
  119.              float minY = points.Min(p => p.Y);
  120.              float maxX = points.Max(p => p.X);
  121.              float maxY = points.Max(p => p.Y);
  122.              return new RectF(minX, minY, maxX - minX, maxY - minY);
  123.          }
  124.      }
  125.      // 获取变换矩
  126.      protected Matrix3x2 GetTransformMatrix()
  127.      {
  128.          // 计算原始中心点(局部坐标系)
  129.          float centerX = X + Width / 2;
  130.          float centerY = Y + Height / 2;
  131.          // 构建变换矩阵
  132.          return
  133.              Matrix3x2.CreateScale(AspectRatio, AspectRatio) *
  134.              Matrix3x2.CreateRotation(Rotation * (MathF.PI / 180), new Vector2(centerX, centerY)) *
  135.              Matrix3x2.CreateScale(ScaleX, ScaleY);
  136.      }
  137.      //获取逆矩阵
  138.      protected Matrix3x2 GetInverseMatrix()
  139.      {
  140.          Matrix3x2.Invert(GetTransformMatrix(), out Matrix3x2 result);
  141.          return result;
  142.      }
  143.      public PointF Transform(PointF point, Matrix3x2 matrix)
  144.      {
  145.          return new PointF(
  146.              point.X * matrix.M11 + point.Y * matrix.M21 + matrix.M31,
  147.              point.X * matrix.M12 + point.Y * matrix.M22 + matrix.M32
  148.          );
  149.      }
  150.      //子类可重写
  151.      public virtual bool HitTest(PointF p, float tolerance = 5f)
  152.      {
  153.          PointF[] points = GetPoints();
  154.          Matrix3x2 transform = GetTransformMatrix();
  155.          for (int i = 0; i < points.Length; i++)
  156.          {
  157.              points[i] = Transform(points[i], transform);
  158.          }
  159.          //简单判断
  160.          if (Bounds.Contains(p))
  161.          {
  162.              //检查一个点或者两个点
  163.              if (points.Count() == 1)
  164.              {
  165.                  return Math.Sqrt(DistanceSquare(p, points[0])) <= tolerance;
  166.              }
  167.              else if (points.Count() == 2)
  168.              {
  169.                  return DistanceToSegment(p, points[0], points[1]) <= tolerance;
  170.              }
  171.              //点在形状类
  172.              return IsPointInPolygon(p, points) || IsPointNearPolygonEdge(p, points, tolerance);
  173.          }
  174.          return false;
  175.      }
  176.      public virtual bool HitTest(Shape other)
  177.      {
  178.          if (this.Bounds.IntersectsWith(other.Bounds))
  179.          {
  180.              // 使用局部坐标系(X, Y)为左上角的顶点
  181.              PointF[] pointsA = this.GetPoints();
  182.              PointF[] pointsB = other.GetPoints();
  183.              // 应用当前变换矩阵到所有顶点
  184.              Matrix3x2 transformA = GetTransformMatrix();
  185.              Matrix3x2 transformB = other.GetTransformMatrix();
  186.              for (int i = 0; i < pointsA.Length; i++)
  187.              {
  188.                  pointsA[i] = Transform(pointsA[i], transformA);
  189.              }
  190.              for (int i = 0; i < pointsB.Length; i++)
  191.              {
  192.                  pointsB[i] = Transform(pointsB[i], transformB);
  193.              }
  194.              return PolygonIntersects(pointsA, pointsB);
  195.          }
  196.          return false;
  197.      }
  198.      //形状到形状 : 检测两个多边形是否相交
  199.      public static bool PolygonIntersects(PointF[] polyA, PointF[] polyB)
  200.      {
  201.          // 检测polyA的边是否与polyB相交
  202.          for (int i = 0; i < polyA.Length; i++)
  203.          {
  204.              int nextI = (i + 1) % polyA.Length;
  205.              for (int j = 0; j < polyB.Length; j++)
  206.              {
  207.                  int nextJ = (j + 1) % polyB.Length;
  208.                  if (LinesIntersect(polyA[i], polyA[nextI], polyB[j], polyB[nextJ]))
  209.                      return true;
  210.              }
  211.          }
  212.          // 检测一个多边形是否完全包含在另一个多边形中
  213.          if (IsPointInPolygon(polyA[0], polyB) || IsPointInPolygon(polyB[0], polyA))
  214.              return true;
  215.          return false;
  216.      }
  217.      //点到点
  218.      public static float DistanceSquare(PointF v, PointF w)
  219.      {
  220.          return (v.X - w.X) * (v.X - w.X) + (v.Y - w.Y) * (v.Y - w.Y);
  221.      }
  222.      //点到线
  223.      public static float DistanceToSegment(PointF p, PointF v, PointF w)
  224.      {
  225.          float l2 = (v.X - w.X) * (v.X - w.X) + (v.Y - w.Y) * (v.Y - w.Y);
  226.          if (l2 == 0.0)
  227.              return (float)Math.Sqrt(DistanceSquare(p, v));
  228.          float t = Math.Max(0, Math.Min(1,
  229.              ((p.X - v.X) * (w.X - v.X) + (p.Y - v.Y) * (w.Y - v.Y)) / l2));
  230.          PointF projection = new PointF(
  231.              v.X + t * (w.X - v.X),
  232.              v.Y + t * (w.Y - v.Y));
  233.          return (float)Math.Sqrt(DistanceSquare(p, projection));
  234.      }
  235.      // 射线法判断点是否在多边形内部,默认是闭合路径
  236.      public static bool IsPointInPolygon(PointF p, PointF[] polygon)
  237.      {
  238.          if (polygon.Length < 3) return false;
  239.          bool inside = false;
  240.          int j = polygon.Length - 1;
  241.          for (int i = 0; i < polygon.Length; i++)
  242.          {
  243.              if ((polygon[i].Y > p.Y) != (polygon[j].Y > p.Y) &&
  244.                  p.X < (polygon[j].X - polygon[i].X) * (p.Y - polygon[i].Y) /
  245.                  (polygon[j].Y - polygon[i].Y) + polygon[i].X)
  246.              {
  247.                  inside = !inside;
  248.              }
  249.              j = i;
  250.          }
  251.          return inside;
  252.      }
  253.      // 判断点是否在多边形边线附近
  254.      public static bool IsPointNearPolygonEdge(PointF p, PointF[] points, float tolerance)
  255.      {
  256.          if (points.Length < 2)
  257.              return false;
  258.          for (int i = 0; i < points.Length; i++)
  259.          {
  260.              int next = (i + 1) % points.Length;
  261.              float distance = DistanceToSegment(p, points[i], points[next]);
  262.              if (distance <= tolerance)
  263.                  return true;
  264.          }
  265.          return false;
  266.      }
  267.      //检测两条线段是否相交
  268.      public static bool LinesIntersect(PointF a1, PointF a2, PointF b1, PointF b2)
  269.      {
  270.          float d = (b2.Y - b1.Y) * (a2.X - a1.X) - (b2.X - b1.X) * (a2.Y - a1.Y);
  271.          if (d == 0)
  272.              return false; // 平行线
  273.          float uA = ((b2.X - b1.X) * (a1.Y - b1.Y) - (b2.Y - b1.Y) * (a1.X - b1.X)) / d;
  274.          float uB = ((a2.X - a1.X) * (a1.Y - b1.Y) - (a2.Y - a1.Y) * (a1.X - b1.X)) / d;
  275.          return uA >= 0 && uA <= 1 && uB >= 0 && uB <= 1;
  276.      }
  277.      public void Draw(ICanvas canvas, RectF dirtyRect)
  278.      {
  279.          canvas.SaveState();
  280.          canvas.FillColor = FillColor;
  281.          canvas.StrokeColor = StrokeColor;
  282.          canvas.StrokeSize = StrokeThickness;
  283.          canvas.StrokeDashPattern = StrokeDashPattern?.Split(" ").Select(s => float.Parse(s)).ToArray();
  284.          canvas.StrokeDashOffset = StrokeDashOffset;
  285.          //测试点击区域
  286.          if (IsSelected)
  287.          {
  288.              canvas.SaveState();
  289.              RectF bounds = this.Bounds;
  290.              canvas.StrokeColor = Colors.Gray;
  291.              canvas.StrokeSize = 1f;
  292.              canvas.StrokeDashPattern = new float[] { 5, 3 };
  293.              canvas.DrawRectangle(bounds);
  294.              canvas.RestoreState();
  295.          }
  296.          canvas.ConcatenateTransform(this.GetTransformMatrix());
  297.          StyleDraw(canvas, dirtyRect);
  298.          canvas.RestoreState();
  299.      }
  300.      //点集到线
  301.      public static PathF CreatePathF(PointF[] points, bool closed = true)
  302.      {
  303.          if (points.Length == 0)
  304.              return new PathF();
  305.          PathF path = new PathF();
  306.          path.MoveTo(points[0]);
  307.          for (int i = 1; i < points.Length; i++)
  308.          {
  309.              path.LineTo(points[i]);
  310.          }
  311.          if (closed)
  312.              path.Close();
  313.          return path;
  314.      }
  315.      protected abstract void StyleDraw(ICanvas canvas, RectF dirtyRect);
  316.      protected abstract PointF[] GetPoints();
  317. }
  318. //长方形类
  319. public class RectangleShape : Shape
  320. {
  321.      public static readonly BindableProperty CornerRadiusProperty =
  322.          BindableProperty.Create(nameof(CornerRadius), typeof(float), typeof(RectangleShape), 0f);
  323.      public float CornerRadius
  324.      {
  325.          get => (float)GetValue(CornerRadiusProperty);
  326.          set => SetValue(CornerRadiusProperty, value);
  327.      }
  328.      protected override PointF[] GetPoints()
  329.      {
  330.          return new PointF[]
  331.          {
  332.              new PointF(X, Y),                 // 左上      
  333.              new PointF(X + Width, Y),             // 右上
  334.              new PointF(X + Width, Y + Height),        // 右下                                               
  335.              new PointF(X, Y + Height)             // 左下
  336.          };
  337.      }
  338.      protected override void StyleDraw(ICanvas canvas, RectF dirtyRect)
  339.      {
  340.          // 绘制原始矩形(局部坐标)
  341.          canvas.FillRoundedRectangle(X, Y, Width, Height, CornerRadius);
  342.          canvas.DrawRoundedRectangle(X, Y, Width, Height, CornerRadius);
  343.      }
  344. }
  345. //椭圆形类
  346. public class EllipseShape : Shape
  347. {
  348.      //Length越大,性能要求越高,但是碰撞判断越精确。
  349.      public static readonly BindableProperty LengthProperty =
  350.          BindableProperty.Create(nameof(Length), typeof(int), typeof(EllipseShape), 60);
  351.      public static readonly BindableProperty RadiusXProperty =
  352.          BindableProperty.Create(nameof(RadiusX), typeof(float), typeof(EllipseShape), 0f);
  353.      public static readonly BindableProperty RadiusYProperty =
  354.          BindableProperty.Create(nameof(RadiusY), typeof(float), typeof(EllipseShape), 0f);
  355.      public int Length
  356.      {
  357.          get => (int)GetValue(LengthProperty);
  358.          set => SetValue(LengthProperty, value);
  359.      }
  360.      public float RadiusX
  361.      {
  362.          get => (float)GetValue(RadiusXProperty);
  363.          set =>  SetValue(RadiusXProperty, value);
  364.      }
  365.      public float RadiusY
  366.      {
  367.          get => (float)GetValue(RadiusYProperty);
  368.          set => SetValue(RadiusYProperty, value);
  369.      }
  370.      protected override PointF[] GetPoints()
  371.      {
  372.          List<PointF> points = new List<PointF>();
  373.          float radiusX = RadiusX == 0 ? Width / 2 : RadiusX;
  374.          float radiusY = RadiusY == 0 ? Height / 2 : RadiusY;
  375.          float centerX = X + radiusX;
  376.          float centerY = Y + radiusY;
  377.          for (int i = 0; i < Length; i++)
  378.          {
  379.              float angle = i * (float)Math.PI / Length * 2f;
  380.              points.Add(new PointF(
  381.                  centerX + radiusX * (float)Math.Cos(angle),
  382.                  centerY + radiusY * (float)Math.Sin(angle)));
  383.          }
  384.          return points.ToArray();
  385.      }
  386.      protected override void StyleDraw(ICanvas canvas, RectF dirtyRect)
  387.      {
  388.          float radiusX = RadiusX == 0 ? Width / 2 : RadiusX;
  389.          float radiusY = RadiusY == 0 ? Height / 2 : RadiusY;
  390.          canvas.FillEllipse(X, Y, radiusX * 2, radiusY * 2);
  391.          canvas.DrawEllipse(X, Y, radiusX * 2, radiusY * 2);
  392.      }
  393. }
  394. //三角形类
  395. public class TriangleShape : Shape
  396. {
  397.      protected override PointF[] GetPoints()
  398.      {
  399.          return new PointF[]
  400.          {
  401.              new PointF(X + Width / 2, Y),    // 顶点
  402.              new PointF(X + Width, Y + Height), // 右下角      
  403.              new PointF(X, Y + Height)         // 左下角
  404.          };
  405.      }
  406.      protected override void StyleDraw(ICanvas canvas, RectF dirtyRect)
  407.      {
  408.          PathF path = CreatePathF(GetPoints());
  409.          canvas.FillPath(path);
  410.          canvas.DrawPath(path);
  411.      }
  412. }
  413. //线段或自定义类,支持SVG等
  414. public class PathShape : Shape
  415. {
  416.      public static readonly BindableProperty DataProperty =      
  417.          BindableProperty.Create(nameof(Data), typeof(string), typeof(PathShape), null);
  418.      public static readonly BindableProperty IsClosedPathProperty =
  419.          BindableProperty.Create(nameof(IsClosedPath), typeof(bool), typeof(PathShape), true);
  420.      public string Data
  421.      {
  422.          get => (string)GetValue(DataProperty);
  423.          set => SetValue(DataProperty, value);
  424.      }
  425.      public bool IsClosedPath
  426.      {
  427.          get => (bool)GetValue(IsClosedPathProperty);
  428.          set => SetValue(IsClosedPathProperty, value);
  429.      }
  430.      protected override PointF[] GetPoints()
  431.      {
  432.          if (Data != null)
  433.          {
  434.              PointF[] points = PathBuilder.Build(Data).Points.ToArray();
  435.              for (int i = 0; i < points.Length; i++)
  436.              {
  437.                  points[i].X += X;
  438.                  points[i].Y += Y;
  439.              }
  440.              return points;
  441.          }
  442.          return Array.Empty<PointF>();
  443.      }
  444.      protected override void StyleDraw(ICanvas canvas, RectF dirtyRect)
  445.      {
  446.          PathF path = CreatePathF(GetPoints(), IsClosedPath);
  447.          canvas.FillPath(path);
  448.          canvas.DrawPath(path);
  449.      }
  450.      //分割线段
  451.      public List<PointF[]> SplitAt(PointF point, float tolerance = 5f)
  452.      {
  453.          PointF[] points = GetPoints();
  454.          // 应用当前变换矩阵到所有顶点
  455.          Matrix3x2 transform = GetTransformMatrix();
  456.          for (int i = 0; i < points.Length; i++)
  457.          {
  458.              points[i] = Transform(points[i], transform);
  459.          }
  460.          List<PointF[]> result = new List<PointF[]>();
  461.          if (points.Length < 2)
  462.          {
  463.              // 点太少无法分割
  464.              return result;
  465.          }
  466.          // 1. 查找最近的线段和分割点
  467.          float minDistance = float.MaxValue;
  468.          int splitIndex = -1;
  469.          PointF splitPoint = PointF.Zero;
  470.          bool isClosingSegment = false;
  471.          // 检查所有线段(包括可能的闭合线段)
  472.          for (int i = 0; i < points.Length - 1; i++)
  473.          {
  474.              CheckSegment(points[i], points[i + 1], i, ref minDistance, ref splitIndex, ref splitPoint, point);
  475.          }
  476.          // 如果是闭合路径,检查最后一段(从最后一个点到第一个点)
  477.          if (IsClosedPath && points.Length > 1)
  478.          {
  479.              isClosingSegment = CheckSegment(points[points.Length - 1], points[0],
  480.                                             points.Length - 1,
  481.                                             ref minDistance, ref splitIndex,
  482.                                             ref splitPoint, point);
  483.          }
  484.          // 2. 如果没有找到在容差范围内的分割点
  485.          if (minDistance > tolerance || splitIndex == -1)
  486.          {
  487.              return result;
  488.          }
  489.          // 3. 执行分割
  490.          if (isClosingSegment)
  491.          {
  492.              // 在闭合线段上分割
  493.              SplitClosingSegment(points, splitPoint, result);
  494.          }
  495.          else
  496.          {
  497.              // 在普通线段上分割
  498.              SplitRegularSegment(points, splitIndex, splitPoint, result);
  499.          }
  500.          return result;
  501.      }
  502.      private bool CheckSegment(PointF a, PointF b, int index,
  503.                              ref float minDistance, ref int splitIndex,
  504.                              ref PointF splitPoint, PointF testPoint)
  505.      {
  506.          float distance;
  507.          PointF projection = GetProjectionOnSegment(testPoint, a, b, out distance);
  508.          if (distance < minDistance)
  509.          {
  510.              minDistance = distance;
  511.              splitIndex = index;
  512.              splitPoint = projection;
  513.              return true;
  514.          }
  515.          return false;
  516.      }
  517.      private PointF GetProjectionOnSegment(PointF p, PointF a, PointF b, out float distance)
  518.      {
  519.          Vector2 ap = new Vector2(p.X - a.X, p.Y - a.Y);
  520.          Vector2 ab = new Vector2(b.X - a.X, b.Y - a.Y);
  521.          float magnitude = ab.LengthSquared();
  522.          if (magnitude == 0)
  523.          {
  524.              distance = (float)Math.Sqrt(DistanceSquare(p, a));
  525.              return a;
  526.          }
  527.          float t = Math.Clamp(Vector2.Dot(ap, ab) / magnitude, 0, 1);
  528.          PointF projection = new PointF(
  529.              a.X + t * ab.X,
  530.              a.Y + t * ab.Y
  531.          );
  532.          distance = (float)Math.Sqrt(DistanceSquare(p, projection));
  533.          return projection;
  534.      }
  535.      private void SplitRegularSegment(PointF[] points, int splitIndex,
  536.                                     PointF splitPoint, List<PointF[]> result)
  537.      {
  538.          // 第一部分:起点到分割点
  539.          List<PointF> part1 = new List<PointF>();
  540.          for (int i = 0; i <= splitIndex; i++)
  541.          {
  542.              part1.Add(points[i]);
  543.          }
  544.          part1.Add(splitPoint);
  545.          // 第二部分:分割点到终点
  546.          List<PointF> part2 = new List<PointF>();
  547.          part2.Add(splitPoint);
  548.          for (int i = splitIndex + 1; i < points.Length; i++)
  549.          {
  550.              part2.Add(points[i]);
  551.          }
  552.          result.Add(part1.ToArray());
  553.          result.Add(part2.ToArray());
  554.      }
  555.      private void SplitClosingSegment(PointF[] points, PointF splitPoint, List<PointF[]> result)
  556.      {
  557.          // 第一部分:起点到最后一个点 + 分割点
  558.          List<PointF> part1 = new List<PointF>(points);
  559.          part1.Add(splitPoint);
  560.          // 第二部分:分割点到起点
  561.          List<PointF> part2 = new List<PointF>();
  562.          part2.Add(splitPoint);
  563.          part2.Add(points[0]);
  564.          result.Add(part1.ToArray());
  565.          result.Add(part2.ToArray());
  566.      }
  567. }
  568. //图片
  569. public class ImageShape : Shape
  570. {
  571.      public static readonly BindableProperty SourceProperty =
  572.          BindableProperty.Create(nameof(Source), typeof(ImageSource), typeof(ImageShape), null);
  573.      public ImageSource Source
  574.      {
  575.          get => (ImageSource)GetValue(SourceProperty);
  576.          set => SetValue(SourceProperty, value);
  577.      }
  578.      private IImage? image;
  579.      public ImageShape()
  580.      {
  581.          Dispatcher.Dispatch(() =>
  582.          {
  583.              image = ConvertImageSourceToIImage(Source);
  584.          });
  585.      }
  586.      protected override PointF[] GetPoints()
  587.      {
  588.          return new PointF[]
  589.          {
  590.              new PointF(X, Y),                 // 左上      
  591.              new PointF(X + Width, Y),             // 右上
  592.              new PointF(X + Width, Y + Height),        // 右下                                               
  593.              new PointF(X, Y + Height)             // 左下
  594.          };
  595.      }
  596.      protected override void StyleDraw(ICanvas canvas, RectF dirtyRect)
  597.      {
  598.          if (image != null)
  599.              canvas.DrawImage(image, X, Y, Width, Height);
  600.      }
  601.      public static IImage? ConvertImageSourceToIImage(ImageSource imageSource)
  602.      {
  603.          try
  604.          {
  605.              // 1. 将 ImageSource 转换为 Stream
  606.              Stream? stream = GetStreamFromImageSource(imageSource);
  607.              // 2. 使用 PlatformImage 加载流
  608.              return PlatformImage.FromStream(stream);
  609.          }
  610.          catch (Exception ex)
  611.          {
  612.              Trace.WriteLine($"转换失败: {ex.Message}");
  613.              return null;
  614.          }
  615.      }
  616.      private static Stream? GetStreamFromImageSource(ImageSource imageSource)
  617.      {
  618.          if (imageSource is FileImageSource fileSource)
  619.          {
  620.              // 资源一定是"嵌入的资源"
  621.              Assembly assembly = Shell.Current.GetType().GetTypeInfo().Assembly;
  622.              return assembly.GetManifestResourceStream(assembly.FullName?.Split(',').First() + ".Resources.Images." + fileSource.File);
  623.          }
  624.          else if (imageSource is StreamImageSource streamSource)
  625.          {
  626.              // 处理流
  627.              return streamSource.Stream(CancellationToken.None).Result;
  628.          }
  629.          else if (imageSource is UriImageSource uriSource)
  630.          {
  631.              // 处理网络图片
  632.              using var httpClient = new HttpClient();
  633.              var response = httpClient.GetAsync(uriSource.Uri).Result;
  634.              return response.Content.ReadAsStreamAsync().Result;
  635.          }
  636.          Trace.WriteLine("不支持的ImageSource类型");
  637.          return null;
  638.      }
  639. }
  640. //文字
  641. public class TextShape : Shape
  642. {
  643.      [Flags]
  644.      public enum TextAttributes
  645.      {
  646.          None = 0,
  647.          Bold = 1 << 0,
  648.          Italic = 1 << 1,
  649.          Underline = 1 << 2,
  650.          Shadow = 1 << 3,
  651.      }
  652.      public static readonly BindableProperty TextProperty =
  653.          BindableProperty.Create(nameof(Text), typeof(string), typeof(TextShape), null);
  654.      public static readonly BindableProperty FontSizeProperty =
  655.          BindableProperty.Create(nameof(FontSize), typeof(float), typeof(TextShape), 16f);
  656.      public static readonly BindableProperty FontColorProperty =
  657.          BindableProperty.Create(nameof(FontColor), typeof(Color), typeof(TextShape), Colors.Black);
  658.      public static readonly BindableProperty FontFamilyProperty =
  659.          BindableProperty.Create(nameof(FontFamily), typeof(string), typeof(TextShape), "Arial");
  660.      public static readonly BindableProperty FontAttributesProperty =
  661.          BindableProperty.Create(nameof(FontAttributes), typeof(TextAttributes), typeof(TextShape), TextAttributes.None);
  662.      public static readonly BindableProperty HorizontalAlignmentProperty =
  663.          BindableProperty.Create(nameof(HorizontalAlignment), typeof(HorizontalAlignment), typeof(TextShape), HorizontalAlignment.Left);
  664.      public static readonly BindableProperty VerticalAlignmentProperty =
  665.          BindableProperty.Create(nameof(VerticalAlignment), typeof(VerticalAlignment), typeof(TextShape), VerticalAlignment.Center);
  666.      public string Text
  667.      {
  668.          get => (string)GetValue(TextProperty);
  669.          set => SetValue(TextProperty, value);
  670.      }
  671.      public float FontSize
  672.      {
  673.          get => (float)GetValue(FontSizeProperty);
  674.          set => SetValue(FontSizeProperty, value);
  675.      }
  676.      public Color FontColor
  677.      {
  678.          get => (Color)GetValue(FontColorProperty);
  679.          set => SetValue(FontColorProperty, value);
  680.      }
  681.      public string FontFamily
  682.      {
  683.          get => (string)GetValue(FontFamilyProperty);
  684.          set => SetValue(FontFamilyProperty, value);
  685.      }
  686.      public TextAttributes FontAttributes
  687.      {
  688.          get => (TextAttributes)GetValue(FontAttributesProperty);
  689.          set => SetValue(FontAttributesProperty, value);
  690.      }
  691.      public HorizontalAlignment HorizontalAlignment
  692.      {
  693.          get => (HorizontalAlignment)GetValue(HorizontalAlignmentProperty);
  694.          set => SetValue(HorizontalAlignmentProperty, value);
  695.      }
  696.      public VerticalAlignment VerticalAlignment
  697.      {
  698.          get => (VerticalAlignment)GetValue(VerticalAlignmentProperty);
  699.          set => SetValue(VerticalAlignmentProperty, value);
  700.      }
  701.      private SizeF size = SizeF.Zero;
  702.      private const float shadowOffset = 2;
  703.      protected override PointF[] GetPoints()
  704.      {
  705.          //canvas.GetStringSize存在bug,长宽反了
  706.          float w = size.Height, h = size.Width * 1.2f;
  707.          return new PointF[]
  708.            {
  709.                  new PointF(X, Y),                 // 左上      
  710.                  new PointF(X + w, Y),             // 右上
  711.                  new PointF(X + w, Y + h),        // 右下                                               
  712.                  new PointF(X, Y + h)             // 左下      
  713.            };
  714.      }
  715.      protected override void StyleDraw(ICanvas canvas, RectF dirtyRect)
  716.      {
  717.          //获取Text大小
  718.          Font font = new Font(FontFamily,
  719.              (int)(FontAttributes.HasFlag(TextAttributes.Bold) ? FontWeight.Bold : FontWeight.Regular),
  720.                (FontAttributes.HasFlag(TextAttributes.Italic) ? FontStyleType.Italic : FontStyleType.Normal));
  721.          size = canvas.GetStringSize(Text, font, FontSize, this.HorizontalAlignment, this.VerticalAlignment);
  722.          canvas.Font = font;
  723.          canvas.FontSize = FontSize;
  724.          SizeF rc = GetSizeF();
  725.          // 处理阴影(先绘制)
  726.          if (FontAttributes.HasFlag(TextAttributes.Shadow))
  727.          {
  728.              canvas.FontColor = new Color(0, 0, 0, 0.5f);
  729.              canvas.DrawString(Text, X + shadowOffset, Y + shadowOffset / 2, rc.Width, rc.Height,
  730.                  this.HorizontalAlignment, this.VerticalAlignment);
  731.          }
  732.          // 主文本
  733.          canvas.FontColor = FontColor;
  734.          canvas.DrawString(Text, X, Y, rc.Width, rc.Height, this.HorizontalAlignment, this.VerticalAlignment);
  735.          // 处理下划线
  736.          if (FontAttributes.HasFlag(TextAttributes.Underline))
  737.          {
  738.              canvas.StrokeColor = StrokeColor;
  739.              canvas.StrokeSize = StrokeThickness;
  740.              canvas.DrawLine(X, Y + rc.Height, X + rc.Width, Y + rc.Height);
  741.          }
  742.      }
  743.      private SizeF GetSizeF()
  744.      {
  745.          PointF[] points = GetPoints();
  746.          return new SizeF()
  747.          {
  748.              Width = (float)Math.Sqrt(DistanceSquare(points[0], points[1])),
  749.              Height = (float)Math.Sqrt(DistanceSquare(points[0], points[3]))
  750.          };
  751.      }
  752. }
复制代码
Canvas类
  1. [ContentProperty(nameof(Shapes))]
  2. public class Canvas : GraphicsView, IDrawable
  3. {
  4.     public ObservableCollection<Shape> Shapes { get; set; } = new ObservableCollection<Shape>();
  5.     private RectangleShape selection = new RectangleShape()
  6.     {
  7.         IsSelected = false,
  8.         StrokeDashPattern = "5 3",
  9.         StrokeColor = Colors.Red
  10.     };
  11.     private PointF v = PointF.Zero, w = PointF.Zero;//支持单指或者双指手势
  12.     public Canvas()
  13.     {
  14.         this.Drawable = this;
  15.         this.StartInteraction += OnTouchStarted;
  16.         this.DragInteraction += OnTouchMoved;
  17.         this.EndInteraction += OnTouchEnded;
  18.     }
  19.     private void OnTouchStarted(object? sender, TouchEventArgs e)
  20.     {
  21.         if (e.Touches.Length == 0)
  22.             return;
  23.         else if (e.Touches.Length ==1)
  24.         {
  25.             v = e.Touches[0];
  26.             if (Shapes.Any((shape) => shape.HitTest(v) && shape.IsSelected))
  27.                 return;
  28.         }
  29.         else if (e.Touches.Length == 2)
  30.         {
  31.             v = e.Touches[0];
  32.             w = e.Touches[1];
  33.         }
  34.         foreach (Shape shape in Shapes)
  35.         {
  36.             if (e.Touches.Any((p) => shape.HitTest(p)))
  37.             {
  38.                 shape.IsSelected = true;
  39.             }
  40.             else
  41.             {
  42.                 shape.IsSelected = false;
  43.             }
  44.         }
  45.         //如果没有任何选中且是单点,则启动选择框
  46.         if (!Shapes.Any((shape) => shape.IsSelected) && e.Touches.Length == 1)
  47.         {
  48.             selection.IsSelected = true;
  49.             selection.X = e.Touches[0].X;
  50.             selection.Y = e.Touches[0].Y;
  51.         }
  52.     }
  53.     private void OnTouchMoved(object? sender, TouchEventArgs e)
  54.     {
  55.         if (e.Touches.Length == 0)
  56.             return;
  57.         else if (e.Touches.Length == 1)
  58.         {
  59.             //选择框
  60.             if (selection.IsSelected)
  61.             {
  62.                 selection.Width = e.Touches[0].X - selection.X;
  63.                 selection.Height = e.Touches[0].Y - selection.Y;
  64.                 foreach (var shapre in Shapes)
  65.                 {
  66.                     if (selection.HitTest(shapre))
  67.                         shapre.IsSelected = true;
  68.                 }
  69.             }
  70.             else
  71.             {
  72.                 var delta = GetOffsetPoint(v, e.Touches[0]);
  73.                 foreach (var shape in Shapes)
  74.                 {
  75.                     if (shape.IsSelected)
  76.                     {
  77.                         shape.X += delta.X;
  78.                         shape.Y += delta.Y;
  79.                     }
  80.                 }
  81.                 v = e.Touches[0];
  82.             }
  83.         }
  84.         else if (e.Touches.Length == 2)
  85.         {
  86.             if (!selection.IsSelected)
  87.             {
  88.                 PointF p3 = e.Touches[0], p4 = e.Touches[1];
  89.                 float factor = GetZoomFactor(v, w, p3, p4);
  90.                 foreach (var shape in Shapes)
  91.                 {
  92.                     if (shape.IsSelected)
  93.                     {
  94.                         shape.X *= factor;
  95.                         shape.Y *= factor;
  96.                     }
  97.                 }
  98.                 v = p3;
  99.                 w = p4;
  100.             }
  101.         }
  102.         
  103.         this.Invalidate();
  104.     }
  105.     private void OnTouchEnded(object? sender, TouchEventArgs e)
  106.     {
  107.         v = PointF.Zero;
  108.         w = PointF.Zero;
  109.         selection.IsSelected = false;
  110.         this.Invalidate();
  111.     }
  112.     private PointF GetOffsetPoint(PointF p1, PointF p2)
  113.     {
  114.         return new PointF(p2.X - p1.X, p2.Y - p1.Y);
  115.     }
  116.     private float GetZoomFactor(PointF p1, PointF p2, PointF p3, PointF p4)
  117.     {
  118.         float current = (float)Math.Sqrt(Shape.DistanceSquare(p3, p4));
  119.         float previous = (float)Math.Sqrt(Shape.DistanceSquare(p1, p2));
  120.         return previous == 0 ? 1 : current / previous;
  121.     }
  122.     public void Draw(ICanvas canvas, RectF dirtyRect)
  123.     {
  124.         foreach (var shape in Shapes)
  125.         {
  126.             // 绘制形状
  127.             shape.Draw(canvas, dirtyRect);
  128.         }
  129.         if (selection.IsSelected)
  130.             selection.Draw(canvas, dirtyRect);
  131.     }
  132. }
复制代码
xmal使用,这里我创建了一个Canvas.xaml。
  1. <?xml version="1.0" encoding="utf-8" ?>
  2. <ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
  3.              xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
  4.              x:Class="MauiViews.MauiDemos.Book._03.Canvas"
  5.              Title="Canvas" WidthRequest="800" HeightRequest="800">
  6.     <Canvas>
  7.         <RectangleShape FillColor="Blue" StrokeColor="Red" StrokeThickness="3" CornerRadius="20"                       
  8.                 X="50" Y="50" Width="150" Rotation="30"/>
  9.         <EllipseShape X="300" Y="50" FillColor="Blue" StrokeColor="Red" StrokeThickness="3" RadiusX="80" Rotation="45"/>
  10.         <TriangleShape X="500" Y="50" FillColor="Blue" StrokeColor="Red" StrokeThickness="3" Rotation="15"/>
  11.         <PathShape X="50" Y="200" FillColor="Blue" StrokeColor="Red" StrokeThickness="3"
  12.                    ScaleX="0.8" Rotation="60"
  13.                    Data="M10 80 C 40 10, 65 10, 95 80 S 150 150, 180 80"/>
  14.         <ImageShape X="150" Y="200" FillColor="Blue" StrokeColor="Red" StrokeThickness="3"               
  15.                     Rotation="30" Width="200" AspectRatio="1.2"
  16.                     Source="dotnet_bot.png"/>
  17.         <TextShape Text="Hello C# Maui,自定义" X="350" Y="250" FontAttributes="Italic,Bold,Underline,Shadow"
  18.                    Rotation="30" ScaleX="1.2"
  19.                    FontColor="Blue" StrokeColor="Red"/>
  20.     </Canvas>
  21. </ContentPage>
复制代码
运行效果。选中部分,部分不选中。虚框是外接矩形。
1.png


来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
您需要登录后才可以回帖 登录 | 立即注册