在 WPF 中,若是是鼠标点击拖动窗口坐标,能够调用 Window 的 DragMove 方法,可是若是是触摸,就须要本身调用 Win32 的方法实现git
在 WPF 中,调用 Window 的 DragMove 方法要求鼠标的左键(主键)按下,不然将会抛出以下代码github
System.InvalidOperationException:“只能在按下主鼠标按钮时调用 DragMove。”
或英文版的代码app
System.InvalidOperationException:"Can only call DragMove when primary mouse button is down"
所以想要在 WPF 中使用手指 finger 进行 Touch 触摸拖拽窗口,拖动修改窗口坐标就须要用到 Win32 的方法了。相信你们都知道,在修改某个容器的坐标的时候,不能使用这个容器内的坐标作参考,因此在 Touch 拖动修改窗口坐标的时候,就不能使用监听窗口的事件拿到的坐标来做为参考ide
想要能平滑的移动窗口,就须要获取相对于屏幕的坐标,而若是此时处理多指的 Manipulation 的动做,那么整个逻辑将会很是复杂。本文仅仅支持使用一个手指的移动,由于使用了 GetCursorPos 的方法函数
固然了,此时伪装是支持多指拖动也是能够的,只须要在进行多指触摸的时候开启拖动就能够了,此时用户的交互上不会有很大的差异post
在开始以前,咱来封装一个类 DragMoveWindowHelper 用来在触摸下拖动窗口测试
public static class DragMoveWindowHelper
{
public static void DragMove(Window window)
{
// 这里的 DragMoveMode 在下文实现
var dragMoveMode = new DragMoveMode(window);
dragMoveMode.Start();
}
}
上面代码的 DragMoveMode 类放在下文实现。在封装完成了 DragMoveWindowHelper 类就能够尝试在拖动的时候使用,以下面代码ui
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 是必备的,不然你会发现拖动的时候,拖动太快了,就丢失触摸设备了,触摸设备被你窗口后面的其余软件抓了this
下面开始实现 DragMoveMode 也就是核心的经过触摸拖动窗口的逻辑spa
大概对外的接口方法实现请看代码
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={dy}");
// 如下的 60 是表示最大移动速度
if (Math.Abs(dx) < 60 && Math.Abs(dy) < 60)
{
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));
}
_lastPoint = lpPoint;
}
在 Window_PreviewMouseUp 和 Window_LostMouseCapture 方法调用的是清理的代码,解决内存泄露
private void Window_LostMouseCapture(object sender, MouseEventArgs e)
{
Stop();
}
private void Window_PreviewMouseUp(object sender, MouseButtonEventArgs e)
{
Stop();
}
大概就完成了触摸拖动窗口的逻辑,下面代码是 Win32 的代码,须要加到你的代码里面,这样才能构建经过
private static class Win32
{
public static class User32
{
/// <summary>
/// 改变一个子窗口、弹出式窗口和顶层窗口的尺寸、位置和 Z 序。
/// </summary>
/// <param name="hWnd">窗口句柄。</param>
/// <param name="hWndInsertAfter">
/// 在z序中的位于被置位的窗口前的窗口句柄。该参数必须为一个窗口句柄,或下列值之一:
/// <para>HWND_BOTTOM:将窗口置于 Z 序的底部。若是参数hWnd标识了一个顶层窗口,则窗口失去顶级位置,而且被置在其余窗口的底部。</para>
/// <para>HWND_NOTOPMOST:将窗口置于全部非顶层窗口之上(即在全部顶层窗口以后)。若是窗口已是非顶层窗口则该标志不起做用。</para>
/// <para>HWND_TOP:将窗口置于Z序的顶部。</para>
/// <para>HWND_TOPMOST:将窗口置于全部非顶层窗口之上。即便窗口未被激活窗口也将保持顶级位置。</para>
/// </param>
/// <param name="x">以客户坐标指定窗口新位置的左边界。</param>
/// <param name="y">以客户坐标指定窗口新位置的顶边界。</param>
/// <param name="cx">以像素指定窗口的新的宽度。</param>
/// <param name="cy">以像素指定窗口的新的高度。</param>
/// <param name="wFlagslong">
/// 窗口尺寸和定位的标志。该参数能够是下列值的组合:
/// <para>SWP_ASYNCWINDOWPOS:若是调用进程不拥有窗口,系统会向拥有窗口的线程发出需求。这就防止调用线程在其余线程处理需求的时候发生死锁。</para>
/// <para>SWP_DEFERERASE:防止产生 WM_SYNCPAINT 消息。</para>
/// <para>SWP_DRAWFRAME:在窗口周围画一个边框(定义在窗口类描述中)。</para>
/// <para>SWP_FRAMECHANGED:给窗口发送 WM_NCCALCSIZE 消息,即便窗口尺寸没有改变也会发送该消息。若是未指定这个标志,只有在改变了窗口尺寸时才发送 WM_NCCALCSIZE。</para>
/// <para>SWP_HIDEWINDOW:隐藏窗口。</para>
/// <para>SWP_NOACTIVATE:不激活窗口。若是未设置标志,则窗口被激活,并被设置到其余最高级窗口或非最高级组的顶部(根据参数hWndlnsertAfter设置)。</para>
/// <para>SWP_NOCOPYBITS:清除客户区的全部内容。若是未设置该标志,客户区的有效内容被保存而且在窗口尺寸更新和重定位后拷贝回客户区。</para>
/// <para>SWP_NOMOVE:维持当前位置(忽略X和Y参数)。</para>
/// <para>SWP_NOOWNERZORDER:不改变 Z 序中的全部者窗口的位置。</para>
/// <para>SWP_NOREDRAW:不重画改变的内容。若是设置了这个标志,则不发生任何重画动做。适用于客户区和非客户区(包括标题栏和滚动条)和任何因为窗回移动而露出的父窗口的全部部分。若是设置了这个标志,应用程序必须明确地使窗口无效并区重画窗口的任何部分和父窗口须要重画的部分。</para>
/// <para>SWP_NOREPOSITION:与 SWP_NOOWNERZORDER 标志相同。</para>
/// <para>SWP_NOSENDCHANGING:防止窗口接收 WM_WINDOWPOSCHANGING 消息。</para>
/// <para>SWP_NOSIZE:维持当前尺寸(忽略 cx 和 cy 参数)。</para>
/// <para>SWP_NOZORDER:维持当前 Z 序(忽略 hWndlnsertAfter 参数)。</para>
/// <para>SWP_SHOWWINDOW:显示窗口。</para>
/// </param>
/// <returns>若是函数成功,返回值为非零;若是函数失败,返回值为零。若想得到更多错误消息,请调用 GetLastError 函数。</returns>
[DllImport(LibraryName, ExactSpelling = true, SetLastError = true)]
public static extern Int32 SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, Int32 x, Int32 y, Int32 cx,
Int32 cy, Int32 wFlagslong);
[Flags]
public enum WindowPositionFlags
{
/// <summary>
/// If the calling thread and the thread that owns the window are attached to different input queues, the system posts
/// the request to the thread that owns the window. This prevents the calling thread from blocking its execution while
/// other threads process the request.
/// </summary>
SWP_ASYNCWINDOWPOS = 0x4000,
/// <summary>
/// Prevents generation of the WM_SYNCPAINT message.
/// </summary>
SWP_DEFERERASE = 0x2000,
/// <summary>
/// Draws a frame (defined in the window's class description) around the window.
/// </summary>
SWP_DRAWFRAME = 0x0020,
/// <summary>
/// Applies new frame styles set using the SetWindowLong function. Sends a WM_NCCALCSIZE message to the window, even if
/// the window's size is not being changed. If this flag is not specified, WM_NCCALCSIZE is sent only when the window's
/// size is being changed.
/// </summary>
SWP_FRAMECHANGED = 0x0020,
/// <summary>
/// Hides the window.
/// </summary>
SWP_HIDEWINDOW = 0x0080,
/// <summary>
/// Does not activate the window. If this flag is not set, the window is activated and moved to the top of either the
/// topmost or non-topmost group (depending on the setting of the hWndInsertAfter parameter).
/// </summary>
SWP_NOACTIVATE = 0x0010,
/// <summary>
/// Discards the entire contents of the client area. If this flag is not specified, the valid contents of the client
/// area are saved and copied back into the client area after the window is sized or repositioned.
/// </summary>
SWP_NOCOPYBITS = 0x0100,
/// <summary>
/// Retains the current position (ignores X and Y parameters).
/// </summary>
SWP_NOMOVE = 0x0002,
/// <summary>
/// Does not change the owner window's position in the Z order.
/// </summary>
SWP_NOOWNERZORDER = 0x0200,
/// <summary>
/// Does not redraw changes. If this flag is set, no repainting of any kind occurs. This applies to the client area,
/// the nonclient area (including the title bar and scroll bars), and any part of the parent window uncovered as a
/// result of the window being moved. When this flag is set, the application must explicitly invalidate or redraw any
/// parts of the window and parent window that need redrawing.
/// </summary>
SWP_NOREDRAW = 0x0008,
/// <summary>
/// Same as the SWP_NOOWNERZORDER flag.
/// </summary>
SWP_NOREPOSITION = 0x0200,
/// <summary>
/// Prevents the window from receiving the WM_WINDOWPOSCHANGING message.
/// </summary>
SWP_NOSENDCHANGING = 0x0400,
/// <summary>
/// Retains the current size (ignores the cx and cy parameters).
/// </summary>
SWP_NOSIZE = 0x0001,
/// <summary>
/// Retains the current Z order (ignores the hWndInsertAfter parameter).
/// </summary>
SWP_NOZORDER = 0x0004,
/// <summary>
/// Displays the window.
/// </summary>
SWP_SHOWWINDOW = 0x0040
}
public const string LibraryName = "user32";
/// <summary>
/// 获取的是以屏幕为坐标轴窗口坐标
/// </summary>
/// <param name="hWnd"></param>
/// <param name="lpRect"></param>
/// <returns></returns>
[return: MarshalAs(UnmanagedType.Bool)]
[DllImport(LibraryName, ExactSpelling = true)]
public static extern bool GetWindowRect(IntPtr hWnd, out Rectangle lpRect);
/// <summary>
/// 在 Win32 函数使用的矩形
/// </summary>
[StructLayout(LayoutKind.Sequential)]
public partial struct Rectangle : IEquatable<Rectangle>
{
/// <summary>
/// 建立在 Win32 函数使用的矩形
/// </summary>
/// <param name="left"></param>
/// <param name="top"></param>
/// <param name="right"></param>
/// <param name="bottom"></param>
public Rectangle(int left = 0, int top = 0, int right = 0, int bottom = 0)
{
Left = left;
Top = top;
Right = right;
Bottom = bottom;
}
/// <summary>
/// 建立在 Win32 函数使用的矩形
/// </summary>
/// <param name="width">矩形的宽度</param>
/// <param name="height">矩形的高度</param>
public Rectangle(int width = 0, int height = 0) : this(0, 0, width, height)
{
}
public int Left;
public int Top;
public int Right;
public int Bottom;
public bool Equals(Rectangle other)
{
return (Left == other.Left) && (Right == other.Right) && (Top == other.Top) &&
(Bottom == other.Bottom);
}
public override bool Equals(object obj)
{
return obj is Rectangle && Equals((Rectangle) obj);
}
public static bool operator ==(Rectangle left, Rectangle right)
{
return left.Equals(right);
}
public static bool operator !=(Rectangle left, Rectangle right)
{
return !(left == right);
}
public override int GetHashCode()
{
unchecked
{
var hashCode = (int) Left;
hashCode = (hashCode * 397) ^ (int) Top;
hashCode = (hashCode * 397) ^ (int) Right;
hashCode = (hashCode * 397) ^ (int) Bottom;
return hashCode;
}
}
/// <summary>
/// 获取当前矩形是否空矩形
/// </summary>
public bool IsEmpty => this.Left == 0 && this.Top == 0 && this.Right == 0 && this.Bottom == 0;
/// <summary>
/// 矩形的宽度
/// </summary>
public int Width
{
get { return unchecked((int) (Right - Left)); }
set { Right = unchecked((int) (Left + value)); }
}
/// <summary>
/// 矩形的高度
/// </summary>
public int Height
{
get { return unchecked((int) (Bottom - Top)); }
set { Bottom = unchecked((int) (Top + value)); }
}
/// <summary>
/// 经过 x、y 坐标和宽度高度建立矩形
/// </summary>
/// <param name="x"></param>
/// <param name="y"></param>
/// <param name="width"></param>
/// <param name="height"></param>
/// <returns></returns>
public static Rectangle Create(int x, int y, int width, int height)
{
unchecked
{
return new Rectangle(x, y, (int) (width + x), (int) (height + y));
}
}
public static Rectangle From(ref Rectangle lvalue, ref Rectangle rvalue,
Func<int, int, int> leftTopOperation,
Func<int, int, int> rightBottomOperation = null)
{
if (rightBottomOperation == null)
rightBottomOperation = leftTopOperation;
return new Rectangle(
leftTopOperation(lvalue.Left, rvalue.Left),
leftTopOperation(lvalue.Top, rvalue.Top),
rightBottomOperation(lvalue.Right, rvalue.Right),
rightBottomOperation(lvalue.Bottom, rvalue.Bottom)
);
}
public void Add(Rectangle value)
{
Add(ref this, ref value);
}
public void Subtract(Rectangle value)
{
Subtract(ref this, ref value);
}
public void Multiply(Rectangle value)
{
Multiply(ref this, ref value);
}
public void Divide(Rectangle value)
{
Divide(ref this, ref value);
}
public void Deflate(Rectangle value)
{
Deflate(ref this, ref value);
}
public void Inflate(Rectangle value)
{
Inflate(ref this, ref value);
}
public void Offset(int x, int y)
{
Offset(ref this, x, y);
}
public void OffsetTo(int x, int y)
{
OffsetTo(ref this, x, y);
}
public void Scale(int x, int y)
{
Scale(ref this, x, y);
}
public void ScaleTo(int x, int y)
{
ScaleTo(ref this, x, y);
}
public static void Add(ref Rectangle lvalue, ref Rectangle rvalue)
{
lvalue.Left += rvalue.Left;
lvalue.Top += rvalue.Top;
lvalue.Right += rvalue.Right;
lvalue.Bottom += rvalue.Bottom;
}
public static void Subtract(ref Rectangle lvalue, ref Rectangle rvalue)
{
lvalue.Left -= rvalue.Left;
lvalue.Top -= rvalue.Top;
lvalue.Right -= rvalue.Right;
lvalue.Bottom -= rvalue.Bottom;
}
public static void Multiply(ref Rectangle lvalue, ref Rectangle rvalue)
{
lvalue.Left *= rvalue.Left;
lvalue.Top *= rvalue.Top;
lvalue.Right *= rvalue.Right;
lvalue.Bottom *= rvalue.Bottom;
}
public static void Divide(ref Rectangle lvalue, ref Rectangle rvalue)
{
lvalue.Left /= rvalue.Left;
lvalue.Top /= rvalue.Top;
lvalue.Right /= rvalue.Right;
lvalue.Bottom /= rvalue.Bottom;
}
public static void Deflate(ref Rectangle target, ref Rectangle deflation)
{
target.Top += deflation.Top;
target.Left += deflation.Left;
target.Bottom -= deflation.Bottom;
target.Right -= deflation.Right;
}
public static void Inflate(ref Rectangle target, ref Rectangle inflation)
{
target.Top -= inflation.Top;
target.Left -= inflation.Left;
target.Bottom += inflation.Bottom;
target.Right += inflation.Right;
}
public static void Offset(ref Rectangle target, int x, int y)
{
target.Top += y;
target.Left += x;
target.Bottom += y;
target.Right += x;
}
public static void OffsetTo(ref Rectangle target, int x, int y)
{
var width = target.Width;
var height = target.Height;
target.Left = x;
target.Top = y;
target.Right = width;
target.Bottom = height;
}
public static void Scale(ref Rectangle target, int x, int y)
{
target.Top *= y;
target.Left *= x;
target.Bottom *= y;
target.Right *= x;
}
public static void ScaleTo(ref Rectangle target, int x, int y)
{
unchecked
{
x = (int) (target.Left / x);
y = (int) (target.Top / y);
}
Scale(ref target, x, y);
}
}
[DllImport(LibraryName, SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool GetCursorPos(out Point lpPoint);
[StructLayout(LayoutKind.Sequential)]
public struct Point
{
public int X;
public int Y;
public Point(int x, int y)
{
this.X = x;
this.Y = y;
}
public static implicit operator System.Drawing.Point(Point p)
{
return new System.Drawing.Point(p.X, p.Y);
}
public static implicit operator Point(System.Drawing.Point p)
{
return new Point(p.X, p.Y);
}
}
}
}
若是发现你的代码依然没法构建经过,还请参阅个人测试代码从里面抄代码解决找不到某个类
本文代码放在github欢迎小伙伴访问
关于 Win32 方法的定义,我推荐使用官方的 dotnet/pinvoke: A library containing all P/Invoke code so you don't have to import it every time. Maintained and updated to support the latest Windows OS. 库
本做品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。欢迎转载、使用、从新发布,但务必保留文章署名林德熙(包含连接:不得用于商业目的,基于本文修改后的做品务必以相同的许可发布。若有任何疑问,请与我联系。