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
{
///
/// 虚拟摇杆 UserControl,提供圆形区域内的鼠标拖拽操控能力 | Virtual joystick UserControl providing mouse drag interaction within a circular area
///
///
/// 支持双轴/单轴 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.
///
public partial class VirtualJoystick : UserControl
{
#region 私有字段 | Private Fields
/// 是否正在拖拽 | Whether dragging is in progress
private bool _isDragging;
/// 控件中心点坐标 | Control center point coordinates
private Point _centerPoint;
/// 操控点的平移变换 | Translate transform for thumb element
private readonly TranslateTransform _thumbTransform = new TranslateTransform();
/// 操控点元素引用 | Thumb element reference
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));
/// 获取或设置摇杆轴模式 | Gets or sets the joystick axis mode
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;
/// X 轴归一化输出值 [-1.0, 1.0] | X-axis normalized output
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;
/// Y 轴归一化输出值 [-1.0, 1.0] | Y-axis normalized output
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));
/// 死区比例 [0.0, 1.0],默认 0.05 | Dead zone ratio, default 0.05
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;
/// 当前激活的鼠标按键 | Currently active mouse button
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();
}
/// 重置操控点到中心位置 | Reset thumb to center
internal void ResetToCenter()
{
OutputX = 0.0;
OutputY = 0.0;
ActiveMouseButton = MouseButtonType.None;
_isDragging = false;
_thumbTransform.X = 0.0;
_thumbTransform.Y = 0.0;
}
///
/// 获取可用半径(限制为控件尺寸的 80%)| Get usable radius (limited to 80% of control size)
/// 双轴取宽高最小值的一半,单轴取高度的一半 | DualAxis uses min(W,H)/2, SingleAxisY uses H/2
///
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
///
/// 根据 ActiveMouseButton 和 JoystickMode 更新图标可见性 | Update icon visibility based on ActiveMouseButton and JoystickMode
///
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
}
}