在 WPF 中,如果是鼠标点击拖动窗口坐标,可以调用 Window 的 DragMove 方法,但是如果是触摸,就需要自己调用 Win32 的方法实现
在 WPF 中,调用 Window 的 DragMove 方法要求鼠标的左键(主键)按下,否则将会抛出如下代码
System.InvalidOperationException:“只能在按下主鼠标按钮时调用 DragMove。”
或英文版的代码
System.InvalidOperationException:"Can only call DragMove when primary mouse button is down"
因此想要在 WPF 中使用手指 finger 进行 Touch 触摸拖拽窗口,拖动修改窗口坐标就需要用到 Win32 的方法了。相信大家都知道,在修改某个容器的坐标的时候,不能使用这个容器内的坐标做参考,所以在 Touch 拖动修改窗口坐标的时候,就不能使用监听窗口的事件拿到的坐标来作为参考
想要能平滑的移动窗口,就需要获取相对于屏幕的坐标,而如果此时处理多指的 Manipulation 的动作,那么整个逻辑将会非常复杂。本文仅仅支持使用一个手指的移动,因为使用了 GetCursorPos 的方法
当然了,此时假装是支持多指拖动也是可以的,只需要在进行多指触摸的时候开启拖动就可以了,此时用户的交互上不会有很大的差别
在开始之前,咱来封装一个类 DragMoveWindowHelper 用来在触摸下拖动窗口
public static class DragMoveWindowHelper
{
public static void DragMove(Window window)
{
// 这里的 DragMoveMode 在下文实现
var dragMoveMode = new DragMoveMode(window);
dragMoveMode.Start();
}
}
上面代码的 DragMoveMode 类放在下文实现。在封装完成了 DragMoveWindowHelper 类就可以尝试在拖动的时候使用,如下面代码
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
TouchDown += MainWindow_TouchDown;
TouchUp += MainWindow_TouchUp;
}
private void MainWindow_TouchUp(object sender, TouchEventArgs e)
{
_currentTouchCount--;
}
private void MainWindow_TouchDown(object sender, TouchEventArgs e)
{
CaptureTouch(e.TouchDevice);
if (_currentTouchCount == 0)
{
DragMoveWindowHelper.DragMove(this);
}
_currentTouchCount++;
}
private uint _currentTouchCount;
}
上面代码有一点需要小心就是 CaptureTouch 是必备的,否则你会发现拖动的时候,拖动太快了,就丢失触摸设备了,触摸设备被你窗口后面的其他软件抓了
下面开始实现 DragMoveMode 也就是核心的通过触摸拖动窗口的逻辑
大概对外的接口方法实现请看代码
class DragMoveMode
{
public DragMoveMode(Window window)
{
_window = window;
}
public void Start()
{
var window = _window;
window.PreviewMouseMove += Window_PreviewMouseMove;
window.PreviewMouseUp += Window_PreviewMouseUp;
window.LostMouseCapture += Window_LostMouseCapture;
}
public void Stop()
{
Window window = _window;
window.PreviewMouseMove -= Window_PreviewMouseMove;
window.PreviewMouseUp -= Window_PreviewMouseUp;
window.LostMouseCapture -= Window_LostMouseCapture;
}
private readonly Window _window;
}
在上面代码里面监听 PreviewMouseMove 是为了获取移动的时机,而不是为了获取相对的坐标。而 PreviewMouseUp 可以用来了解啥时候结束。当然了 LostMouseCapture 也需要监听,和 PreviewMouseUp 一样用来了解啥时候结束
在 Window_PreviewMouseMove 方法需要先判断是否第一次进入移动,因此咱没有监听 MouseDown 方法。为什么没有监听 MouseDown 方法,是因为在上层业务此时业务调用 MoseDown 完成
判断是否第一次进入移动需要一个辅助的字段,咱定义一个叫上一次点击的坐标字段
private Win32.User32.Point? _lastPoint;
上面代码的 Win32.User32 是我定义的代码,这些定义将会放在本文最后
判断是第一次进入移动可以使用下面代码
private void Window_PreviewMouseMove(object sender, MouseEventArgs e)
{
Win32.User32.GetCursorPos(out var lpPoint);
if (_lastPoint == null)
{
_lastPoint = lpPoint;
_window.CaptureMouse();
}
}
通过 GetCursorPos 的 Win32 方法可以拿到相对于屏幕坐标的鼠标坐标,而触摸默认会将第一个触摸点转换为鼠标坐标,因此拿到的坐标点不是相对于窗口内的,这样就能做到在移动的时候不会抖
接下来判断相对上一次的移动距离,如下面代码
var dx = lpPoint.X - _lastPoint.Value.X;
var dy = lpPoint.Y - _lastPoint.Value.Y;
Debug.WriteLine($"dx={dx} dy={dy}");
拿到的 dx 和 dy 就可以用来设置窗口的左上角坐标了。而此时不能通过 Window 的 Top 和 Left 属性获取,这两个属性的值使用的是 WPF 单位和坐标,而咱计算的 dx 和 dy 是相对于屏幕的坐标,因此需要调用 GetWindowRect 这个 win32 方法获取窗口所在屏幕的坐标
设置窗口坐标也需要使用屏幕坐标来设置,需要调用 SetWindowPos 方法,代码如下
var handle = new WindowInteropHelper(_window).Handle;
Win32.User32.GetWindowRect(handle, out var lpRect);
Win32.User32.SetWindowPos(handle, IntPtr.Zero, lpRect.Left + dx, lpRect.Top + dy, 0, 0,
(int) (Win32.User32.WindowPositionFlags.SWP_NOSIZE |
Win32.User32.WindowPositionFlags.SWP_NOZORDER));
这个 Window_PreviewMouseMove 方法代码如下
private void Window_PreviewMouseMove(object sender, MouseEventArgs e)
{
Win32.User32.GetCursorPos(out var lpPoint);
if (_lastPoint == null)
{
_lastPoint = lpPoint;
_window.CaptureMouse();
}
var dx = lpPoint.X - _lastPoint.Value.X;
var dy = lpPoint.Y - _lastPoint.Value.Y;
Debug.WriteLine($"dx={dx} dy&