操纵杆不会旋转超出范围

Joystick does not rotate out of range

我的项目中需要一个操纵杆。我在设计和功能方面找到了一个很好的例子。

但是,当鼠标指针离开操纵杆时,此示例不旋转并冻结。自然,这会给用户留下不好的印象。我该如何解决这个问题?

Example Link GitHub

我也分享了代码,因为他们无法通过 Github 下载项目。

CustomControl.cs

public partial class OnScreenJoystick : UserControl
{
    /// <summary>Current angle in degrees from 0 to 360</summary>
    public static readonly DependencyProperty AngleProperty =
        DependencyProperty.Register("Angle", typeof(double), typeof(OnScreenJoystick), null);

    /// <summary>Current distance (or "power"), from 0 to 100</summary>
    public static readonly DependencyProperty DistanceProperty =
        DependencyProperty.Register("Distance", typeof(double), typeof(OnScreenJoystick), null);

    /// <summary>How often should be raised StickMove event in degrees</summary>
    public static readonly DependencyProperty AngleStepProperty =
        DependencyProperty.Register("AngleStep", typeof(double), typeof(OnScreenJoystick), new PropertyMetadata(1.0));

    /// <summary>How often should be raised StickMove event in distance units</summary>
    public static readonly DependencyProperty DistanceStepProperty =
        DependencyProperty.Register("DistanceStep", typeof(double), typeof(OnScreenJoystick), new PropertyMetadata(1.0));

    /* Unstable - needs work */
    ///// <summary>Indicates whether the joystick knob resets its place after being released</summary>
    //public static readonly DependencyProperty ResetKnobAfterReleaseProperty =
    //    DependencyProperty.Register(nameof(ResetKnobAfterRelease), typeof(bool), typeof(VirtualJoystick), new PropertyMetadata(true));

    /// <summary>Current angle in degrees from 0 to 360</summary>
    public double Angle
    {
        get { return Convert.ToDouble(GetValue(AngleProperty)); }
        private set { SetValue(AngleProperty, value); }
    }

    /// <summary>current distance (or "power"), from 0 to 100</summary>
    public double Distance
    {
        get { return Convert.ToDouble(GetValue(DistanceProperty)); }
        private set { SetValue(DistanceProperty, value); }
    }

    /// <summary>How often should be raised StickMove event in degrees</summary>
    public double AngleStep
    {
        get { return Convert.ToDouble(GetValue(AngleStepProperty)); }
        set
        {
            if (value < 1) value = 1; else if (value > 90) value = 90;
            SetValue(AngleStepProperty, Math.Round(value));
        }
    }

    /// <summary>How often should be raised StickMove event in distance units</summary>
    public double DistanceStep
    {
        get { return Convert.ToDouble(GetValue(DistanceStepProperty)); }
        set
        {
            if (value < 1) value = 1; else if (value > 50) value = 50;
            SetValue(DistanceStepProperty, value);
        }
    }

    /// <summary>Indicates whether the joystick knob resets its place after being released</summary>
    //public bool ResetKnobAfterRelease
    //{
    //    get { return Convert.ToBoolean(GetValue(ResetKnobAfterReleaseProperty)); }
    //    set { SetValue(ResetKnobAfterReleaseProperty, value); }
    //}

    /// <summary>Delegate holding data for joystick state change</summary>
    /// <param name="sender">The object that fired the event</param>
    /// <param name="args">Holds new values for angle and distance</param>
    public delegate void OnScreenJoystickEventHandler(OnScreenJoystick sender, VirtualJoystickEventArgs args);

    /// <summary>Delegate for joystick events that hold no data</summary>
    /// <param name="sender">The object that fired the event</param>
    public delegate void EmptyJoystickEventHandler(OnScreenJoystick sender);

    /// <summary>This event fires whenever the joystick moves</summary>
    public event OnScreenJoystickEventHandler Moved;

