簡體   English   中英

無需鼠標拖動手勢即可移動窗口

[英]Move window without a mouse drag gesture

我有一個沒有邊框的窗口( System.Windows.Forms.Form的子類),以便對其應用自定義樣式。 按下鼠標按鈕時移動窗口似乎很容易。 通過發送WM_NCLBUTTONDOWN HT_CAPTIONWM_SYSCOMMAND 0xF102消息,可以將窗口拖動到新位置。 但是,一旦鼠標按鈕抬起,似乎就無法移動窗口。

可以發送WM_SYSCOMMAND SC_MOVE消息,但隨后光標移動到窗口的頂部中心並等待用戶按任意箭頭鍵以掛鈎窗口以進行移動 - 這至少很尷尬。 我試圖偽造一個按鍵/釋放序列,但這當然沒有用,因為我用當前表單Handle作為參數調用SendMessage ,但我想消息不應該發送到當前表單。

期望的行為是:單擊一個按鈕(即釋放鼠標按鈕)將窗體移動到光標所在的位置,再次單擊以釋放窗體。 winapi可以嗎? 不幸的是我不熟悉它。

附錄

發送鍵輸入:我嘗試使用SendInput ,因為SendMessage應該是不好的做法 仍然沒有鈎住窗戶。 我嘗試使用Marshal.GetLastWin32Error()讀取並打印 winapi 錯誤代碼,但我得到了 5,即訪問被拒絕。 奇怪的是我在移動序列結束收到了消息(即我手動按下了一個鍵或鼠標按鈕)。 不知道如何解決這個問題。

使用IMessageFilter (IVSoftware 的回答):這就是我最終做的,但有 2 個問題:與本地方式相比,使用其Location屬性移動窗口有滯后(現在沒什么大不了的)而且它不接收鼠標出現在主窗體之外的消息。 這意味着它不會工作。 對於多屏幕環境 b. 如果光標移到應用程序的窗體之外。 我可以為每個監視器創建全屏透明表單,作為“消息畫布”,但仍然......為什么不給操作系統一個機會。

據我了解,所需的行為是啟用“單擊以移動”(一種或另一種方式),然后單擊多屏幕表面上的任意位置,並讓無邊框形式跟隨鼠標移動到新位置。 在我的簡短測試中似乎有效的一種解決方案是調用 WinApi SetWindowsHookExWH_MOUSE_LL安裝全局低級掛鈎,以便攔截WM_LBUTTONDOWN

*這個答案已經過修改,以便跟蹤問題的更新。


低級全局鼠標鈎子

    public MainForm()
    {
        InitializeComponent();
        using (var process = Process.GetCurrentProcess())
        {
            using (var module = process.MainModule!)
            {
                var mname = module.ModuleName!;
                var handle = GetModuleHandle(mname);
                _hook = SetWindowsHookEx(
                    HookType.WH_MOUSE_LL,
                    lpfn: callback,
                    GetModuleHandle(mname),
                    0);
            }
        }

        // Unhook when this `Form` disposes.
        Disposed += (sender, e) => UnhookWindowsHookEx(_hook);

        // A little hack to keep window on top while Click-to-Move is enabled.
        checkBoxEnableCTM.CheckedChanged += (sender, e) =>
        {
            TopMost = checkBoxEnableCTM.Checked;
        };

        // Compensate move offset with/without the title NC area.
        var offset = RectangleToScreen(ClientRectangle);
        CLIENT_RECT_OFFSET = offset.Y - Location.Y;
    }
    readonly int CLIENT_RECT_OFFSET;
    IntPtr _hook;
    private IntPtr callback(int code, IntPtr wParam, IntPtr lParam)
    {
        var next = IntPtr.Zero;
        if (code >= 0)
        {
            switch ((int)wParam)
            {
                case WM_LBUTTONDOWN:
                    if (checkBoxEnableCTM.Checked)
                    {
                        _ = onClickToMove(MousePosition);
                        // This is a very narrow condition and the window is topmost anyway.
                        // So probably swallow this mouse click and skip other hooks in the chain.
                        return (IntPtr)1;
                    }
                    break;
            }
        }
        return CallNextHookEx(IntPtr.Zero, code, wParam, lParam);
    }
}

執行移動

private async Task onClickToMove(Point mousePosition)
{
    // Exempt clicks that occur on the 'Enable Click to Move` button itself.
    if (!checkBoxEnableCTM.ClientRectangle.Contains(checkBoxEnableCTM.PointToClient(mousePosition)))
    {
        // Try this. Offset the new `mousePosition` so that the cursor lands
        // in the middle of the button when the move is over. This feels
        // like a semi-intuitive motion perhaps. This means we have to
        // subtract the relative position of the button from the new loc.
        var clientNew = PointToClient(mousePosition);

        var centerButton =
            new Point(
                checkBoxEnableCTM.Location.X + checkBoxEnableCTM.Width / 2,
                checkBoxEnableCTM.Location.Y + checkBoxEnableCTM.Height / 2);

        var offsetToNow = new Point(
            mousePosition.X - centerButton.X,
            mousePosition.Y - centerButton.Y - CLIENT_RECT_OFFSET);

        // Allow the pending mouse messages to pump. 
        await Task.Delay(TimeSpan.FromMilliseconds(1));
        WindowState = FormWindowState.Normal; // JIC window happens to be maximized.
        Location = offsetToNow;            
    }
    checkBoxEnableCTM.Checked = false; // Turn off after each move.
}

在我用來測試這個答案的代碼中,將發生點擊的按鈕居中似乎很直觀(如果不適合你,這個偏移量很容易改變)。 這是多屏測試的結果:

無邊界形式

多屏幕

WinAPI

