Common类库新增摇杆控件。

This commit is contained in:
QI Mingxuan
2026-04-22 20:48:39 +08:00
parent 1279885924
commit be03bdf34e
5 changed files with 976 additions and 0 deletions
+629
View File
@@ -0,0 +1,629 @@
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
}
}