    /// <summary>This event fires once the joystick is released and its position is reset</summary>
    public event EmptyJoystickEventHandler Released;

    /// <summary>This event fires once the joystick is captured</summary>
    public event EmptyJoystickEventHandler Captured;

    private Point _startPos;
    private double _prevAngle, _prevDistance;
    private readonly Storyboard centerKnob;

    public OnScreenJoystick()
    {
        InitializeComponent();

        Knob.MouseLeftButtonDown += Knob_MouseLeftButtonDown;
        Knob.MouseLeftButtonUp += Knob_MouseLeftButtonUp;
        Knob.MouseMove += Knob_MouseMove;

        centerKnob = Knob.Resources["CenterKnob"] as Storyboard;
    }

    private void Knob_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
    {
        _startPos = e.GetPosition(Base);
        _prevAngle = _prevDistance = 0;

        Captured?.Invoke(this);
        Knob.CaptureMouse();

        centerKnob.Stop();
    }

    private void Knob_MouseMove(object sender, MouseEventArgs e)
    {
        if (!Knob.IsMouseCaptured) return;

        Point newPos = e.GetPosition(Base);

        Point deltaPos = new Point(newPos.X - _startPos.X, newPos.Y - _startPos.Y);

        double angle = Math.Atan2(deltaPos.Y, deltaPos.X) * 180 / Math.PI;
        if (angle > 0)
            angle += 90;
        else
        {
            angle = 270 + (180 + angle);
            if (angle >= 360) angle -= 360;
        }

        double distance = Math.Round(Math.Sqrt(deltaPos.X * deltaPos.X + deltaPos.Y * deltaPos.Y) / 135 * 100);
        if (distance <= 100)
        {
            Angle = angle;
            Distance = distance;

            knobPosition.X = deltaPos.X;
            knobPosition.Y = deltaPos.Y;

            if (Moved == null ||
                (!(Math.Abs(_prevAngle - angle) > AngleStep) && !(Math.Abs(_prevDistance - distance) > DistanceStep)))
                return;

            Moved?.Invoke(this, new VirtualJoystickEventArgs { Angle = Angle, Distance = Distance });
            _prevAngle = Angle;
            _prevDistance = Distance;
        }
    }

    private void Knob_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
    {
        Knob.ReleaseMouseCapture();
        centerKnob.Begin();
    }

    private void centerKnob_Completed(object sender, EventArgs e)
    {
        Angle = Distance = _prevAngle = _prevDistance = 0;
        Released?.Invoke(this);
    }
}

CustomControl.xaml

<UserControl x:Class="WpfCustomControls.OnScreenJoystick"
         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:WpfCustomControls"
         mc:Ignorable="d"
         d:DesignHeight="300" d:DesignWidth="300">