#region P I N V O K E
public enum HookType : int { WH_MOUSE = 7, WH_MOUSE_LL = 14 }
const int WM_LBUTTONDOWN = 0x0201;

delegate IntPtr HookProc(int code, IntPtr wParam, IntPtr lParam);

[DllImport("user32.dll", SetLastError = true)]
static extern IntPtr SetWindowsHookEx(HookType hookType, HookProc lpfn, IntPtr hMod, int dwThreadId);

[DllImport("user32.dll")]
static extern IntPtr CallNextHookEx(IntPtr hhk, int nCode, IntPtr wParam,
    IntPtr lParam);

[DllImport("user32.dll", SetLastError = true)]
static extern bool UnhookWindowsHookEx(IntPtr hhk);

[DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
public static extern IntPtr GetModuleHandle(string lpModuleName);
#endregion P I N V O K E

畢竟,這是一個可能的解決方案。 不是IVSoftware的回答不行,是可以的,我試過了。 這是因為我的解決方案具有一些與我正在嘗試做的事情相關的優點。 要點是:

  • 利用IMessageFilter (感謝SwDevMan81 的回答)提醒我“全局”處理消息的正確方法不是覆蓋WndProc
  • 在所有屏幕上布置一組透明窗口,以便隨處接收鼠標移動消息。

優點

  • 它無需進行任何 P/Invoke 即可工作
  • 它允許完成更多技巧,例如利用透明表單來實現“移動邊框而不是表單”功能(雖然我沒有測試它,繪畫可能會很棘手)
  • 也可以輕松應用於調整大小。
  • 可以使用左鍵/主鍵以外的鼠標鍵。

缺點

  • 它有太多的“活動部件”。 至少對我來說是這樣。 到處鋪設透明窗戶? 嗯。
  • 它有一些極端情況。 在移動表單時按Alt + F4將關閉“畫布表單”。 這可以很容易地緩解,但也可能有其他問題。
  • 必須有一種操作系統方法來做到這一點......

代碼(基本部分; github上的完整代碼)

public enum WindowMessage
{
    WM_MOUSEMOVE = 0x200,
    WM_LBUTTONDOWN = 0x201,
    WM_LBUTTONUP = 0x202,
    WM_RBUTTONDOWN = 0x204,
    //etc. omitted for brevity
}

public class MouseMessageFilter : IMessageFilter
{
    public event EventHandler MouseMoved;
    public event EventHandler<MouseButtons> MouseDown;
    public event EventHandler<MouseButtons> MouseUp;

    public bool PreFilterMessage(ref Message m)
    {
        switch (m.Msg)
        {
            case (int)WindowMessage.WM_MOUSEMOVE:
                MouseMoved?.Invoke(this, EventArgs.Empty);
                break;
            case (int)WindowMessage.WM_LBUTTONDOWN:
                MouseDown?.Invoke(this, MouseButtons.Left);
                break;
            //etc. omitted for brevity
        }

        return false;
    }
}

public partial class CustomForm : Form
{
    private MouseMessageFilter windowMoveHandler = new();
    private Point originalLocation;
    private Point offset;

    private static List<Form> canvases = new(SystemInformation.MonitorCount);

    public CustomForm()
    {
        InitializeComponent();
        
        windowMoveHandler.MouseMoved += (_, _) =>
        {
            Point position = Cursor.Position;
            position.Offset(offset);
            Location = position;
        };
        windowMoveHandler.MouseDown += (_, button) =>
        {
            switch (button)
            {
                case MouseButtons.Left:
                    EndMove();
                    break;
                case MouseButtons.Middle:
                    CancelMove();
                    break;
            }
        };
        moveButton.MouseClick += (_, _) =>
        {
            BeginMove();
        };
    }

    private void BeginMove()
    {
        Application.AddMessageFilter(windowMoveHandler);
        originalLocation = Location;
        offset = Invert(PointToClient(Cursor.Position));
        ShowCanvases();
    }
    
    //Normally an extension method in another library of mine but I didn't want to
    //add a dependency just for that
    private static Point Invert(Point p) => new Point(-p.X, -p.Y);

    private void ShowCanvases()
    {
        for (int i = 0; i < Screen.AllScreens.Length; i++)
        {
            Screen screen = Screen.AllScreens[i];
            Form form = new TransparentForm
            {
                Bounds = screen.Bounds,
                Owner = Owner
            };
            canvases.Add(form);
            form.Show();
        }
    }

    private void EndMove()
    {
        DisposeCanvases();
    }

    private void DisposeCanvases()
    {
        Application.RemoveMessageFilter(windowMoveHandler);
        for (var i = 0; i < canvases.Count; i++)
        {
            canvases[i].Close();
        }
        canvases.Clear();
    }

    private void CancelMove()
    {
        EndMove();
        Location = originalLocation;
    }

    //The form used as a "message canvas" for moving the form outside the client area.
    //It practically helps extend the client area. Without it we won't be able to get
    //the events from everywhere
    private class TransparentForm : Form
    {
        public TransparentForm()
        {
            StartPosition = FormStartPosition.Manual;
            FormBorderStyle = FormBorderStyle.None;
            ShowInTaskbar = false;
        }

        protected override void OnPaintBackground(PaintEventArgs e)
        {
            //Draws a white border mostly useful for debugging. For example that's
            //how I realised I needed Screen.Bounds instead of WorkingArea.
            ControlPaint.DrawBorder(e.Graphics, new Rectangle(Point.Empty, Size),
                Color.White, 2, ButtonBorderStyle.Solid,
                Color.White, 2, ButtonBorderStyle.Solid,
                Color.White, 2, ButtonBorderStyle.Solid,
                Color.White, 2, ButtonBorderStyle.Solid);
        }
    }
}

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM