Files
XplorePlane/XP.Common/Controls/VirtualJoystick.cs
T
2026-04-22 20:48:39 +08:00

630 lines
24 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;
namespace XP.Common.Controls
{
/// <summary>
/// 虚拟摇杆自定义控件,提供圆形区域内的鼠标拖拽操控能力 | Virtual joystick custom control 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>
[TemplatePart(Name = PART_Thumb, Type = typeof(UIElement))]
public class VirtualJoystick : Control
{
/// <summary>
/// 操控点模板部件名称 | Thumb template part name
/// </summary>
private const string PART_Thumb = "PART_Thumb";
#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 to follow mouse movement
/// </summary>
private TranslateTransform _thumbTransform = new TranslateTransform();
#endregion
#region | Static Constructor
static VirtualJoystick()
{
DefaultStyleKeyProperty.OverrideMetadata(
typeof(VirtualJoystick),
new FrameworkPropertyMetadata(typeof(VirtualJoystick)));
}
#endregion
#region JoystickMode | JoystickMode Dependency Property
/// <summary>
/// 摇杆轴模式依赖属性 | Joystick axis mode dependency property
/// </summary>
public static readonly DependencyProperty JoystickModeProperty =
DependencyProperty.Register(
nameof(JoystickMode),
typeof(JoystickMode),
typeof(VirtualJoystick),
new PropertyMetadata(JoystickMode.DualAxis));
/// <summary>
/// 获取或设置摇杆轴模式(双轴或单轴 Y)| Gets or sets the joystick axis mode (DualAxis or SingleAxisY)
/// </summary>
public JoystickMode JoystickMode
{
get => (JoystickMode)GetValue(JoystickModeProperty);
set => SetValue(JoystickModeProperty, value);
}
#endregion
#region OutputX | OutputX Read-Only Dependency Property
/// <summary>
/// OutputX 只读依赖属性键 | OutputX read-only dependency property key
/// </summary>
private static readonly DependencyPropertyKey OutputXPropertyKey =
DependencyProperty.RegisterReadOnly(
nameof(OutputX),
typeof(double),
typeof(VirtualJoystick),
new PropertyMetadata(0.0));
/// <summary>
/// X 轴归一化输出依赖属性 | X-axis normalized output dependency property
/// </summary>
public static readonly DependencyProperty OutputXProperty = OutputXPropertyKey.DependencyProperty;
/// <summary>
/// 获取 X 轴归一化输出值,范围 [-1.0, 1.0] | Gets the X-axis normalized output value, range [-1.0, 1.0]
/// </summary>
public double OutputX
{
get => (double)GetValue(OutputXProperty);
internal set => SetValue(OutputXPropertyKey, value);
}
#endregion
#region OutputY | OutputY Read-Only Dependency Property
/// <summary>
/// OutputY 只读依赖属性键 | OutputY read-only dependency property key
/// </summary>
private static readonly DependencyPropertyKey OutputYPropertyKey =
DependencyProperty.RegisterReadOnly(
nameof(OutputY),
typeof(double),
typeof(VirtualJoystick),
new PropertyMetadata(0.0));
/// <summary>
/// Y 轴归一化输出依赖属性 | Y-axis normalized output dependency property
/// </summary>
public static readonly DependencyProperty OutputYProperty = OutputYPropertyKey.DependencyProperty;
/// <summary>
/// 获取 Y 轴归一化输出值,范围 [-1.0, 1.0] | Gets the Y-axis normalized output value, range [-1.0, 1.0]
/// </summary>
public double OutputY
{
get => (double)GetValue(OutputYProperty);
internal set => SetValue(OutputYPropertyKey, value);
}
#endregion
#region DeadZone | DeadZone Dependency Property
/// <summary>
/// 死区比例依赖属性 | Dead zone ratio dependency property
/// </summary>
public static readonly DependencyProperty DeadZoneProperty =
DependencyProperty.Register(
nameof(DeadZone),
typeof(double),
typeof(VirtualJoystick),
new PropertyMetadata(0.05));
/// <summary>
/// 获取或设置死区比例,范围 [0.0, 1.0],默认值 0.05 | Gets or sets the dead zone ratio, range [0.0, 1.0], default 0.05
/// </summary>
public double DeadZone
{
get => (double)GetValue(DeadZoneProperty);
set => SetValue(DeadZoneProperty, value);
}
#endregion
#region ActiveMouseButton | ActiveMouseButton Read-Only Dependency Property
/// <summary>
/// ActiveMouseButton 只读依赖属性键 | ActiveMouseButton read-only dependency property key
/// </summary>
private static readonly DependencyPropertyKey ActiveMouseButtonPropertyKey =
DependencyProperty.RegisterReadOnly(
nameof(ActiveMouseButton),
typeof(MouseButtonType),
typeof(VirtualJoystick),
new PropertyMetadata(MouseButtonType.None));
/// <summary>
/// 当前激活的鼠标按键依赖属性 | Active mouse button dependency property
/// </summary>
public static readonly DependencyProperty ActiveMouseButtonProperty = ActiveMouseButtonPropertyKey.DependencyProperty;
/// <summary>
/// 获取当前激活的鼠标按键(None、Left、Right| Gets the currently active mouse button (None, Left, Right)
/// </summary>
public MouseButtonType ActiveMouseButton
{
get => (MouseButtonType)GetValue(ActiveMouseButtonProperty);
internal set => SetValue(ActiveMouseButtonPropertyKey, value);
}
#endregion
#region | Left Button Directional Icon Dependency Properties
/// <summary>
/// 左键拖动时上方图标内容依赖属性 | Left button top icon content dependency property
/// </summary>
public static readonly DependencyProperty LeftButtonTopIconProperty =
DependencyProperty.Register(
nameof(LeftButtonTopIcon),
typeof(object),
typeof(VirtualJoystick),
new PropertyMetadata(null));
/// <summary>
/// 获取或设置左键拖动时上方图标内容 | Gets or sets the top icon content when left button is dragging
/// </summary>
public object? LeftButtonTopIcon
{
get => GetValue(LeftButtonTopIconProperty);
set => SetValue(LeftButtonTopIconProperty, value);
}
/// <summary>
/// 左键拖动时下方图标内容依赖属性 | Left button bottom icon content dependency property
/// </summary>
public static readonly DependencyProperty LeftButtonBottomIconProperty =
DependencyProperty.Register(
nameof(LeftButtonBottomIcon),
typeof(object),
typeof(VirtualJoystick),
new PropertyMetadata(null));
/// <summary>
/// 获取或设置左键拖动时下方图标内容 | Gets or sets the bottom icon content when left button is dragging
/// </summary>
public object? LeftButtonBottomIcon
{
get => GetValue(LeftButtonBottomIconProperty);
set => SetValue(LeftButtonBottomIconProperty, value);
}
/// <summary>
/// 左键拖动时左方图标内容依赖属性 | Left button left icon content dependency property
/// </summary>
public static readonly DependencyProperty LeftButtonLeftIconProperty =
DependencyProperty.Register(
nameof(LeftButtonLeftIcon),
typeof(object),
typeof(VirtualJoystick),
new PropertyMetadata(null));
/// <summary>
/// 获取或设置左键拖动时左方图标内容 | Gets or sets the left icon content when left button is dragging
/// </summary>
public object? LeftButtonLeftIcon
{
get => GetValue(LeftButtonLeftIconProperty);
set => SetValue(LeftButtonLeftIconProperty, value);
}
/// <summary>
/// 左键拖动时右方图标内容依赖属性 | Left button right icon content dependency property
/// </summary>
public static readonly DependencyProperty LeftButtonRightIconProperty =
DependencyProperty.Register(
nameof(LeftButtonRightIcon),
typeof(object),
typeof(VirtualJoystick),
new PropertyMetadata(null));
/// <summary>
/// 获取或设置左键拖动时右方图标内容 | Gets or sets the right icon content when left button is dragging
/// </summary>
public object? LeftButtonRightIcon
{
get => GetValue(LeftButtonRightIconProperty);
set => SetValue(LeftButtonRightIconProperty, value);
}
#endregion
#region | Right Button Directional Icon Dependency Properties
/// <summary>
/// 右键拖动时上方图标内容依赖属性 | Right button top icon content dependency property
/// </summary>
public static readonly DependencyProperty RightButtonTopIconProperty =
DependencyProperty.Register(
nameof(RightButtonTopIcon),
typeof(object),
typeof(VirtualJoystick),
new PropertyMetadata(null));
/// <summary>
/// 获取或设置右键拖动时上方图标内容 | Gets or sets the top icon content when right button is dragging
/// </summary>
public object? RightButtonTopIcon
{
get => GetValue(RightButtonTopIconProperty);
set => SetValue(RightButtonTopIconProperty, value);
}
/// <summary>
/// 右键拖动时下方图标内容依赖属性 | Right button bottom icon content dependency property
/// </summary>
public static readonly DependencyProperty RightButtonBottomIconProperty =
DependencyProperty.Register(
nameof(RightButtonBottomIcon),
typeof(object),
typeof(VirtualJoystick),
new PropertyMetadata(null));
/// <summary>
/// 获取或设置右键拖动时下方图标内容 | Gets or sets the bottom icon content when right button is dragging
/// </summary>
public object? RightButtonBottomIcon
{
get => GetValue(RightButtonBottomIconProperty);
set => SetValue(RightButtonBottomIconProperty, value);
}
/// <summary>
/// 右键拖动时左方图标内容依赖属性 | Right button left icon content dependency property
/// </summary>
public static readonly DependencyProperty RightButtonLeftIconProperty =
DependencyProperty.Register(
nameof(RightButtonLeftIcon),
typeof(object),
typeof(VirtualJoystick),
new PropertyMetadata(null));
/// <summary>
/// 获取或设置右键拖动时左方图标内容 | Gets or sets the left icon content when right button is dragging
/// </summary>
public object? RightButtonLeftIcon
{
get => GetValue(RightButtonLeftIconProperty);
set => SetValue(RightButtonLeftIconProperty, value);
}
/// <summary>
/// 右键拖动时右方图标内容依赖属性 | Right button right icon content dependency property
/// </summary>
public static readonly DependencyProperty RightButtonRightIconProperty =
DependencyProperty.Register(
nameof(RightButtonRightIcon),
typeof(object),
typeof(VirtualJoystick),
new PropertyMetadata(null));
/// <summary>
/// 获取或设置右键拖动时右方图标内容 | Gets or sets the right icon content when right button is dragging
/// </summary>
public object? RightButtonRightIcon
{
get => GetValue(RightButtonRightIconProperty);
set => SetValue(RightButtonRightIconProperty, value);
}
#endregion
#region ThumbElement | ThumbElement Internal Property
/// <summary>
/// 操控点 UI 元素引用(从模板获取)| Thumb UI element reference (obtained from template)
/// </summary>
internal UIElement? ThumbElement { get; private set; }
#endregion
#region | Default Directional Icon Dependency Properties
/// <summary>
/// 默认状态上方图标内容依赖属性 | Default top icon content dependency property
/// </summary>
public static readonly DependencyProperty DefaultTopIconProperty =
DependencyProperty.Register(
nameof(DefaultTopIcon),
typeof(object),
typeof(VirtualJoystick),
new PropertyMetadata(null));
/// <summary>
/// 获取或设置默认状态上方图标内容 | Gets or sets the top icon content in default state
/// </summary>
public object? DefaultTopIcon
{
get => GetValue(DefaultTopIconProperty);
set => SetValue(DefaultTopIconProperty, value);
}
/// <summary>
/// 默认状态下方图标内容依赖属性 | Default bottom icon content dependency property
/// </summary>
public static readonly DependencyProperty DefaultBottomIconProperty =
DependencyProperty.Register(
nameof(DefaultBottomIcon),
typeof(object),
typeof(VirtualJoystick),
new PropertyMetadata(null));
/// <summary>
/// 获取或设置默认状态下方图标内容 | Gets or sets the bottom icon content in default state
/// </summary>
public object? DefaultBottomIcon
{
get => GetValue(DefaultBottomIconProperty);
set => SetValue(DefaultBottomIconProperty, value);
}
/// <summary>
/// 默认状态左方图标内容依赖属性 | Default left icon content dependency property
/// </summary>
public static readonly DependencyProperty DefaultLeftIconProperty =
DependencyProperty.Register(
nameof(DefaultLeftIcon),
typeof(object),
typeof(VirtualJoystick),
new PropertyMetadata(null));
/// <summary>
/// 获取或设置默认状态左方图标内容 | Gets or sets the left icon content in default state
/// </summary>
public object? DefaultLeftIcon
{
get => GetValue(DefaultLeftIconProperty);
set => SetValue(DefaultLeftIconProperty, value);
}
/// <summary>
/// 默认状态右方图标内容依赖属性 | Default right icon content dependency property
/// </summary>
public static readonly DependencyProperty DefaultRightIconProperty =
DependencyProperty.Register(
nameof(DefaultRightIcon),
typeof(object),
typeof(VirtualJoystick),
new PropertyMetadata(null));
/// <summary>
/// 获取或设置默认状态右方图标内容 | Gets or sets the right icon content in default state
/// </summary>
public object? DefaultRightIcon
{
get => GetValue(DefaultRightIconProperty);
set => SetValue(DefaultRightIconProperty, value);
}
#endregion
#region | Template Application
/// <summary>
/// 应用控件模板时查找操控点元素并设置平移变换 | Find thumb element and set up translate transform when template is applied
/// </summary>
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
// 查找模板中的操控点元素 | Find the thumb element in the template
ThumbElement = GetTemplateChild(PART_Thumb) as UIElement;
if (ThumbElement != null)
{
// 设置平移变换用于拖拽跟随 | Set translate transform for drag following
ThumbElement.RenderTransform = _thumbTransform;
}
}
#endregion
#region | Mouse Interaction Event Handlers
/// <summary>
/// 鼠标左键按下时开始拖拽 | Start dragging when left mouse button is pressed
/// </summary>
protected override void OnMouseLeftButtonDown(MouseButtonEventArgs e)
{
base.OnMouseLeftButtonDown(e);
// 如果已在拖拽中或控件未启用,忽略 | Ignore if already dragging or control is disabled
if (_isDragging || !IsEnabled)
return;
StartDrag(MouseButtonType.Left);
e.Handled = true;
}
/// <summary>
/// 鼠标右键按下时开始拖拽 | Start dragging when right mouse button is pressed
/// </summary>
protected override void OnMouseRightButtonDown(MouseButtonEventArgs e)
{
base.OnMouseRightButtonDown(e);
// 如果已在拖拽中或控件未启用,忽略 | Ignore if already dragging or control is disabled
if (_isDragging || !IsEnabled)
return;
StartDrag(MouseButtonType.Right);
e.Handled = true;
}
/// <summary>
/// 鼠标移动时更新操控点位置和输出值 | Update thumb position and output values when mouse moves
/// </summary>
protected override void OnMouseMove(MouseEventArgs e)
{
base.OnMouseMove(e);
if (!_isDragging)
return;
try
{
// 获取鼠标相对于控件的位置 | Get mouse position relative to the control
var mousePosition = e.GetPosition(this);
// 计算相对于中心的偏移量 | Calculate displacement from center
var dx = mousePosition.X - _centerPoint.X;
var dy = mousePosition.Y - _centerPoint.Y;
// 计算可用半径(取宽高较小值的一半)| Calculate available radius (half of the smaller dimension)
var radius = GetRadius();
if (radius <= 0)
return;
// 将位移限制在圆形区域内 | Clamp displacement within circular area
var (clampedDx, clampedDy) = JoystickCalculator.ClampToRadius(dx, dy, radius);
// 计算归一化输出(含死区映射)| Calculate normalized output (with dead zone mapping)
var (outputX, outputY) = JoystickCalculator.CalculateOutput(dx, dy, radius, DeadZone, JoystickMode);
// 更新输出值 | Update output values
OutputX = outputX;
OutputY = outputY;
// 更新操控点的平移变换位置(使用限制后的位移)| Update thumb translate transform (using clamped displacement)
_thumbTransform.X = clampedDx;
_thumbTransform.Y = clampedDy;
}
catch
{
// 异常时重置到中心位置并输出归零 | Reset to center and zero output on exception
ResetToCenter();
}
e.Handled = true;
}
/// <summary>
/// 鼠标左键释放时结束拖拽 | End dragging when left mouse button is released
/// </summary>
protected override void OnMouseLeftButtonUp(MouseButtonEventArgs e)
{
base.OnMouseLeftButtonUp(e);
// 仅当正在拖拽且激活按键为左键时处理 | Only handle when dragging with left button active
if (!_isDragging || ActiveMouseButton != MouseButtonType.Left)
return;
EndDrag();
e.Handled = true;
}
/// <summary>
/// 鼠标右键释放时结束拖拽 | End dragging when right mouse button is released
/// </summary>
protected override void OnMouseRightButtonUp(MouseButtonEventArgs e)
{
base.OnMouseRightButtonUp(e);
// 仅当正在拖拽且激活按键为右键时处理 | Only handle when dragging with right button active
if (!_isDragging || ActiveMouseButton != MouseButtonType.Right)
return;
EndDrag();
e.Handled = true;
}
#endregion
#region | Drag Helper Methods
/// <summary>
/// 开始拖拽操作 | Start drag operation
/// </summary>
/// <param name="buttonType">触发拖拽的鼠标按键类型 | Mouse button type that initiated the drag</param>
private void StartDrag(MouseButtonType buttonType)
{
// 捕获鼠标以接收控件外的鼠标事件 | Capture mouse to receive events outside the control
CaptureMouse();
// 设置激活的鼠标按键 | Set the active mouse button
ActiveMouseButton = buttonType;
// 计算控件中心点 | Calculate control center point
_centerPoint = new Point(ActualWidth / 2.0, ActualHeight / 2.0);
// 标记拖拽状态 | Mark dragging state
_isDragging = true;
}
/// <summary>
/// 结束拖拽操作 | End drag operation
/// </summary>
private void EndDrag()
{
// 重置到中心位置并归零输出 | Reset to center and zero output
ResetToCenter();
// 释放鼠标捕获 | Release mouse capture
ReleaseMouseCapture();
}
/// <summary>
/// 重置操控点到中心位置,输出归零,清除拖拽状态 | Reset thumb to center, zero output, clear drag state
/// </summary>
internal void ResetToCenter()
{
// 输出值归零 | Zero output values
OutputX = 0.0;
OutputY = 0.0;
// 清除激活的鼠标按键 | Clear active mouse button
ActiveMouseButton = MouseButtonType.None;
// 清除拖拽状态 | Clear dragging state
_isDragging = false;
// 重置操控点平移变换到中心 | Reset thumb translate transform to center
_thumbTransform.X = 0.0;
_thumbTransform.Y = 0.0;
}
/// <summary>
/// 获取可用半径(取宽高较小值的一半)| Get available radius (half of the smaller dimension)
/// </summary>
/// <returns>可用半径 | Available radius</returns>
private double GetRadius()
{
return Math.Min(ActualWidth, ActualHeight) / 2.0;
}
#endregion
}
}