您的位置:首页 > 汽车 > 时评 > WINUI——实现点在直线上随意移动

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

2024/12/22 22:46:41 来源:https://blog.csdn.net/bianguanyue/article/details/140419461  浏览:    关键词: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><Linex:Name="line"Stroke="Black"StrokeThickness="2"X1="10"X2="400"Y1="{Binding ElementName=line, Path=X1}"Y2="400" /><EllipseName="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);}elsey = 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);}elsey = 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) => { };};};}}

注:后台代码中控件获取指针焦点后,即WIN的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. 后台代码中控件获取指针焦点后,即WIN的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,以供后续的复用。

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com