在WPF中旋转形状以跟随光标

3
我已经完成了大部分的工作,但问题出现在我的数学计算中。我希望箭头(用户控件)可以旋转并指向鼠标,当它在WPF画布上单击并拖动时。我已经弄清楚如何计算弧度的角度,但是当我应用该值时,看起来效果不尽如人意。 当前状态 enter image description here 目标状态 enter image description here enter image description here 关注的主要代码片段如下...
private double Angle(Point origin, Point target)
        {
            //Calculate the distance from the square to the mouse's X and Y position
            var radians = Math.Atan2(origin.Y - target.Y, origin.X - target.X);
            var degrees = radians * (180 / Math.PI) - 90;

            Console.WriteLine(target + "--" + origin + "--" + degrees);
            return degrees;
        }

        private void Canvas_MouseMove(object sender, MouseEventArgs e)
        {                                     
            var canvas = (Canvas)sender;

            if (e.LeftButton == MouseButtonState.Pressed)
            {
                if (_followMouse)
                {
                    // Get Cursor Position
                    Point _targetPoint = e.GetPosition(this);

                    // Follow mouse
                    foreach (UIElement element in canvas.Children)
                    {                        
                        Arrow arrow = (Arrow)element;

                        // example 1
                        double x = Canvas.GetTop(arrow) + arrow.ActualWidth / 2.0;
                        double y = Canvas.GetLeft(arrow) + arrow.ActualHeight / 2.0;
                        Point _originPoint = new Point(x,y);

                        arrow.rotateTransform.Angle = Angle(_originPoint, _targetPoint);         
                    }
                }
            }
        }

以下是整个项目的代码...

MainWindow.xaml.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;

namespace WpfApplication1
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }

        bool _followMouse;

        private void Canvas_MouseDown(object sender, MouseButtonEventArgs e)
        {
            _followMouse = true;
        }
        private void Canvas_MouseUp(object sender, MouseButtonEventArgs e)
        {
            _followMouse = false;
        }

        private double Angle(Point origin, Point target)
        {
            //Calculate the distance from the square to the mouse's X and Y position
            var radians = Math.Atan2(origin.Y - target.Y, origin.X - target.X);
            var degrees = radians * (180 / Math.PI) - 90;

            Console.WriteLine(target + "--" + origin + "--" + degrees);
            return degrees;
        }

        private void Canvas_MouseMove(object sender, MouseEventArgs e)
        {                                     
            var canvas = (Canvas)sender;

            if (e.LeftButton == MouseButtonState.Pressed)
            {
                if (_followMouse)
                {
                    // Get Cursor Position
                    Point _targetPoint = e.GetPosition(this);

                    // Follow mouse
                    foreach (UIElement element in canvas.Children)
                    {                        
                        Arrow arrow = (Arrow)element;

                        // example 1
                        double x = Canvas.GetTop(arrow) + arrow.ActualWidth / 2.0;
                        double y = Canvas.GetLeft(arrow) + arrow.ActualHeight / 2.0;
                        Point _originPoint = new Point(x,y);

                        arrow.rotateTransform.Angle = Angle(_originPoint, _targetPoint);         
                    }
                }
            }
        }
    }
}

MainWindow.xaml

<Window x:Class="WpfApplication1.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:WpfApplication1"
        mc:Ignorable="d"
        Title="MainWindow" Height="300" Width="300"
        WindowStartupLocation="CenterScreen">

    <Canvas
        MouseDown="Canvas_MouseDown" 
        MouseUp="Canvas_MouseUp"
        MouseMove="Canvas_MouseMove"
        Background="LightBlue">
        <local:Arrow Canvas.Left="158" Canvas.Top="43"/>
        <local:Arrow Canvas.Left="38" Canvas.Top="108"/>
        <local:Arrow Canvas.Left="158" Canvas.Top="170"/>
        <local:Arrow Canvas.Left="78" Canvas.Top="158"/>
        <local:Arrow Canvas.Left="196" Canvas.Top="108"/>
        <local:Arrow Canvas.Left="78" Canvas.Top="53"/>
    </Canvas>

</Window>

Arrow.xaml

<UserControl x:Class="WpfApplication1.Arrow"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             xmlns:local="clr-namespace:WpfApplication1"
             mc:Ignorable="d" 
             d:DesignHeight="50" d:DesignWidth="50">
    <Grid>
        <Path Data="M 0 4 L 4 0 L 8 4 Z" RenderTransformOrigin="0.5,0.5"
              Width="50"
              Height="50"
              Stretch="Uniform"
              Fill="Red">
            <Path.RenderTransform>
                <RotateTransform x:Name="rotateTransform"/>
            </Path.RenderTransform>

        </Path>
    </Grid>
</UserControl>

你确定转换需要用弧度而不是角度吗? - ChrisF
我尝试了度数,但这也没有解决问题。 - JokerMartini
这就是“目标”下面的两个图像所代表的。我希望箭头能够旋转指向光标。 - JokerMartini
3个回答