<Viewbox Name="Viewbox" Stretch="Uniform">
    <Grid Background="Transparent">

        <Canvas x:Name="Base" Margin="0" Width="340" Height="340">
            <Ellipse HorizontalAlignment="Left" Height="340" VerticalAlignment="Top" Width="340">
                <Ellipse.Fill>
                    <RadialGradientBrush>
                        <GradientStop Color="#FF2C2A2A" Offset="1" />
                        <GradientStop Color="#FF3A3737" />
                    </RadialGradientBrush>
                </Ellipse.Fill>
            </Ellipse>
            <Ellipse HorizontalAlignment="Left" Height="170" VerticalAlignment="Top" Width="170" Canvas.Left="84" Canvas.Top="84">
                <Ellipse.Fill>
                    <RadialGradientBrush>
                        <GradientStop Color="#FF0E0E0E" Offset="1" />
                        <GradientStop Color="#FF1D1D1D" />
                        <GradientStop Color="#FF323030" Offset="0.453" />
                    </RadialGradientBrush>
                </Ellipse.Fill>
            </Ellipse>
            <Path Data="M205.75,65.625 L226.875,47.25 L248.5,65.625 z" Fill="#FF575757" HorizontalAlignment="Left" Height="18.375" Stretch="Fill" UseLayoutRounding="False" VerticalAlignment="Top" Width="42.75" Canvas.Left="147.875" Canvas.Top="37.625" />
            <Path Data="M205.75,65.625 L226.875,47.25 L248.5,65.625 z" Fill="#FF575757" HorizontalAlignment="Left" Height="18.375" Stretch="Fill" UseLayoutRounding="False" VerticalAlignment="Top" Width="42.75" RenderTransformOrigin="0.5,0.5" Canvas.Left="147.875" Canvas.Top="284.125">
                <Path.RenderTransform>
                    <ScaleTransform ScaleY="-1" />
                </Path.RenderTransform>
            </Path>
            <Path Data="M205.75,65.625 L226.875,47.25 L248.5,65.625 z" Fill="#FF575757" HorizontalAlignment="Left" Height="18.375" Stretch="Fill" UseLayoutRounding="False" VerticalAlignment="Top" Width="42.75" RenderTransformOrigin="0.5,0.5" Canvas.Left="270.875" Canvas.Top="162.125">
                <Path.RenderTransform>
                    <TransformGroup>
                        <ScaleTransform ScaleY="-1" ScaleX="-1" />
                        <RotateTransform Angle="-90" />
                    </TransformGroup>
                </Path.RenderTransform>
            </Path>
            <Path Data="M205.75,65.625 L226.875,47.25 L248.5,65.625 z"  Fill="#FF575757" HorizontalAlignment="Left" Height="18.375" Stretch="Fill" UseLayoutRounding="False" VerticalAlignment="Top" Width="42.75" RenderTransformOrigin="0.5,0.5" Canvas.Left="24.375" Canvas.Top="163.625">
                <Path.RenderTransform>
                    <TransformGroup>
                        <RotateTransform Angle="90" />
                        <ScaleTransform ScaleX="-1" />
                    </TransformGroup>
                </Path.RenderTransform>
            </Path>

            <Canvas  x:Name="Knob" VerticalAlignment="Top" HorizontalAlignment="Left" Width="0" Height="0" RenderTransformOrigin="0.5,0.5" Canvas.Left="125" Canvas.Top="125">
                <!--<Ellipse x:Name="Shadow" HorizontalAlignment="Left" Height="88" VerticalAlignment="Top" Width="86" Fill="#52131212" Canvas.Left="22" Canvas.Top="18" />-->
                <Ellipse x:Name="KnobBase" HorizontalAlignment="Left" Height="90" VerticalAlignment="Top" Width="90" RenderTransformOrigin="0.5,0.5" Canvas.Top="1">
                    <Ellipse.Fill>
                        <RadialGradientBrush>
                            <GradientStop Color="#FF8A8A8A" />
                            <GradientStop Color="#FF979797" Offset="1" />
                        </RadialGradientBrush>
                    </Ellipse.Fill>
                </Ellipse>
                <Ellipse HorizontalAlignment="Left" Height="74.313" VerticalAlignment="Top" Width="82.189" RenderTransformOrigin="0.5,0.5" UseLayoutRounding="False" d:LayoutRounding="Auto" Canvas.Left="0.613" Canvas.Top="1.692">
                    <Ellipse.Fill>
                        <RadialGradientBrush>
                            <GradientStop Color="#C0828080" Offset="0.797" />
                            <GradientStop Color="#FD000000" />
                        </RadialGradientBrush>
                    </Ellipse.Fill>
                    <Ellipse.RenderTransform>
                        <TransformGroup>
                            <RotateTransform Angle="-28.434" />
                            <SkewTransform AngleX="-2.144" />
                            <TranslateTransform X="-1.199" Y="0.649" />
                        </TransformGroup>
                    </Ellipse.RenderTransform>
                </Ellipse>
                <Ellipse HorizontalAlignment="Left" Height="75.491"  VerticalAlignment="Top" Width="70.887" RenderTransformOrigin="0.5,0.5" UseLayoutRounding="False" d:LayoutRounding="Auto" Canvas.Left="12.396" Canvas.Top="5.057">
                    <Ellipse.Fill>
                        <LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0">
                            <GradientStop Color="#00858585" Offset="0" />
                            <GradientStop Color="#1AFFFFFF" Offset="1" />
                            <GradientStop Color="#3FC2C2C2" Offset="0.349" />
                        </LinearGradientBrush>
                    </Ellipse.Fill>
                    <Ellipse.RenderTransform>
                        <TransformGroup>
                            <SkewTransform CenterX="3" CenterY="-4" />
                            <RotateTransform Angle="-.7628" />
                        </TransformGroup>
                    </Ellipse.RenderTransform>
                </Ellipse>
                <Ellipse HorizontalAlignment="Left" Height="72.722" VerticalAlignment="Top" Width="72.936" RenderTransformOrigin="0.5,0.5" UseLayoutRounding="False" d:LayoutRounding="Auto" Canvas.Left="0.631" Canvas.Top="4.896">
                    <Ellipse.Fill>
                        <RadialGradientBrush>
                            <GradientStop Color="#9A909090" Offset="1" />
                            <GradientStop Color="Gray" />
                        </RadialGradientBrush>
                    </Ellipse.Fill>
                    <Ellipse.RenderTransform>
                        <RotateTransform Angle="-31.733"></RotateTransform>
                    </Ellipse.RenderTransform>
                </Ellipse>
                <Ellipse HorizontalAlignment="Left" Height="37" VerticalAlignment="Top" Width="39" RenderTransformOrigin="0.5,0.5" UseLayoutRounding="False" d:LayoutRounding="Auto" Canvas.Left="14.001" Canvas.Top="11.001">
                    <Ellipse.RenderTransform>
                        <SkewTransform CenterX="-8"></SkewTransform>
                    </Ellipse.RenderTransform>
                    <Ellipse.Fill>
                        <RadialGradientBrush>
                            <GradientStop Color="#FF898989" Offset="0" />
                            <GradientStop Color="#38777777" Offset="1" />
                            <GradientStop Color="#55676767" Offset="0.672" />
                        </RadialGradientBrush>
                    </Ellipse.Fill>
                </Ellipse>
                <Path Data="M9.74935,11.916 L13.084,15.166 L12.1668,16.833 L11.3333,18.583 L10.4999,20.416 L9.24961,20.833 L6.99967,20.583 L6.75,18.333 L7.66697,15.333 L8.75037,12.916 z M3.6672,9.74999 L7.084,10.083 L5.75037,12.25 L4.66704,14 L4.33365,16.583 L4.25036,18.75 L4.41695,20.5 L0,20.166 L0.16699,16.916 L1.16693,13.833 L2.50016,11.583 z M18.1671,6.33301 L21.167,6.33301 L21.667,8.5 L20.75,9.75 L18.5841,10.833 L15.8337,13 L12.584,8.83301 L15.2502,7 z M20.917,0 L20.917,3.16601 L18.1674,2.99999 L15.8337,3.583 L13.5837,4.833 L11.3337,5.99999 L10.5003,6.416 L8.584,3.833 L11.0842,2.41601 L13.3341,0.833006 L16.417,0.166016 z" Fill="#99EEEEEE" HorizontalAlignment="Left" Height="20.833" Stretch="Fill" UseLayoutRounding="False" VerticalAlignment="Top" Width="21.667" Canvas.Left="18.166" Canvas.Top="15.917" />

                <Canvas.RenderTransform>
                    <TranslateTransform x:Name="knobPosition" />
                </Canvas.RenderTransform>
                <Canvas.Resources>

                    <Storyboard x:Key="CenterKnob" Name="centerKnob" Completed="centerKnob_Completed">

                        <DoubleAnimation Storyboard.TargetName="knobPosition"
                         Storyboard.TargetProperty="X" To="0" Duration="0:0:0.2">
                            <DoubleAnimation.EasingFunction>
                                <BackEase EasingMode="EaseInOut" />
                            </DoubleAnimation.EasingFunction>
                        </DoubleAnimation>
                        <DoubleAnimation Storyboard.TargetName="knobPosition" Storyboard.TargetProperty="Y" To="0" Duration="0:0:0.2">
                            <DoubleAnimation.EasingFunction>
                                <BackEase EasingMode="EaseInOut" />
                            </DoubleAnimation.EasingFunction>
                        </DoubleAnimation>
                    </Storyboard>
                </Canvas.Resources>
            </Canvas>
            
        </Canvas>
    </Grid>
