Can the mouse wheel be used while dragging/dropping?
Asked Answered
J

4

10

In WinForms, after calling a DoDragDrop to start dragging an item, controls no longer scroll with the mouse-wheel, and the control's MouseWheel event is no longer called, until the user drops whatever he is dragging.

Is there a way to get the mouse wheel to work while dragging?

Jp answered 27/1, 2011 at 18:7 Comment(5)
hey I ain't positive about this as I just found about it today. But would it be possible to do what you asking with Rx ?Resorcinol
Can you check and see whether MouseWheel events are being delivered to the drag source instead of the control the mouse is over?Titanic
So, you wanna drop an object in a form with scrolls, or an larger object that will not fit in a form. Am i correct ?Plenary
@Chuck: My specific use-case is dragging rows from one datagridview to another. But, I can think of plenty of other use-cases where this would be useful.Jp
@Ben Voigt: No, neither the source- nor destination-controls, nor the hosting form, receive the MouseWheel event. It is just lost in the abyss..Jp
A
6

You could get a global MouseWheel with a keyboard hook.

using System;
using System.ComponentModel;
using System.Runtime.InteropServices;
using System.Windows.Forms;
using Microsoft.Win32.SafeHandles;

using BOOL = System.Boolean;
using DWORD = System.UInt32;
using HHOOK = SafeHookHandle;
using HINSTANCE = System.IntPtr;
using HOOKPROC = HookProc;
using LPARAM = System.IntPtr;
using LRESULT = System.IntPtr;
using POINT = System.Drawing.Point;
using ULONG_PTR = System.IntPtr;
using WPARAM = System.IntPtr;

public delegate LRESULT HookProc(int nCode, WPARAM wParam, LPARAM lParam);

internal static class NativeMethods
{
    [DllImport("User32.dll", SetLastError = true)]
    internal static extern HHOOK SetWindowsHookEx(
        HookType idHook,
        HOOKPROC lpfn,
        HINSTANCE hMod,
        DWORD dwThreadId);

    [DllImport("User32.dll")]
    internal static extern LRESULT CallNextHookEx(
        HHOOK hhk,
        int nCode,
        WPARAM wParam,
        LPARAM lParam);

    [DllImport("User32.dll", SetLastError = true)]
    [return: MarshalAs(UnmanagedType.Bool)]
    internal static extern BOOL UnhookWindowsHookEx(
        IntPtr hhk);

    [DllImport("kernel32.dll", CharSet = CharSet.Auto)]
    public static extern IntPtr GetModuleHandle(string lpModuleName);
}

internal static class NativeTypes
{
    internal enum MSLLHOOKSTRUCTFlags : uint
    {
        LLMHF_INJECTED = 0x00000001U,
    }

    [StructLayout(LayoutKind.Sequential)]
    internal struct MSLLHOOKSTRUCT
    {
        internal POINT pt;
        internal DWORD mouseData;
        internal MSLLHOOKSTRUCTFlags flags;
        internal DWORD time;
        internal ULONG_PTR dwExtraInfo;
    }
}

internal static class NativeConstants
{
    internal const int WH_MOUSE_LL = 14;

    internal const int HC_ACTION = 0;

    internal const int WM_MOUSEWHEEL = 0x020A;
    internal const int WM_MOUSEHWHEEL = 0x020E;

    internal const int WHEEL_DELTA = 120;
}

public enum HookType
{
    LowLevelMouseHook = NativeConstants.WH_MOUSE_LL
}

public enum HookScope
{
    LowLevelGlobal,
}

public class SafeHookHandle : SafeHandleZeroOrMinusOneIsInvalid
{
    private SafeHookHandle() : base(true) { }

    public static SafeHookHandle SetWindowsHook(HookType idHook, HookProc lpfn, IntPtr hMod, uint dwThreadId)
    {
        var hhk = NativeMethods.SetWindowsHookEx(idHook, lpfn, hMod, dwThreadId);

        if(hhk.IsInvalid)
        {
            throw new Win32Exception();
        }
        else
        {
            return hhk;
        }
    }

