winforms 不使用鼠标拖动手势移动窗口

tuwxkamq  于 2022-12-19  发布在  其他
关注(0)|答案(2)|浏览(142)

我有一扇窗(System.Windows.Forms.Form的子类)没有边框,以便应用自定义样式。当鼠标按钮按下时移动窗口似乎很容易。通过发送WM_NCLBUTTONDOWNHT_CAPTIONWM_SYSCOMMAND0xF102消息,可以将窗口“拖动”到新位置。但只要鼠标按钮按下,这扇Windows好像是不可能移动的。
一个人可以发送WM_SYSCOMMANDSC_MOVE消息,但随后光标在窗口的顶部中心移动,并等待用户按下任何箭头键,以便钩住窗口移动-这至少是尴尬的。我试图伪造一个键按下/释放序列,但当然没有。当我用当前的表单Handle作为参数调用SendMessage时,它不起作用,但我猜消息不应该发送到当前的表单。
所需行为为:* 点击 * 一个按钮(即鼠标按钮被释放)移动光标所在的窗体,再次点击释放窗体。这在winapi中是可能的吗?不幸的是我不熟悉它。

附录

**发送按键输入:**我尝试使用SendInput,因为SendMessage应该是bad practice。但它仍然没有钩住窗口。我尝试用Marshal.GetLastWin32Error()读取和打印winapi错误代码,但我得到了一个5,这是拒绝访问。奇怪的是,我收到消息后 * 移动序列结束(即我手动按下一个键或鼠标按钮)。不知道如何解决这个问题。
使用IMessageFilter(IVSoftware的答案):这是我最终做的,但有两个问题:用Location属性移动窗口与原生方式相比有延迟(现在没什么大不了的),而且它不会接收主窗体外的鼠标消息。这意味着它不会工作a.对于多屏幕环境b.如果光标移动到应用程序窗体外。我可以为每个监视器创建全屏透明窗体,作为“消息画布”不过,为什么不给予操作系统一个机会呢。

lymgl2op

lymgl2op1#

据我所知,所需的行为是启用“单击移动”(一种或另一种方式),然后单击多屏幕表面上的任何地方,让无边框表单跟随鼠标移动到新位置。在我的简短测试中,一个似乎有效的解决方案是pinvoke WinApi SetWindowsHookEx,为WH_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.
}

在我用来测试这个答案的代码中,将点击发生的按钮居中似乎是很直观的(如果这个偏移不适合你,很容易改变)。

威爱比

#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
2exbekwf

2exbekwf2#

这是一个我最终会采用的“可能”的解决方案。这并不是说IVSoftware's answer不起作用,它起作用了,我试过了。这是因为我的解决方案有一些与我正在尝试做的事情相关的优点。要点是:

  • 利用IMessageFilter(多亏了SwDevMan81's answer),它提醒我“全局”处理消息的正确方法是 * 不要 * 覆盖WndProc
  • 在所有屏幕上布置一组透明窗口,以便在任何地方接收鼠标移动消息。

优点

  • 它无需执行任何P/Invoke即可工作
  • 它允许使用更多的技巧,例如利用透明表单实现“移动边框而不是表单”功能(虽然我没有测试它,但画图可能很棘手)
  • 也可以很容易地应用于调整大小。
  • 可以使用鼠标左键/主键以外的其他键。

缺点

  • 它有太多的“活动部件”。至少对我的口味来说是这样。到处布置透明的Windows?嗯。
  • 它有一些极端的情况。在移动窗体的同时按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);
        }
    }
}

相关问题