Files
XplorePlane/XP.Common/Controls/Joystick/VirtualJoystick.cs
T

406 lines
20 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Shapes;
using XP.Common.Logging.Interfaces;
namespace XP.Common.Controls.Joystick
{
/// <summary>
/// 虚拟摇杆 UserControl,提供圆形区域内的鼠标拖拽操控能力 | Virtual joystick UserControl providing mouse drag interaction within a circular area
/// </summary>
/// <remarks>
/// 支持双轴/单轴 Y 模式、死区配置、归一化比例输出(-1.0 ~ 1.0)、鼠标左右键区分及四向功能图标切换。
/// Supports dual-axis/single-axis Y mode, dead zone configuration, normalized proportional output (-1.0 ~ 1.0),
/// left/right mouse button differentiation, and four-directional icon switching.
/// </remarks>
public partial class VirtualJoystick : UserControl
{
#region | Private Fields
/// <summary>是否正在拖拽 | Whether dragging is in progress</summary>
private bool _isDragging;
/// <summary>控件中心点坐标 | Control center point coordinates</summary>
private Point _centerPoint;
/// <summary>操控点的平移变换 | Translate transform for thumb element</summary>
private readonly TranslateTransform _thumbTransform = new TranslateTransform();
/// <summary>操控点元素引用 | Thumb element reference</summary>
private Ellipse? _thumbElement;
#endregion
#region | Constructor
public VirtualJoystick()
{
InitializeComponent();
// 控件加载完成后绑定操控点的 TranslateTransform | Bind thumb TranslateTransform after control loaded
Loaded += (s, e) =>
{
_thumbElement = FindName("PART_Thumb") as Ellipse;
if (_thumbElement != null)
_thumbElement.RenderTransform = _thumbTransform;
// 初始化时更新背景和图标可见性 | Update background and icon visibility on init
UpdateIconVisibility();
};
}
#endregion
#region JoystickMode | JoystickMode Dependency Property
public static readonly DependencyProperty JoystickModeProperty =
DependencyProperty.Register(nameof(JoystickMode), typeof(JoystickMode), typeof(VirtualJoystick),
new PropertyMetadata(JoystickMode.DualAxis, OnJoystickModeChanged));
/// <summary>获取或设置摇杆轴模式 | Gets or sets the joystick axis mode</summary>
public JoystickMode JoystickMode
{
get => (JoystickMode)GetValue(JoystickModeProperty);
set => SetValue(JoystickModeProperty, value);
}
private static void OnJoystickModeChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is VirtualJoystick joystick)
joystick.UpdateIconVisibility();
}
#endregion
#region OutputX/OutputY | OutputX/OutputY Read-Only Dependency Properties
private static readonly DependencyPropertyKey OutputXPropertyKey =
DependencyProperty.RegisterReadOnly(nameof(OutputX), typeof(double), typeof(VirtualJoystick), new PropertyMetadata(0.0));
public static readonly DependencyProperty OutputXProperty = OutputXPropertyKey.DependencyProperty;
/// <summary>X 轴归一化输出值 [-1.0, 1.0] | X-axis normalized output</summary>
public double OutputX
{
get => (double)GetValue(OutputXProperty);
internal set => SetValue(OutputXPropertyKey, value);
}
private static readonly DependencyPropertyKey OutputYPropertyKey =
DependencyProperty.RegisterReadOnly(nameof(OutputY), typeof(double), typeof(VirtualJoystick), new PropertyMetadata(0.0));
public static readonly DependencyProperty OutputYProperty = OutputYPropertyKey.DependencyProperty;
/// <summary>Y 轴归一化输出值 [-1.0, 1.0] | Y-axis normalized output</summary>
public double OutputY
{
get => (double)GetValue(OutputYProperty);
internal set => SetValue(OutputYPropertyKey, value);
}
#endregion
#region DeadZone | DeadZone Dependency Property
public static readonly DependencyProperty DeadZoneProperty =
DependencyProperty.Register(nameof(DeadZone), typeof(double), typeof(VirtualJoystick), new PropertyMetadata(0.05));
/// <summary>死区比例 [0.0, 1.0],默认 0.05 | Dead zone ratio, default 0.05</summary>
public double DeadZone
{
get => (double)GetValue(DeadZoneProperty);
set => SetValue(DeadZoneProperty, value);
}
#endregion
#region ActiveMouseButton | ActiveMouseButton Read-Only Dependency Property
private static readonly DependencyPropertyKey ActiveMouseButtonPropertyKey =
DependencyProperty.RegisterReadOnly(nameof(ActiveMouseButton), typeof(MouseButtonType), typeof(VirtualJoystick),
new PropertyMetadata(MouseButtonType.None, OnActiveMouseButtonChanged));
public static readonly DependencyProperty ActiveMouseButtonProperty = ActiveMouseButtonPropertyKey.DependencyProperty;
/// <summary>当前激活的鼠标按键 | Currently active mouse button</summary>
public MouseButtonType ActiveMouseButton
{
get => (MouseButtonType)GetValue(ActiveMouseButtonProperty);
internal set => SetValue(ActiveMouseButtonPropertyKey, value);
}
private static void OnActiveMouseButtonChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is VirtualJoystick joystick)
joystick.UpdateIconVisibility();
}
#endregion
#region SwapMouseButtons | SwapMouseButtons Dependency Property
public static readonly DependencyProperty SwapMouseButtonsProperty =
DependencyProperty.Register(nameof(SwapMouseButtons), typeof(bool), typeof(VirtualJoystick),
new PropertyMetadata(false, OnSwapMouseButtonsChanged));
/// <summary>是否交换左右键功能 | Whether to swap left and right button functions</summary>
public bool SwapMouseButtons
{
get => (bool)GetValue(SwapMouseButtonsProperty);
set => SetValue(SwapMouseButtonsProperty, value);
}
private static void OnSwapMouseButtonsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is VirtualJoystick joystick)
{
joystick.ActiveMouseButton = MouseButtonType.None;
joystick.UpdateIconVisibility();
}
}
#endregion
#region | Directional Icon Dependency Properties
// 左键图标 | Left button icons
public static readonly DependencyProperty LeftButtonTopIconProperty = DependencyProperty.Register(nameof(LeftButtonTopIcon), typeof(object), typeof(VirtualJoystick), new PropertyMetadata(null));
public object? LeftButtonTopIcon { get => GetValue(LeftButtonTopIconProperty); set => SetValue(LeftButtonTopIconProperty, value); }
public static readonly DependencyProperty LeftButtonBottomIconProperty = DependencyProperty.Register(nameof(LeftButtonBottomIcon), typeof(object), typeof(VirtualJoystick), new PropertyMetadata(null));
public object? LeftButtonBottomIcon { get => GetValue(LeftButtonBottomIconProperty); set => SetValue(LeftButtonBottomIconProperty, value); }
public static readonly DependencyProperty LeftButtonLeftIconProperty = DependencyProperty.Register(nameof(LeftButtonLeftIcon), typeof(object), typeof(VirtualJoystick), new PropertyMetadata(null));
public object? LeftButtonLeftIcon { get => GetValue(LeftButtonLeftIconProperty); set => SetValue(LeftButtonLeftIconProperty, value); }
public static readonly DependencyProperty LeftButtonRightIconProperty = DependencyProperty.Register(nameof(LeftButtonRightIcon), typeof(object), typeof(VirtualJoystick), new PropertyMetadata(null));
public object? LeftButtonRightIcon { get => GetValue(LeftButtonRightIconProperty); set => SetValue(LeftButtonRightIconProperty, value); }
// 右键图标 | Right button icons
public static readonly DependencyProperty RightButtonTopIconProperty = DependencyProperty.Register(nameof(RightButtonTopIcon), typeof(object), typeof(VirtualJoystick), new PropertyMetadata(null));
public object? RightButtonTopIcon { get => GetValue(RightButtonTopIconProperty); set => SetValue(RightButtonTopIconProperty, value); }
public static readonly DependencyProperty RightButtonBottomIconProperty = DependencyProperty.Register(nameof(RightButtonBottomIcon), typeof(object), typeof(VirtualJoystick), new PropertyMetadata(null));
public object? RightButtonBottomIcon { get => GetValue(RightButtonBottomIconProperty); set => SetValue(RightButtonBottomIconProperty, value); }
public static readonly DependencyProperty RightButtonLeftIconProperty = DependencyProperty.Register(nameof(RightButtonLeftIcon), typeof(object), typeof(VirtualJoystick), new PropertyMetadata(null));
public object? RightButtonLeftIcon { get => GetValue(RightButtonLeftIconProperty); set => SetValue(RightButtonLeftIconProperty, value); }
public static readonly DependencyProperty RightButtonRightIconProperty = DependencyProperty.Register(nameof(RightButtonRightIcon), typeof(object), typeof(VirtualJoystick), new PropertyMetadata(null));
public object? RightButtonRightIcon { get => GetValue(RightButtonRightIconProperty); set => SetValue(RightButtonRightIconProperty, value); }
// 默认图标 | Default icons
public static readonly DependencyProperty DefaultTopIconProperty = DependencyProperty.Register(nameof(DefaultTopIcon), typeof(object), typeof(VirtualJoystick), new PropertyMetadata(null));
public object? DefaultTopIcon { get => GetValue(DefaultTopIconProperty); set => SetValue(DefaultTopIconProperty, value); }
public static readonly DependencyProperty DefaultBottomIconProperty = DependencyProperty.Register(nameof(DefaultBottomIcon), typeof(object), typeof(VirtualJoystick), new PropertyMetadata(null));
public object? DefaultBottomIcon { get => GetValue(DefaultBottomIconProperty); set => SetValue(DefaultBottomIconProperty, value); }
public static readonly DependencyProperty DefaultLeftIconProperty = DependencyProperty.Register(nameof(DefaultLeftIcon), typeof(object), typeof(VirtualJoystick), new PropertyMetadata(null));
public object? DefaultLeftIcon { get => GetValue(DefaultLeftIconProperty); set => SetValue(DefaultLeftIconProperty, value); }
public static readonly DependencyProperty DefaultRightIconProperty = DependencyProperty.Register(nameof(DefaultRightIcon), typeof(object), typeof(VirtualJoystick), new PropertyMetadata(null));
public object? DefaultRightIcon { get => GetValue(DefaultRightIconProperty); set => SetValue(DefaultRightIconProperty, value); }
#endregion
#region | Mouse Interaction Event Handlers
protected override void OnMouseLeftButtonDown(MouseButtonEventArgs e)
{
base.OnMouseLeftButtonDown(e);
if (_isDragging || !IsEnabled) return;
// 如果启用了左右键交换,则左键触发右键逻辑,反之亦然
var buttonType = SwapMouseButtons ? MouseButtonType.Right : MouseButtonType.Left;
StartDrag(buttonType);
e.Handled = true;
}
protected override void OnMouseRightButtonDown(MouseButtonEventArgs e)
{
base.OnMouseRightButtonDown(e);
if (_isDragging || !IsEnabled) return;
// 如果启用了左右键交换,则右键触发左键逻辑,反之亦然
var buttonType = SwapMouseButtons ? MouseButtonType.Left : MouseButtonType.Right;
StartDrag(buttonType);
e.Handled = true;
}
protected override void OnMouseMove(MouseEventArgs e)
{
base.OnMouseMove(e);
if (!_isDragging) return;
try
{
var mousePosition = e.GetPosition(this);
var dx = mousePosition.X - _centerPoint.X;
var dy = mousePosition.Y - _centerPoint.Y;
// SingleAxisY 模式下强制水平位移为零,操控点只能上下移动 | Force dx=0 in SingleAxisY mode, thumb moves vertically only
if (JoystickMode == JoystickMode.SingleAxisY)
dx = 0;
var radius = GetRadius();
if (radius <= 0) return;
var (clampedDx, clampedDy) = JoystickCalculator.ClampToRadius(dx, dy, radius);
var (outputX, outputY) = JoystickCalculator.CalculateOutput(dx, dy, radius, DeadZone, JoystickMode);
OutputX = outputX;
OutputY = outputY;
_thumbTransform.X = clampedDx;
_thumbTransform.Y = clampedDy;
}
catch
{
ResetToCenter();
}
e.Handled = true;
}
protected override void OnMouseLeftButtonUp(MouseButtonEventArgs e)
{
base.OnMouseLeftButtonUp(e);
if (!_isDragging) return;
// 结束拖拽时,根据当前实际按下的按钮类型判断
var expectedType = SwapMouseButtons ? MouseButtonType.Right : MouseButtonType.Left;
if (ActiveMouseButton != expectedType) return;
EndDrag();
e.Handled = true;
}
protected override void OnMouseRightButtonUp(MouseButtonEventArgs e)
{
base.OnMouseRightButtonUp(e);
if (!_isDragging) return;
// 结束拖拽时,根据当前实际按下的按钮类型判断
var expectedType = SwapMouseButtons ? MouseButtonType.Left : MouseButtonType.Right;
if (ActiveMouseButton != expectedType) return;
EndDrag();
e.Handled = true;
}
#endregion
#region | Drag Helper Methods
private void StartDrag(MouseButtonType buttonType)
{
CaptureMouse();
ActiveMouseButton = buttonType;
_centerPoint = new Point(ActualWidth / 2.0, ActualHeight / 2.0);
_isDragging = true;
}
private void EndDrag()
{
ResetToCenter();
ReleaseMouseCapture();
}
/// <summary>重置操控点到中心位置 | Reset thumb to center</summary>
internal void ResetToCenter()
{
OutputX = 0.0;
OutputY = 0.0;
ActiveMouseButton = MouseButtonType.None;
_isDragging = false;
_thumbTransform.X = 0.0;
_thumbTransform.Y = 0.0;
}
/// <summary>
/// 获取可用半径(限制为控件尺寸的 80%| Get usable radius (limited to 80% of control size)
/// 双轴取宽高最小值的一半,单轴取高度的一半 | DualAxis uses min(W,H)/2, SingleAxisY uses H/2
/// </summary>
private double GetRadius()
{
var raw = JoystickMode == JoystickMode.SingleAxisY
? ActualHeight / 2.0
: Math.Min(ActualWidth, ActualHeight) / 2.0;
return raw * 0.8;
}
#endregion
#region | Icon Visibility Switching
/// <summary>
/// 根据 ActiveMouseButton 和 JoystickMode 更新图标可见性 | Update icon visibility based on ActiveMouseButton and JoystickMode
/// </summary>
private void UpdateIconVisibility()
{
// 查找命名元素 | Find named elements
var defaultTop = FindName("DefaultTopPresenter") as UIElement;
var defaultBottom = FindName("DefaultBottomPresenter") as UIElement;
var defaultLeft = FindName("DefaultLeftPresenter") as UIElement;
var defaultRight = FindName("DefaultRightPresenter") as UIElement;
var leftTop = FindName("LeftTopPresenter") as UIElement;
var leftBottom = FindName("LeftBottomPresenter") as UIElement;
var leftLeft = FindName("LeftLeftPresenter") as UIElement;
var leftRight = FindName("LeftRightPresenter") as UIElement;
var rightTop = FindName("RightTopPresenter") as UIElement;
var rightBottom = FindName("RightBottomPresenter") as UIElement;
var rightLeft = FindName("RightLeftPresenter") as UIElement;
var rightRight = FindName("RightRightPresenter") as UIElement;
var dualBg = FindName("DualAxisBackground") as UIElement;
var singleBg = FindName("SingleAxisBackground") as UIElement;
var hLine = FindName("HorizontalLine") as UIElement;
if (defaultTop == null) return; // 控件尚未加载 | Control not yet loaded
// 切换背景形状:双轴=圆形,单轴=腰圆 | Switch background: DualAxis=circle, SingleAxisY=capsule
var isSingleAxis = JoystickMode == JoystickMode.SingleAxisY;
if (dualBg != null) dualBg.Visibility = isSingleAxis ? Visibility.Collapsed : Visibility.Visible;
if (singleBg != null) singleBg.Visibility = isSingleAxis ? Visibility.Visible : Visibility.Collapsed;
if (hLine != null) hLine.Visibility = isSingleAxis ? Visibility.Collapsed : Visibility.Visible;
// 先全部隐藏 | Hide all first
SetVisibility(Visibility.Collapsed, leftTop, leftBottom, leftLeft, leftRight, rightTop, rightBottom, rightLeft, rightRight);
SetVisibility(Visibility.Visible, defaultTop, defaultBottom, defaultLeft, defaultRight);
// 根据 ActiveMouseButton 切换 | Switch based on ActiveMouseButton
// 如果启用了左右键交换,则显示相反的图标组
switch (ActiveMouseButton)
{
case MouseButtonType.Left:
SetVisibility(Visibility.Collapsed, defaultTop, defaultBottom, defaultLeft, defaultRight);
// 如果交换了左右键,这里显示的是右键图标组
var leftIcons = new[] { leftTop, leftBottom, leftLeft, leftRight };
SetVisibility(Visibility.Visible, leftIcons);
if (_thumbElement != null) _thumbElement.Fill = new SolidColorBrush(Color.FromRgb(0x2D, 0x63, 0x9E)); // 深一点的浅蓝色 | Darker light blue
break;
case MouseButtonType.Right:
SetVisibility(Visibility.Collapsed, defaultTop, defaultBottom, defaultLeft, defaultRight);
// 如果交换了左右键,这里显示的是左键图标组
var rightIcons = new[] { rightTop, rightBottom, rightLeft, rightRight };
SetVisibility(Visibility.Visible, rightIcons);
if (_thumbElement != null) _thumbElement.Fill = new SolidColorBrush(Color.FromRgb(0x45, 0x88, 0x45)); // 深一点的浅绿色 | Darker light green
break;
default:
// 根据 SwapMouseButtons 决定中心按钮颜色 | Determine center button color based on SwapMouseButtons
if (_thumbElement != null)
_thumbElement.Fill = SwapMouseButtons
? new SolidColorBrush(Color.FromRgb(0x5B, 0xA8, 0x5B)) // 浅绿色 | Light green
: new SolidColorBrush(Color.FromRgb(0x4A, 0x90, 0xD9)); // 浅蓝色 | Light blue
break;
}
// SingleAxisY 模式下隐藏左右图标 | Hide left/right icons in SingleAxisY mode
if (isSingleAxis)
{
SetVisibility(Visibility.Collapsed, defaultLeft, defaultRight, leftLeft, leftRight, rightLeft, rightRight);
}
}
private static void SetVisibility(Visibility visibility, params UIElement?[] elements)
{
foreach (var element in elements)
if (element != null) element.Visibility = visibility;
}
#endregion
}
}