    public IntPtr CallNextHook(int nCode, IntPtr wParam, IntPtr lParam)
    {
        return NativeMethods.CallNextHookEx(this, nCode, wParam, lParam);
    }

    protected override bool ReleaseHandle()
    {
        return NativeMethods.UnhookWindowsHookEx(this.handle);
    }
}

public abstract class WindowsHook : IDisposable
{
    private SafeHookHandle hhk;
    private HookProc lpfn;

    protected WindowsHook(HookType idHook, HookScope scope)
    {
        this.lpfn = this.OnWindowsHook;

        switch(scope)
        {
            case HookScope.LowLevelGlobal:
                IntPtr moduleHandle = NativeMethods.GetModuleHandle(null);
                this.hhk = SafeHookHandle.SetWindowsHook(idHook, this.lpfn, moduleHandle, 0U);
                return;
            default:
                throw new InvalidEnumArgumentException("scope", (int)scope, typeof(HookScope));
        }
    }

    protected virtual IntPtr OnWindowsHook(int nCode, IntPtr wParam, IntPtr lParam)
    {
        return this.hhk.CallNextHook(nCode, wParam, lParam);
    }

    public void Dispose()
    {
        this.Dispose(true);
        GC.SuppressFinalize(this);
    }

    protected virtual void Dispose(bool disposing)
    {
        if(disposing)
        {
            if(this.hhk != null) { this.hhk.Dispose(); }
        }
    }
}

public class LowLevelMouseHook : WindowsHook
{
    public event MouseEventHandler MouseWheel;

    public LowLevelMouseHook() : base(HookType.LowLevelMouseHook, HookScope.LowLevelGlobal) { }

    protected sealed override IntPtr OnWindowsHook(int nCode, IntPtr wParam, IntPtr lParam)
    {
        if(nCode == NativeConstants.HC_ACTION)
        {
            var msLLHookStruct = (NativeTypes.MSLLHOOKSTRUCT)Marshal.PtrToStructure(lParam, typeof(NativeTypes.MSLLHOOKSTRUCT));

            switch(wParam.ToInt32())
            {
                case NativeConstants.WM_MOUSEWHEEL:
                case NativeConstants.WM_MOUSEHWHEEL:
                    this.OnMouseWheel(new MouseEventArgs(Control.MouseButtons, 0, msLLHookStruct.pt.X, msLLHookStruct.pt.Y, (int)msLLHookStruct.mouseData >> 16));
                    break;
            }
        }

        return base.OnWindowsHook(nCode, wParam, lParam);
    }

    protected virtual void OnMouseWheel(MouseEventArgs e)
    {
        if(this.MouseWheel != null)
        {
            this.MouseWheel(this, e);
        }
    }
} 

Sample Usage:

using (LowLevelMouseHook hook = new LowLevelMouseHook())
{
    hook.MouseWheel += (sender, e) =>
    {
        Console.WriteLine(e.Delta);
    };
    Application.Run();
}

The code provides a class LowLevelMouseHook with an event MouseWheel that behaves like the event from the builtin windows forms control classes.

(Moreover the code is split into an abstract class WindowsHooks for use with other hooks, a SafeHookHandle class to ensure the handle is released and the helper classes for native methods)

You should look at SetWindowsHookEx and CALLBACK LowLevelMouseProc to understand the technique behind this.


This event is not limited to your application but will also capture the mouse outside your form, so it should also work for your operations where you can not use the local events.