</Viewbox>

我用不同的方法解决了这个问题。现在下面的代码可以工作了。我还添加了键盘控制。

Xaml代码:

<Window x:Class="Joystick.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:Joystick"
    xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
    mc:Ignorable="d"
    Title="MainWindow" Height="800" Width="1000"   MouseMove="Ellipse_MouseMove" MouseLeftButtonUp="Joystick_MouseLeftButtonUp"  KeyDown="Window_KeyDown">
<Grid Name="MainGrid" >
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="*"/>
        <ColumnDefinition Width="3*"/>

    </Grid.ColumnDefinitions>
    <StackPanel Orientation="Vertical">
        <TextBlock Text="KNOB POSITION"/>
        <TextBlock Name="XTextBlock"/>
        <TextBlock Name="YTextBlock"/>
        <TextBlock Text="MOUSE POSITION"/>
        <TextBlock Name="XMousePos"/>
        <TextBlock Name="YMousePos"/>
        <TextBlock Text="ANGEL / DISTANCE"/>
        <TextBlock Name="Angle"/>
        <TextBlock Name="Distance"/>
    </StackPanel>
    <Canvas Name="LayoutRoot" Grid.Column="1" Width="300" Height="300" Margin="0,234,444,235" >
        <Ellipse Name="Joystick" Height="200" Canvas.Left="50" Canvas.Top="50" Stroke="Black"  Width="200" >
            <Ellipse.Fill>
                <RadialGradientBrush>
                    <GradientStop Color="#FF2C2A2A" Offset="1" />
                    <GradientStop Color="#FF3A3737" />
                </RadialGradientBrush>
            </Ellipse.Fill>
        </Ellipse>
        <Ellipse  Height="100" Width="100" Canvas.Left="100" Canvas.Top="100">
            <Ellipse.Fill>
                <RadialGradientBrush>
                    <GradientStop Color="Red" Offset="1" />
                    <GradientStop Color="#FF1D1D1D" />
                    <GradientStop Color="#FF323030" Offset="0.453" />
                </RadialGradientBrush>
            </Ellipse.Fill>
        </Ellipse>

        <Path Name="UpArrow" Data="M205.75,65.625 L226.875,47.25 L248.5,65.625 z" Fill="Red" HorizontalAlignment="Left" Height="11.025" Width="25.65" Stretch="Fill" UseLayoutRounding="True" VerticalAlignment="Top"  Canvas.Left="137.175" Canvas.Top="70"/>
        <Path Data="M205.75,65.625 L226.875,47.25 L248.5,65.625 z" Fill="{Binding ElementName=UpArrow , Path=Fill}" HorizontalAlignment="Left" Height="11.025" Width="25.65" Stretch="Fill" UseLayoutRounding="True" VerticalAlignment="Top"  Canvas.Left="70" Canvas.Top="137.175">
            <Path.RenderTransform>
                <TransformGroup>
                    <RotateTransform Angle="90"/>
                    <ScaleTransform ScaleX="-1"/>
                </TransformGroup>
            </Path.RenderTransform>
        </Path>
        <Path Data="M205.75,65.625 L226.875,47.25 L248.5,65.625 z" Fill="{Binding ElementName=UpArrow , Path=Fill}" HorizontalAlignment="Left" Height="11.025" Width="25.65" Stretch="Fill" UseLayoutRounding="True" VerticalAlignment="Top"  Canvas.Left="230" Canvas.Top="137.175">
            <Path.RenderTransform>
                <TransformGroup>
                    <RotateTransform Angle="-90"/>
                    <ScaleTransform ScaleX="-1" ScaleY="-1"/>
                </TransformGroup>
            </Path.RenderTransform>
        </Path>
        <Path Name="DownArrow" Data="M205.75,65.625 L226.875,47.25 L248.5,65.625 z" Fill="{Binding ElementName=UpArrow , Path=Fill}" HorizontalAlignment="Left" Height="11.025" Width="25.65" Stretch="Fill" UseLayoutRounding="True" VerticalAlignment="Top"  Canvas.Left="137.175" Canvas.Top="230">
            <Path.RenderTransform>
                <TransformGroup>
                    <RotateTransform/>
                    <ScaleTransform ScaleY="-1"/>
                </TransformGroup>
            </Path.RenderTransform>
        </Path>

        <Canvas  x:Name="Knob" VerticalAlignment="Top" HorizontalAlignment="Left" Width="50" Height="50"  Canvas.Left="125" Canvas.Top="125" MouseDown="Knob_MouseDown" MouseUp="Knob_MouseUp">
            <!--<Ellipse x:Name="Shadow" HorizontalAlignment="Left" Height="88" VerticalAlignment="Top" Width="86" Fill="#52131212" Canvas.Left="22" Canvas.Top="18" />-->
            <Ellipse x:Name="KnobBase" HorizontalAlignment="Left" Height="50" VerticalAlignment="Top" Width="50" RenderTransformOrigin="0.5,0.5" Canvas.Top="1">
                <Ellipse.Fill>
                    <RadialGradientBrush>
                        <GradientStop Color="#FF8A8A8A" />
                        <GradientStop Color="#FF979797" Offset="1" />
                    </RadialGradientBrush>
                </Ellipse.Fill>
            </Ellipse>
            <Ellipse HorizontalAlignment="Left" Height="38.795" VerticalAlignment="Top" Width="44.420" RenderTransformOrigin="0.5,0.5" UseLayoutRounding="False" d:LayoutRounding="Auto" Canvas.Left="0.613" Canvas.Top="1.692">
                <Ellipse.Fill>
                    <RadialGradientBrush>
                        <GradientStop Color="#C0828080" Offset="0.797" />
                        <GradientStop Color="#FD000000" />
                    </RadialGradientBrush>
                </Ellipse.Fill>
                <Ellipse.RenderTransform>
                    <TransformGroup>
                        <RotateTransform Angle="-28.434" />
                        <SkewTransform AngleX="-2.144" />
                        <TranslateTransform X="-1.199" Y="0.649" />
                    </TransformGroup>
                </Ellipse.RenderTransform>
            </Ellipse>
            <Ellipse HorizontalAlignment="Left" Height="39.636"  VerticalAlignment="Top" Width="36.347" RenderTransformOrigin="0.5,0.5" UseLayoutRounding="False" d:LayoutRounding="Auto" Canvas.Left="12.396" Canvas.Top="5.057">
                <Ellipse.Fill>
                    <LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0">
                        <GradientStop Color="#00858585" Offset="0" />
                        <GradientStop Color="#1AFFFFFF" Offset="1" />
                        <GradientStop Color="#3FC2C2C2" Offset="0.349" />
                    </LinearGradientBrush>
                </Ellipse.Fill>
                <Ellipse.RenderTransform>
                    <TransformGroup>
                        <SkewTransform CenterX="3" CenterY="-4" />
                        <RotateTransform Angle="-.7628" />
                    </TransformGroup>
                </Ellipse.RenderTransform>
            </Ellipse>
            <Ellipse HorizontalAlignment="Left" Height="37.658" VerticalAlignment="Top" Width="37.811" RenderTransformOrigin="0.5,0.5" UseLayoutRounding="False" d:LayoutRounding="Auto" Canvas.Left="0.631" Canvas.Top="4.896">
                <Ellipse.Fill>
                    <RadialGradientBrush>
                        <GradientStop Color="#9A909090" Offset="1" />
                        <GradientStop Color="Gray" />
                    </RadialGradientBrush>
                </Ellipse.Fill>
                <Ellipse.RenderTransform>
                    <RotateTransform Angle="-31.733"></RotateTransform>
                </Ellipse.RenderTransform>
            </Ellipse>
            <Ellipse HorizontalAlignment="Left" Height="12.142" VerticalAlignment="Top" Width="13.571" RenderTransformOrigin="0.5,0.5" UseLayoutRounding="False" d:LayoutRounding="Auto" Canvas.Left="14.001" Canvas.Top="11.001">
                <Ellipse.RenderTransform>
                    <SkewTransform CenterX="-8"></SkewTransform>
                </Ellipse.RenderTransform>
                <Ellipse.Fill>
                    <RadialGradientBrush>
                        <GradientStop Color="#FF898989" Offset="0" />
                        <GradientStop Color="#38777777" Offset="1" />
                        <GradientStop Color="#55676767" Offset="0.672" />
                    </RadialGradientBrush>
                </Ellipse.Fill>
            </Ellipse>

            <Path Data="M9.74935,11.916 L13.084,15.166 L12.1668,16.833 L11.3333,18.583 L10.4999,20.416 L9.24961,20.833 L6.99967,20.583 L6.75,18.333 L7.66697,15.333 L8.75037,12.916 z M3.6672,9.74999 L7.084,10.083 L5.75037,12.25 L4.66704,14 L4.33365,16.583 L4.25036,18.75 L4.41695,20.5 L0,20.166 L0.16699,16.916 L1.16693,13.833 L2.50016,11.583 z M18.1671,6.33301 L21.167,6.33301 L21.667,8.5 L20.75,9.75 L18.5841,10.833 L15.8337,13 L12.584,8.83301 L15.2502,7 z M20.917,0 L20.917,3.16601 L18.1674,2.99999 L15.8337,3.583 L13.5837,4.833 L11.3337,5.99999 L10.5003,6.416 L8.584,3.833 L11.0842,2.41601 L13.3341,0.833006 L16.417,0.166016 z" Fill="#99EEEEEE" HorizontalAlignment="Left"  Stretch="Fill" UseLayoutRounding="False" VerticalAlignment="Top" Height="11.904" Width="15.476" Canvas.Left="12.166" Canvas.Top="12.917" />


        </Canvas>

    </Canvas>
