WPF 多指觸摸拖拽窗口 拖動修改窗口座標

在 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 國際許可協議進行許可。歡迎轉載、使用、從新發布,但務必保留文章署名林德熙(包含連接:不得用於商業目的,基於本文修改後的做品務必以相同的許可發佈。若有任何疑問,請與我聯繫

相關文章
相關標籤/搜索