Auditorium answered 16/3, 2011 at 17:4 Comment(3)
Hmm.. unfortuntely, this does not seem to work - I get a "Cannot set nonlocal hook without a module handle" when trying to start the program. It appears SafeHookHandle.SetWindowsHook.hMod cannot be IntPtr.Zero...Jp
Using the result of GetModuleHandle(null) rather than IntPtr.Zero seems to work - I'll edit that in. This is exactly what I was looking for.. Thanks!Jp
You're welcome. I had problems myself with hat IntPtr.Zero, but they disappeared suddenly (Win7 x64) and i left it.Auditorium
U
4

No, there is no identifiable focus during the D+D and the D+D events don't report back mouse wheel motion. A typical trick is using DragOver and checking if the dragging cursor is close to either end of a scrollable region. And scroll with a timer. An example is here.

Unsuccessful answered 27/1, 2011 at 19:53 Comment(7)
Yeah, I'm doing that, but I want the mouse-wheel working too. Damn, I was hoping for an easy solution. I don't suppose you know a way of doing this with global mouse-events (via P/Invoke)?Jp
IMessageFilter can do it. That's not an easy solution.Unsuccessful
No, cancel that, focus is murky. Very maybe.Unsuccessful
I don't believe there's no way to do this; if MS paint can scroll with the wheel while you're dragging a bandbox selection or some other shape, you should be able to do the same in your own program, whether it requires low-level mouse observation or something simpler.Kane
MSPaint doesn't use D+D to do that. There isn't any reason.Unsuccessful
What if we assume my application has focus (and I will only be dragging from one control within the application to another)?Jp
@Hans: Focus isn't important, mouse capture is. Typically the mouse is captured by the drag source, and you want to scroll the drop target.Titanic
I
2

Instead of using the built-in D+D functionality and trying to override its behavior with PInvoke and other events you could instead create your own drag and drop system based on mouse down and up events in a away that will retain the form's mouse-wheel scrolling capabilities.

Here is a very simple example from a test form that contains a label, which is the mock drag source ("activates" drag on mouse down), and a list box populated with arbitrary items, which is the mouse wheel scrollable drop destination. If you run a sample like this you'll notice that changing the cursor in the mouse down event on the label, dragging it over the list box and then scrolling with the mouse wheel will behave as expected. The list box will scroll.

using System;
using System.Windows.Forms;

public partial class Form1 : Form {
    public Form1() {
        InitializeComponent();
    }

    private void Form1_Load(object sender, EventArgs e) {
        for (int i=0; i<250; i++) listBox1.Items.Add("item " + i);
    }

    private void Label1MouseDown(object sender, MouseEventArgs e) {
        Cursor.Current = Cursors.SizeAll;
    }
}

Of course you have to wire up your own logic for dropping items (such as a mouse up handler to define the drop process) and you probably don't want to use the SizeAll cursor but something that is more indicative of dragging and dropping. This sample is just to show that managing your own D+D may be simpler than trying to override an API blackbox.

Inglenook answered 14/3, 2011 at 20:53 Comment(3)
@Ben: My suggestion doesn't actually capture the mouse, it just changes the cursor on a mouse down event. The snippet you see is literally all it took to test this, outside of all the auto-generated and designer file junk of course. All the OP needs to do to complete this is add a flag, a mouse-up handler on the appropriate control and the logic of what the drop is supposed to do.Inglenook
Exactly. The difference between your code and the framework code is that the framework DOES capture the mouse.Titanic
@Ben: sorry, I misinterpreted your initial comment as a criticism of my answer. I thought that you thought that I suggested a capture...Inglenook
P
1

How about this:

In the objective DataGrid (the one where you suppose to drop), when the mouse pointer reaches the end or the begining, you start scrolling down or up (of course it will be controlling the mousein/mouseout events).

Try dragin an object in excel, if you reach the end/begining of what you can see it will start scrolling down/up.

I don't know if i explain myself, let me know and i'll try to make it more explicit

Plenary answered 16/3, 2011 at 16:18 Comment(1)
Yes, I am doing that already - but that is a workaround, not a fix, for this limitation.Jp

© 2022 - 2024 — McMap. All rights reserved.