using System; using System.Windows; using System.Windows.Controls; using System.Windows.Input; using System.Windows.Media; namespace XP.Common.Controls { /// /// 虚拟摇杆自定义控件,提供圆形区域内的鼠标拖拽操控能力 | Virtual joystick custom control 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. /// [TemplatePart(Name = PART_Thumb, Type = typeof(UIElement))] public class VirtualJoystick : Control { /// /// 操控点模板部件名称 | Thumb template part name /// private const string PART_Thumb = "PART_Thumb"; #region 私有字段 | Private Fields /// /// 是否正在拖拽 | Whether dragging is in progress /// private bool _isDragging; /// /// 控件中心点坐标 | Control center point coordinates /// private Point _centerPoint; /// /// 操控点的平移变换,用于跟随鼠标移动 | Translate transform for thumb element to follow mouse movement /// 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 /// /// 摇杆轴模式依赖属性 | Joystick axis mode dependency property /// public static readonly DependencyProperty JoystickModeProperty = DependencyProperty.Register( nameof(JoystickMode), typeof(JoystickMode), typeof(VirtualJoystick), new PropertyMetadata(JoystickMode.DualAxis)); /// /// 获取或设置摇杆轴模式(双轴或单轴 Y)| Gets or sets the joystick axis mode (DualAxis or SingleAxisY) /// public JoystickMode JoystickMode { get => (JoystickMode)GetValue(JoystickModeProperty); set => SetValue(JoystickModeProperty, value); } #endregion #region OutputX 只读依赖属性 | OutputX Read-Only Dependency Property /// /// OutputX 只读依赖属性键 | OutputX read-only dependency property key /// private static readonly DependencyPropertyKey OutputXPropertyKey = DependencyProperty.RegisterReadOnly( nameof(OutputX), typeof(double), typeof(VirtualJoystick), new PropertyMetadata(0.0)); /// /// X 轴归一化输出依赖属性 | X-axis normalized output dependency property /// public static readonly DependencyProperty OutputXProperty = OutputXPropertyKey.DependencyProperty; /// /// 获取 X 轴归一化输出值,范围 [-1.0, 1.0] | Gets the X-axis normalized output value, range [-1.0, 1.0] /// public double OutputX { get => (double)GetValue(OutputXProperty); internal set => SetValue(OutputXPropertyKey, value); } #endregion #region OutputY 只读依赖属性 | OutputY Read-Only Dependency Property /// /// OutputY 只读依赖属性键 | OutputY read-only dependency property key /// private static readonly DependencyPropertyKey OutputYPropertyKey = DependencyProperty.RegisterReadOnly( nameof(OutputY), typeof(double), typeof(VirtualJoystick), new PropertyMetadata(0.0)); /// /// Y 轴归一化输出依赖属性 | Y-axis normalized output dependency property /// public static readonly DependencyProperty OutputYProperty = OutputYPropertyKey.DependencyProperty; /// /// 获取 Y 轴归一化输出值,范围 [-1.0, 1.0] | Gets the Y-axis normalized output value, range [-1.0, 1.0] /// public double OutputY { get => (double)GetValue(OutputYProperty); internal set => SetValue(OutputYPropertyKey, value); } #endregion #region DeadZone 依赖属性 | DeadZone Dependency Property /// /// 死区比例依赖属性 | Dead zone ratio 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 | Gets or sets the dead zone ratio, range [0.0, 1.0], default 0.05 /// public double DeadZone { get => (double)GetValue(DeadZoneProperty); set => SetValue(DeadZoneProperty, value); } #endregion #region ActiveMouseButton 只读依赖属性 | ActiveMouseButton Read-Only Dependency Property /// /// ActiveMouseButton 只读依赖属性键 | ActiveMouseButton read-only dependency property key /// private static readonly DependencyPropertyKey ActiveMouseButtonPropertyKey = DependencyProperty.RegisterReadOnly( nameof(ActiveMouseButton), typeof(MouseButtonType), typeof(VirtualJoystick), new PropertyMetadata(MouseButtonType.None)); /// /// 当前激活的鼠标按键依赖属性 | Active mouse button dependency property /// public static readonly DependencyProperty ActiveMouseButtonProperty = ActiveMouseButtonPropertyKey.DependencyProperty; /// /// 获取当前激活的鼠标按键(None、Left、Right)| Gets the currently active mouse button (None, Left, Right) /// public MouseButtonType ActiveMouseButton { get => (MouseButtonType)GetValue(ActiveMouseButtonProperty); internal set => SetValue(ActiveMouseButtonPropertyKey, value); } #endregion #region 左键四向图标依赖属性 | Left Button Directional Icon Dependency Properties /// /// 左键拖动时上方图标内容依赖属性 | Left button top icon content dependency property /// public static readonly DependencyProperty LeftButtonTopIconProperty = DependencyProperty.Register( nameof(LeftButtonTopIcon), typeof(object), typeof(VirtualJoystick), new PropertyMetadata(null)); /// /// 获取或设置左键拖动时上方图标内容 | Gets or sets the top icon content when left button is dragging /// public object? LeftButtonTopIcon { get => GetValue(LeftButtonTopIconProperty); set => SetValue(LeftButtonTopIconProperty, value); } /// /// 左键拖动时下方图标内容依赖属性 | Left button bottom icon content dependency property /// public static readonly DependencyProperty LeftButtonBottomIconProperty = DependencyProperty.Register( nameof(LeftButtonBottomIcon), typeof(object), typeof(VirtualJoystick), new PropertyMetadata(null)); /// /// 获取或设置左键拖动时下方图标内容 | Gets or sets the bottom icon content when left button is dragging /// public object? LeftButtonBottomIcon { get => GetValue(LeftButtonBottomIconProperty); set => SetValue(LeftButtonBottomIconProperty, value); } /// /// 左键拖动时左方图标内容依赖属性 | Left button left icon content dependency property /// public static readonly DependencyProperty LeftButtonLeftIconProperty = DependencyProperty.Register( nameof(LeftButtonLeftIcon), typeof(object), typeof(VirtualJoystick), new PropertyMetadata(null)); /// /// 获取或设置左键拖动时左方图标内容 | Gets or sets the left icon content when left button is dragging /// public object? LeftButtonLeftIcon { get => GetValue(LeftButtonLeftIconProperty); set => SetValue(LeftButtonLeftIconProperty, value); } /// /// 左键拖动时右方图标内容依赖属性 | Left button right icon content dependency property /// public static readonly DependencyProperty LeftButtonRightIconProperty = DependencyProperty.Register( nameof(LeftButtonRightIcon), typeof(object), typeof(VirtualJoystick), new PropertyMetadata(null)); /// /// 获取或设置左键拖动时右方图标内容 | Gets or sets the right icon content when left button is dragging /// public object? LeftButtonRightIcon { get => GetValue(LeftButtonRightIconProperty); set => SetValue(LeftButtonRightIconProperty, value); } #endregion #region 右键四向图标依赖属性 | Right Button Directional Icon Dependency Properties /// /// 右键拖动时上方图标内容依赖属性 | Right button top icon content dependency property /// public static readonly DependencyProperty RightButtonTopIconProperty = DependencyProperty.Register( nameof(RightButtonTopIcon), typeof(object), typeof(VirtualJoystick), new PropertyMetadata(null)); /// /// 获取或设置右键拖动时上方图标内容 | Gets or sets the top icon content when right button is dragging /// public object? RightButtonTopIcon { get => GetValue(RightButtonTopIconProperty); set => SetValue(RightButtonTopIconProperty, value); } /// /// 右键拖动时下方图标内容依赖属性 | Right button bottom icon content dependency property /// public static readonly DependencyProperty RightButtonBottomIconProperty = DependencyProperty.Register( nameof(RightButtonBottomIcon), typeof(object), typeof(VirtualJoystick), new PropertyMetadata(null)); /// /// 获取或设置右键拖动时下方图标内容 | Gets or sets the bottom icon content when right button is dragging /// public object? RightButtonBottomIcon { get => GetValue(RightButtonBottomIconProperty); set => SetValue(RightButtonBottomIconProperty, value); } /// /// 右键拖动时左方图标内容依赖属性 | Right button left icon content dependency property /// public static readonly DependencyProperty RightButtonLeftIconProperty = DependencyProperty.Register( nameof(RightButtonLeftIcon), typeof(object), typeof(VirtualJoystick), new PropertyMetadata(null)); /// /// 获取或设置右键拖动时左方图标内容 | Gets or sets the left icon content when right button is dragging /// public object? RightButtonLeftIcon { get => GetValue(RightButtonLeftIconProperty); set => SetValue(RightButtonLeftIconProperty, value); } /// /// 右键拖动时右方图标内容依赖属性 | Right button right icon content dependency property /// public static readonly DependencyProperty RightButtonRightIconProperty = DependencyProperty.Register( nameof(RightButtonRightIcon), typeof(object), typeof(VirtualJoystick), new PropertyMetadata(null)); /// /// 获取或设置右键拖动时右方图标内容 | Gets or sets the right icon content when right button is dragging /// public object? RightButtonRightIcon { get => GetValue(RightButtonRightIconProperty); set => SetValue(RightButtonRightIconProperty, value); } #endregion #region ThumbElement 内部属性 | ThumbElement Internal Property /// /// 操控点 UI 元素引用(从模板获取)| Thumb UI element reference (obtained from template) /// internal UIElement? ThumbElement { get; private set; } #endregion #region 默认四向图标依赖属性 | Default Directional Icon Dependency Properties /// /// 默认状态上方图标内容依赖属性 | Default top icon content dependency property /// public static readonly DependencyProperty DefaultTopIconProperty = DependencyProperty.Register( nameof(DefaultTopIcon), typeof(object), typeof(VirtualJoystick), new PropertyMetadata(null)); /// /// 获取或设置默认状态上方图标内容 | Gets or sets the top icon content in default state /// public object? DefaultTopIcon { get => GetValue(DefaultTopIconProperty); set => SetValue(DefaultTopIconProperty, value); } /// /// 默认状态下方图标内容依赖属性 | Default bottom icon content dependency property /// public static readonly DependencyProperty DefaultBottomIconProperty = DependencyProperty.Register( nameof(DefaultBottomIcon), typeof(object), typeof(VirtualJoystick), new PropertyMetadata(null)); /// /// 获取或设置默认状态下方图标内容 | Gets or sets the bottom icon content in default state /// public object? DefaultBottomIcon { get => GetValue(DefaultBottomIconProperty); set => SetValue(DefaultBottomIconProperty, value); } /// /// 默认状态左方图标内容依赖属性 | Default left icon content dependency property /// public static readonly DependencyProperty DefaultLeftIconProperty = DependencyProperty.Register( nameof(DefaultLeftIcon), typeof(object), typeof(VirtualJoystick), new PropertyMetadata(null)); /// /// 获取或设置默认状态左方图标内容 | Gets or sets the left icon content in default state /// public object? DefaultLeftIcon { get => GetValue(DefaultLeftIconProperty); set => SetValue(DefaultLeftIconProperty, value); } /// /// 默认状态右方图标内容依赖属性 | Default right icon content dependency property /// public static readonly DependencyProperty DefaultRightIconProperty = DependencyProperty.Register( nameof(DefaultRightIcon), typeof(object), typeof(VirtualJoystick), new PropertyMetadata(null)); /// /// 获取或设置默认状态右方图标内容 | Gets or sets the right icon content in default state /// public object? DefaultRightIcon { get => GetValue(DefaultRightIconProperty); set => SetValue(DefaultRightIconProperty, value); } #endregion #region 模板应用 | Template Application /// /// 应用控件模板时查找操控点元素并设置平移变换 | Find thumb element and set up translate transform when template is applied /// 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 /// /// 鼠标左键按下时开始拖拽 | Start dragging when left mouse button is pressed /// 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; } /// /// 鼠标右键按下时开始拖拽 | Start dragging when right mouse button is pressed /// 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; } /// /// 鼠标移动时更新操控点位置和输出值 | Update thumb position and output values when mouse moves /// 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; } /// /// 鼠标左键释放时结束拖拽 | End dragging when left mouse button is released /// 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; } /// /// 鼠标右键释放时结束拖拽 | End dragging when right mouse button is released /// 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 /// /// 开始拖拽操作 | Start drag operation /// /// 触发拖拽的鼠标按键类型 | Mouse button type that initiated the drag 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; } /// /// 结束拖拽操作 | End drag operation /// private void EndDrag() { // 重置到中心位置并归零输出 | Reset to center and zero output ResetToCenter(); // 释放鼠标捕获 | Release mouse capture ReleaseMouseCapture(); } /// /// 重置操控点到中心位置,输出归零,清除拖拽状态 | Reset thumb to center, zero output, clear drag state /// 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; } /// /// 获取可用半径(取宽高较小值的一半)| Get available radius (half of the smaller dimension) /// /// 可用半径 | Available radius private double GetRadius() { return Math.Min(ActualWidth, ActualHeight) / 2.0; } #endregion } }