Home > OS >  Move window without a mouse drag gesture
Move window without a mouse drag gesture

Time:12-19

I have a window (subclass of System.Windows.Forms.Form) without a border in order to apply a custom style to it. Moving the window when the mouse button is down seems to be easy. Either by sending a WM_NCLBUTTONDOWN HT_CAPTION or a WM_SYSCOMMAND 0xF102 message the window can be dragged to a new location. As soon as the mouse button is up though, it seems to be impossible to move the window.

One could send WM_SYSCOMMAND SC_MOVE message but then the cursor moves at the top center of the window and awaits for the user to press any arrow key in order to hook the window for move -which is awkward at the least. I tried to fake a key press/release sequence but that of course didn't work as I called SendMessage with the current form Handle as argument but I guess the message should not be sent to the current form.

The desired behavior is: click a button (ie the mouse button is released) move the form where cursor goes, click again to release the form. Is that possible with winapi? Unfortunately I am not familiar with it.

Addendum

Sending a key input: I tried to use SendInput as SendMessage is supposed to be borderless form

multiscreen

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

CodePudding user response:

Here is a possible solution that I will go with after all. It's not that IVSoftware's answer doesn't work, it does, I tried it. It's that my solution has some set of advantages relevant to what I am trying to do. The main points are:

  • Utilizing the IMessageFilter (thanks to SwDevMan81's answer) which reminded me that the correct way to process messages "globally" is not to override WndProc)
  • Laying out a set of transparent windows on all screens in order to receive mouse move messages everywhere.

Pros

  • It works without having to make any P/Invokes
  • It allows more tricks to be done like for example leverage the transparent forms to implement a "move border instead of form" functionality (though I didn't test it, paint might be tricky)
  • Can be easily applied for resize as well.
  • Can work with mouse buttons other than the left/primary.

Cons

  • It has too many "moving parts". At least for my taste. Laying out transparent windows all over the place? Hm.
  • It has some corner cases. Pressing Alt F4 while moving the form will close the "canvas form". That can be easily mitigated but there might be others as well.
  • There must be an OS way to do this...

The code (basic parts; full code on 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);
        }
    }
}
  • Related