3
这个问题本质上归结为找到两个向量之间的角度,一个是0度角的向量(即(0, -1)),另一个是从箭头指向点击点的向量。
数学上相当简单,acos(DotProduct(v1, v2)),由于这是一个非常关键的方程式,有许多资源可以解释它(http://www.wikihow.com/Find-the-Angle-Between-Two-Vectors)。请注意,这只给出了两个向量之间的无符号角度。要确定符号,您应该将您的符号设置为与向量To和一个正交于您的0向量的向量的DotProduct的符号相等。
换句话说,想象一下时钟的表针旋转,其中0度角是12点(并且由向量(0,-1)表示)。如果您顺时针旋转,结果将具有正X值,因为指针尖将位于时钟中心的右侧(3点钟将是(0,1))。向左旋转会给您负值(9点钟将是(0,-1))。因此,如果您知道所需的结果最终在右侧(因为您的X值为正),则您知道应该顺时针旋转的角度为正,反之亦然。这基本上是上面数学的简化视觉表示。
不管怎样 - 代码。
private double Angle(Point origin, Point target)
{

    // Get the vector from origin->point
    Vector vecTo = target - origin;

    // Normalize the vector
    vecTo.Normalize();

    // 0-angle is pointing straight up, aligned with (0, -1). 
    // The equation for the angle between 2 vectors is acos(Dot(v1, v1))
    // Our DotProduct is trivial, as know v1 is (0, -1).  This exands
    // 0 * v2.X + -1 * v2.Y
    double dotAngle = -vecTo.Y;
    double angle = Math.Acos(dotAngle);

    // Convert to rad
    angle = angle * 180 / Math.PI;
    // ACos will always return a positive number, but because Cos is
    // symmetric around 0 a -ve number is also valid,  Figure out which
    // is correct by taking the Dot vs (1, 0).  If result is positive,
    // then vecTo point in the same general direction as (1, 0), and
    // the angle returned should also be positive.  I've skipped
    // all the actual math, but thats the idea.
    if (vecTo.X > 0)
        return angle;
    else
        return -angle;
}

然而,另一个问题是箭头的X和Y坐标被反转了,正确的应该是:

double y = Canvas.GetTop(arrow) + arrow.ActualWidth / 2.0;
double x = Canvas.GetLeft(arrow) + arrow.ActualHeight / 2.0;
Point _originPoint = new Point(x,y);

无论何时转换到新的图形包,确定上下方向和右侧方向总是很重要的。如果找不到文档,请在某个地方设置断点,然后单击左上角和右下角,查看输出的坐标。请保留HTML标记。

你在评论中提到...通过取点来确定哪个是正确的....然而,你提供的代码按预期工作。我有什么遗漏吗? - JokerMartini
你让我觉得好像需要做额外的计算。 - JokerMartini
我发布的代码实际上确实执行点积 - (X,Y) 与 (1,0) 的点积仅为 1 * X + 0 * Y == X。我可能应该更加明确,但我喜欢这种视觉解释,我发现它比抽象的数学更容易理解时钟。 - FrozenKiwi
好的,那就有道理了。谢谢你帮忙,我很感激。 - JokerMartini

0

我无法回答为什么这样可以,但是我从一个假设开始:获取顶部和左侧不正确,你需要获取顶部中心(箭头的位置)。此外,x 应该是左侧,y 应该是顶部。然后添加一半的宽度,使 x 成为中心而不是左上角。

double x = Canvas.GetLeft(arrow);
double y = Canvas.GetTop(arrow);
x += (.5*arrow.ActualWidth);

这让我们接近了... 接下来的部分是我不确定为什么它有效,但它似乎有效。也许数学更好的人可以帮忙解释一下。将您的角度加上90度:

arrow.rotateTransform.Angle = (angleInRadians+90);

我发现这个问题是因为你计算的角度不正确...通过分析第一个箭头迭代的顶部和左侧的值,我得出结论它是最上面的。假设如此,点击左上角区域应该将其旋转大约270度,但我得到的值更接近180度。所以,我加了90度,似乎对于任何光标位置都有效,尽管我不确定原因。


我只能让一个箭头工作,而且只有当箭头在左上角时才有效。 - JokerMartini

0

输入:var angleInRadians = Math.Atan2(yDiff, xDiff) * 180.0 / Math.PI;

由于Math.Atan2已经返回以弧度为单位的向量,因此“* 180.0 / Math.PI”部分是不必要的。如果确实需要度数,请将变量重命名为角度(angle)而不是angleInRadian。


但是角度应该以度数为单位 https://msdn.microsoft.com/zh-cn/library/system.windows.media.rotatetransform(v=vs.110).aspx 因此计算是必要且正确的。 - ChrisF
为什么旋转看起来仍然不正确呢? - JokerMartini

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接