WINUI——实现点在直线上随意移动

开发环境

VS2022

.net core6

需求

需要在一条直线上可随意移动一个点,这个点一定要在这条直线上,不可移出直线。也就是说A点到B点的直线就是这个点的移动范围。

需求深入分析

这个需求看上去,感觉完全可以使用Slider实现,因为Slider上已经将可移动的那个点(Slider的控制柄)完全限制在Slider上了。但深入沟通交流后发现,这条线的两个端点是需要绑定一个平面坐标的,并且两个端点可能会实时变化,那么用slider实现后,要改变它在平面上的位置,就不太方便了,需要通过复杂的计算,才能准确放置它的位置。

转而打算用Line + Ellipse来实现,Line由两个端点A(X1,Y1)B(X2,Y2)就决定了,要改变它的平面位置,只要将两个坐标点绑定给A、B两点即可。那么就只需要实现控制点无论怎么移动就在这条直线上即可,即是将不在直线上的光标位置转化为在直线上的x、y坐标值,然后将求出的坐标值赋值给点(注意是点的圆心在直线上,这个是隐藏需求)即可。相对Slider要简单很多。

注:文中提到的x1、y1均表示Line的A点坐标(x2、y2同理)。

转化为数学问题

因直线的AB两点已经定下来,也就是说(x1,y1)与(x2,y2)为已知,那么根据直线方程:y=kx+b,k为斜率(要注意直线为x轴垂线时需要特殊考虑),b为截距(即x=0时与y轴的交点)。

那么k=(y2-y1)/(x2-x1),b=y1- k x1。

同时直线X的范围限制在[x1,y1]这个闭区间即可,当斜率k存在时,Y就完全不需要限制它的区间(X的区间就限制了Y的区间);当斜率不存在时,即直线为x轴的垂线时,则需要限制y的区间在[y1,y2]的闭区间。开发中可以将X与Y均限制在直线区间内,然后再求对应直线点的值。

开发中b可以不求它,因为在这条直线段上的点的y值可以通过y=y1+k(x-x1), x为通过获取光标位置坐标x值;而斜率不存在时,b也不存在。

UI与代码

通过分析,这个功能仅需要Line与Ellipse即可,而为了实现Ellipse在后续移动,将这两个控件作为canvas的子控件,这样Ellipse后续位置的变动就可以很好的设置,通过Canvas.SetLeft和Canvas.SetTop即可将点位置重新设置。

UI端Xaml代码如下:

        <Canvas>
            <Line
                x:Name="line"
                Stroke="Black"
                StrokeThickness="2"
                X1="10"
                X2="400"
                Y1="{Binding ElementName=line, Path=X1}"
                Y2="400" />
            <Ellipse
                Name="movingCircle"
                Canvas.Left="0"
                Canvas.Top="0"
                Width="20"
                Height="20"
                Fill="Blue"
                Opacity="0.6" />
        </Canvas>

注意:

上述Line暂未用绑定,此可按自己需求进行相应的位置数据绑定。

另Ellipse的坐标设置的是(0,0),以便初始时Line的A点与Ellipse的圆心重合。

WINUI后台代码如下:

            movingCircle.PointerPressed += (sender, e) =>
            {
                var ellipse = sender as Ellipse;
                var startPoint = e.GetCurrentPoint(ellipse).Position;

                ellipse.CapturePointer(e.Pointer);
                ellipse.PointerMoved += (s, me) =>
                {
                    if (ellipse.CapturePointer(e.Pointer))
                    {
                        if (line.X1 == line.X2 && line.Y1 == line.Y2)//这就是一个点了,一个点计算个啥?
                        {
                            return;
                        }
                        var currentPosition = me.GetCurrentPoint(canvas).Position;
                        var x = Math.Max(line.X1, Math.Min(line.X2, currentPosition.X));
                        double y = Math.Max(line.Y1, Math.Min(line.Y2, currentPosition.Y));
                        if (line.X1 == line.X2 || line.Y1 == line.Y2)
                        {
                            //do nothing
                        }
                        else
                        {
                            var slope = (line.Y2 - line.Y1) / (line.X2 - line.X1);//X2==X1时,需要特殊考虑
                            var slope1 = (line.X2 - line.X1) / (line.Y2 - line.Y1);//y2==y1时,需要特殊处理
                            if (Math.Abs(slope) > Math.Abs(slope1))
                            {
                                x = line.X1 + slope1 * (y - line.Y1);
                            }
                            else
                                y = line.Y1 + slope * (x - line.X1);
                        }
                        var r = ellipse.Width / 2;
                        Canvas.SetLeft(ellipse, x - r);
                        Canvas.SetTop(ellipse, y - r);
                    }
                };
                ellipse.PointerReleased += (s, me) =>
                {
                    ellipse.ReleasePointerCapture(e.Pointer);
                    ellipse.PointerMoved -= (s, me2) => { };
                    ellipse.PointerReleased -= (s, me3) => { };
                };
            };