</Grid>

变量

    Point KeyjoystickPos = new Point();
    double step = 0.01;
    double stepMaxRange = 1.00;
    double maxRange = 1.00;
    public MainWindow()
    {
        InitializeComponent();
    }

Ellipse_MoseMove

private void Ellipse_MouseMove(object sender, MouseEventArgs e)
    {
        
        if (!Knob.IsMouseCaptured) return;

        double joystickRadius = Joystick.Height * 0.5;
        Vector joystickPos = e.GetPosition(Joystick) - new Point(joystickRadius, joystickRadius);
        joystickPos /= joystickRadius;

        double angle = Math.Atan2(joystickPos.Y, joystickPos.X);

        if (e.LeftButton == MouseButtonState.Pressed)
        {
            if (joystickPos.Length > 1.0)
            {
                joystickPos.X = Math.Cos(angle);
                joystickPos.Y = Math.Sin(angle);
            }

            UpdateKnobPosition(joystickPos.X , joystickPos.Y);
        }
    }

更新旋钮位置

private void UpdateKnobPosition(double joystickPosX ,double joystickPosY)
    {
        double fJoystickRadius = Joystick.Height * 0.5;
        double fKnobRadius = Knob.Width * 0.5;
        Canvas.SetLeft(Knob, Canvas.GetLeft(Joystick) +
            joystickPosX * fJoystickRadius + fJoystickRadius - fKnobRadius);
        Canvas.SetTop(Knob, Canvas.GetTop(Joystick) +
            joystickPosY * fJoystickRadius + fJoystickRadius - fKnobRadius);

        double distance = Math.Round(Math.Sqrt(joystickPosX * 100.00 * joystickPosX * 100.00 + joystickPosY * 100.00 * joystickPosY * 100.00) / 135.00 * 135.00);

        var angle2 = Math.Atan2(joystickPosY, joystickPosX) * 180 / Math.PI;

        if (angle2 > 0)
            angle2 += 90;
        else
        {
            angle2 = 270 + (180 + angle2);
            if (angle2 >= 360) angle2 -= 360;
        }

        Angle.Text = angle2.ToString();
        Distance.Text = distance.ToString();

        XMousePos.Text = Convert.ToString(joystickPosX * 100);
        YMousePos.Text = Convert.ToString(joystickPosY * 100);
    }

