Wpf中绘制带箭头方向的贝塞尔曲线(可拖动实时更新)

 效果(代码在最下面)

只需要如下引用即可

BsControl.Draw(canvas, startControl, endControl);

 补充说明:

canvas:控件所在的画布

startControl:贝塞尔曲线起点

endControl:贝塞尔曲线终点

PS:不懂的可以来B站问我。https://space.bilibili.com/33526379/dynamicicon-default.png?t=N7T8https://space.bilibili.com/33526379/dynamic

另外,欢迎支持我的个人开源项目:GitHub - fhhyyp/serein-flow: 一款基于WPF(Dotnet 8)的流程可视化编辑器(需二次开发) ----- a process visual editor based on WPF (Dotnet 8) (secondary development required)一款基于WPF(Dotnet 8)的流程可视化编辑器(需二次开发) ----- a process visual editor based on WPF (Dotnet 8) (secondary development required) - fhhyyp/serein-flowicon-default.png?t=N7T8https://github.com/fhhyyp/serein-flow

以下是绘制贝塞尔曲线的代码:


    #region 创建两个控件之间的连接关系,在UI层面上显示为 带箭头指向的贝塞尔曲线


    public static class BsControl
    {
        public static Connection Draw(Canvas canvas, FrameworkElement sele, FrameworkElement eele)
        {
            Connection cc = new()
            {
                Start = sele,
                End = eele,
            };
            UpBsPath(canvas, cc);
            SetEvent(canvas, cc, cc.Start);
            SetEvent(canvas, cc, cc.End);
            cc.IsSet = true;
            return cc;
        } 
        private static Point clickEvent; // 当前点击事件
        private static bool isD = false; // 是否正在移动控件
        private static bool isUp = false; // 是否正在更新线条显示

        private static void SetEvent(Canvas canvas, Connection cc, UIElement uiele)
        {
            if (cc.IsSet) return; 
            uiele.MouseLeftButtonDown += (sender, e) => { isD = true; clickEvent = e.GetPosition(uiele);  uiele.CaptureMouse(); };

            uiele.MouseMove += (sender, e) =>
            {
                if (isD && VisualTreeHelper.GetParent(uiele) is Canvas canvas)
                {
                    var cp = e.GetPosition(canvas); 
                    var nl = cp.X - clickEvent.X;
                    var nt = cp.Y - clickEvent.Y;
                    Canvas.SetLeft(uiele, nl); Canvas.SetTop(uiele, nt); UpBsPath(canvas, cc);
                }
            };
            uiele.MouseLeftButtonUp += (sender, e) => { isD = false; uiele.ReleaseMouseCapture(); };
        }


        // 拖动时重新绘制
        public static void UpBsPath(Canvas canvas, Connection cc)
        {
            if (isUp)  return;
            isUp = true;

            canvas.Dispatcher.InvokeAsync(() =>
            {
                if (cc != null && cc.BsPath == null)
                {
                    cc.BsPath = new Path { Stroke = Brushes.Teal, StrokeThickness = 1 };
                    Canvas.SetZIndex(cc.BsPath, -1);
                    canvas.Children.Add(cc.BsPath);
                }

                if (cc != null && cc.ApPath == null)
                {
                    cc.ApPath = new Path { Stroke = Brushes.Teal, Fill = Brushes.Teal, StrokeThickness = 1 };
                    Canvas.SetZIndex(cc.ApPath, -1);
                    canvas.Children.Add(cc.ApPath);
                }

                BezierLineDrawer.UpLine(canvas, cc.Start, cc.End, cc.BsPath, cc.ApPath);
                isUp = false;
            });
        }
    }


    public class Connection
    {
        public bool IsSet { get; set; }
        public Path BsPath { get; set; }// 贝塞尔曲线路径
        public Path ApPath { get; set; } // 箭头路径
        public required FrameworkElement Start { get; set; } // 起始
        public required FrameworkElement End { get; set; }   // 结束

        public void Remove(Canvas canvas)
        {
            canvas.Children.Remove(BsPath); // 移除线
            canvas.Children.Remove(ApPath); // 移除箭头
        }
    }

    public class BezierLineDrawer
    {
        // 绘制曲线
        public static void UpLine(Canvas canvas, FrameworkElement sele, FrameworkElement eele, Path bp, Path aptah)
        {
            Point sp = sele.TranslatePoint(new Point(sele.ActualWidth / 2, sele.ActualHeight / 2), canvas);
            Point ep = PointPosition(eele, canvas, sp, out bool isSpin);
            PathFigure pf = new PathFigure { StartPoint = sp };
            BezierSegment bs;
            bs = new BezierSegment
            {
                Point1 = isSpin ? new Point((sp.X + ep.X) / 2, sp.Y) : new Point(sp.X, (sp.Y + ep.Y) / 2),
                Point2 = isSpin ? new Point((sp.X + ep.X) / 2, ep.Y) : new Point(ep.X, (sp.Y + ep.Y) / 2),
                Point3 = ep,
            }; 
            pf.Segments.Add(bs);
            PathGeometry pg = new PathGeometry();
            pg.Figures.Add(pf);
            bp.Data = pg;
            Point asp = Diff(sp, bs.Point3, bs.Point2, ep);
            UpAp(ep, asp, aptah);
        }


        // 计算曲线在 t 处的一阶导数,返回切线向量
        private static Point Diff(Point sp, Point cp1, Point cp2, Point ep)
        {
            double t = 10.0; 
            double dx = 3 * Math.Pow(1 - t, 2) * (cp1.X - sp.X) +
                        6 * (1 - t) * t * (cp2.X - cp1.X) +
                        3 * Math.Pow(t, 2) * (ep.X - cp2.X);

            double dy = 3 * Math.Pow(1 - t, 2) * (cp1.Y - sp.Y) +
                        6 * (1 - t) * t * (cp2.Y - cp1.Y) +
                        3 * Math.Pow(t, 2) * (ep.Y - cp2.Y);
            return new Point(dx, dy);
        }

        // 绘制箭头
        private static void UpAp(Point ep,  Point cp,Path aptah)
        {
            double alen = 10;
            double awit = 5;
            Vector d = ep - cp;
            d.Normalize();
            Point ap1 = ep + d * alen + new Vector(-d.Y, d.X) * awit;
            Point ap2 = ep + d * alen + new Vector(d.Y, -d.X) * awit;
            PathFigure af = new() { StartPoint = ep };
            af.Segments.Add(new LineSegment(ap1, true));
            af.Segments.Add(new LineSegment(ap2, true));
            af.Segments.Add(new LineSegment(ep, true));
            PathGeometry ag = new PathGeometry();
            ag.Figures.Add(af);
            aptah.Data = ag;
        }

        // 计算终点落点位置
        private static Point PointPosition(FrameworkElement e, Canvas canvas, Point sp, out bool isSpin)
        {
            Point cp = e.TranslatePoint(new Point(e.ActualWidth / 2, e.ActualHeight / 2), canvas);
            Vector d = cp - sp;
            d.Normalize();
            var x = cp.X - sp.X;
            var y = sp.Y - cp.Y;
            isSpin = Math.Abs(x) > Math.Abs(y);
            cp.X -= (cp.X - sp.X < sp.Y - cp.Y, isSpin) switch
            {
                (false, true) => e.ActualWidth / 2,
                (true, true) => e.ActualWidth / - 2,
                (false, false) => d.X / Math.Abs(d.Y) * e.ActualWidth / 2,
                (true, false) => d.X / Math.Abs(d.Y) * e.ActualWidth / 2,
            };
            cp.Y -= (cp.X - sp.X < sp.Y - cp.Y, isSpin) switch
            {
                (false, true) => d.Y / Math.Abs(d.X) * e.ActualHeight / 2,
                (true, true) => d.Y / Math.Abs(d.X) * e.ActualHeight / 2,
                (false, false) => e.ActualHeight / 2,
                (true, false) => e.ActualHeight / -2,
            };
            return cp;
        }

    }
    #endregion

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值