WPF后台代码如下(UI代码完全一样):

    public  class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();

            movingCircle.MouseLeftButtonDown += (sender, e) =>
            {
                var ellipse = sender as Ellipse;
                var b = ellipse.CaptureMouse();
                ellipse.MouseMove += (s, me) =>
                {
                    if (ellipse.IsMouseCaptured)
                    {
                        if (line.X1 == line.X2 && line.Y1 == line.Y2)//这就是一个点了,一个点计算个啥?
                        {
                            return;
                        }
                        var currentPosition = me.GetPosition(canvas);
                        var x = Math.Max(line.X1, Math.Min(line.X2, currentPosition.X));
                        double y = Math.Max(line.Y1, Math.Min(line.Y2, currentPosition.Y));
                        if (line.X1 == line.X2 || line.Y1 == line.Y2)
                        {
                          //do nothing
                        }
                        else
                        {
                            var slope = (line.Y2 - line.Y1) / (line.X2 - line.X1);//X2==X1时,需要特殊考虑
                            var slope1 = (line.X2 - line.X1) / (line.Y2 - line.Y1);//y2==y1时,需要特殊处理
                            if (Math.Abs(slope) > Math.Abs(slope1))
                            {
                                x = line.X1 + slope1 * (y - line.Y1);
                            }
                            else
                                y = line.Y1 + slope * (x - line.X1);
                        }
                        var r = ellipse.Width / 2;
                        Canvas.SetLeft(ellipse, x - r);
                        Canvas.SetTop(ellipse, y - r);
                    }
                };
                ellipse.MouseLeftButtonUp += (s, me) =>
                {
                    ellipse.ReleaseMouseCapture();
                    ellipse.MouseMove -= (s, me2) => { };
                    ellipse.MouseLeftButtonUp -= (s, me3) => { };
                };
            };

        }
    }

注:后台代码中控件获取指针焦点后,即WINUI的CapturePointer与WPF的CaptureMouse,一定要Release,WINUI中通过ReleasePointerCapture方法,WPF通过ReleaseMouseCapture。

特别注意

斜率K过大时,X移动过小的距离,Y就可能到达直线的端点。也就是说变化率过大会导致调节变得异常困难。

为了避免这个问题,在处理的过程中分别求了斜率slope和它的倒数slope1,然后比较它们绝对值(若斜率是-50,那么它的倒数是-1/50,-1/50的是大于-50的,但调节变化率则应该是-1/50是小于-50的)的大小,用较小的一个来计算当前位置对应在直线上的点位置。

|slope|>|1/slope|时, x = X1 + 1/slope * (y - Y1)  y为获取到的当前光标位置的y坐标值。

|slope|<=|1/slope|时,y = Y1 + slope * (x - X1)    x为获取到的当前光标位置的x坐标值。

注意事项

  1. 后台代码中控件获取指针焦点后,即WINUI的CapturePointer与WPF的CaptureMouse,一定要释放;WINUI中通过ReleasePointerCapture方法,WPF通过ReleaseMouseCapture。
  2. 在后台代码中一定要将在光标移动时绑定的事件移除,同个控件多次订阅相同事件是无益的,性能会有一定下降,还可能导致对象回收不及时或长期持有对象而出现内存相关bug。
  3. 要注间线上移动点Ellipse的圆心位置是否在直线上。
  4. 注意斜率导致的变化过大问题,同时处理斜率过大的边界条件x1=x2要考虑,y1=y2的情况也要考虑。x1=x2 && y1=y2的极端情况也要一并考虑进去。
  5. 注意获取光标位置时通过获取点的容器的方式来获取,直接通过获取点的光标位置,则会因点在快速移动,move事件处理不及时可能导致出现点跳动问题,在测试时斜率过大(绝对值过大)越容易出现点在直线上跳动。
  6. 要注意边界条件的考虑,如直线方程中很可能不会考虑到直线为x轴垂线的情况。
  7. 以上代码若有必要可以封装为一个UserControl,以供后续的复用。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值