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

362 lines
18 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;
namespace XP.Common.Controls
{
/// <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 | 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;
StartDrag(MouseButtonType.Left);
e.Handled = true;
}
protected override void OnMouseRightButtonDown(MouseButtonEventArgs e)
{
base.OnMouseRightButtonDown(e);
if (_isDragging || !IsEnabled) return;
StartDrag(MouseButtonType.Right);
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 || ActiveMouseButton != MouseButtonType.Left) return;
EndDrag();
e.Handled = true;
}
protected override void OnMouseRightButtonUp(MouseButtonEventArgs e)
{
base.OnMouseRightButtonUp(e);
if (!_isDragging || ActiveMouseButton != MouseButtonType.Right) 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);
SetVisibility(Visibility.Visible, leftTop, leftBottom, leftLeft, leftRight);
if (_thumbElement != null) _thumbElement.Fill = new SolidColorBrush(Color.FromRgb(0x3A, 0x7B, 0xC8));
break;
case MouseButtonType.Right:
SetVisibility(Visibility.Collapsed, defaultTop, defaultBottom, defaultLeft, defaultRight);
SetVisibility(Visibility.Visible, rightTop, rightBottom, rightLeft, rightRight);
if (_thumbElement != null) _thumbElement.Fill = new SolidColorBrush(Color.FromRgb(0x5B, 0xA8, 0x5B));
break;
default:
if (_thumbElement != null) _thumbElement.Fill = new SolidColorBrush(Color.FromRgb(0x4A, 0x90, 0xD9));
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
}
}