Joystick_MouseLeftButtonUp

private void Joystick_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
    {
        double joystickRadius = Joystick.Height * 0.5;
        Vector joystickPos = e.GetPosition(Joystick) -
        new Point(joystickRadius, joystickRadius);
        joystickPos /= joystickRadius;

        joystickPos.X = 0;
        joystickPos.Y = 0;
        KeyjoystickPos.X = 0;
        KeyjoystickPos.Y = 0;

        UpdateKnobPosition(joystickPos.X , joystickPos.Y);
    }

Knob_MouseDown

private void Knob_MouseDown(object sender, MouseButtonEventArgs e)
    {
        var Knob = (Canvas)sender;
        Knob.CaptureMouse();
    }

Knob_MouseUp

private void Knob_MouseUp(object sender, MouseButtonEventArgs e)
    {
        var Knob = (Canvas)sender;
        Knob.ReleaseMouseCapture();
    }

Window_KeyDown

private void Window_KeyDown(object sender, KeyEventArgs e)
    {
        if (e.Key == Key.W || e.Key == Key.Up)
        {
            KeyjoystickPos.Y = (KeyjoystickPos.Y <= -stepMaxRange) ? KeyjoystickPos.Y = -maxRange : KeyjoystickPos.Y = KeyjoystickPos.Y - step;
        }

        if (e.Key == Key.A || e.Key == Key.Left)
        {
            KeyjoystickPos.X = (KeyjoystickPos.X <= -stepMaxRange) ? KeyjoystickPos.X = -maxRange : KeyjoystickPos.X = KeyjoystickPos.X - step;
        }

        if (e.Key == Key.S || e.Key == Key.Down)
        {
            KeyjoystickPos.Y = (KeyjoystickPos.Y >= stepMaxRange) ? KeyjoystickPos.Y = maxRange : KeyjoystickPos.Y = KeyjoystickPos.Y + step;
        }

        if (e.Key == Key.D || e.Key == Key.Right)
        {
            KeyjoystickPos.X = (KeyjoystickPos.X >= stepMaxRange) ? KeyjoystickPos.X = maxRange : KeyjoystickPos.X = KeyjoystickPos.X + step;
        }

        if (e.Key == Key.Space)
        {
            KeyjoystickPos.X = 0;
            KeyjoystickPos.Y = 0;
        }

        double angle = Math.Atan2(KeyjoystickPos.Y, KeyjoystickPos.X);

        if(KeyjoystickPos.X*100 * KeyjoystickPos.X*100 + KeyjoystickPos.Y*100 *  KeyjoystickPos.Y*100 > 100*100)
        {
            KeyjoystickPos.X = Math.Cos(angle);
            KeyjoystickPos.Y = Math.Sin(angle);
        }

        UpdateKnobPosition(KeyjoystickPos.X, KeyjoystickPos.Y);
    }