Search code examples
c#.netwindowswinformsraw-input

Strange behaviour when toggling RIDEV_CAPTUREMOUSE | RIDEV_NOLEGACY


I'm writing a mouse object in C# that uses raw input. The device registers and gets data and all that stuff, so it's working in that regard. However, on this object I have a property called "Exclusive" which is meant to mimic the exclusive mode in Direct Input.

When I toggle this property to TRUE, I call RegisterRawInputDevices with the dwFlags member of the RAWINPUTDEVICE set to: RIDEV_CAPTUREMOUSE | RIDEV_NOLEGACY. And when I set the property to FALSE, I set it to 0.

Now the problem is when I do this from a mouse button down/up event. On my mouse object I assign the mouse button down event to set Exclusive to TRUE and on mouse up I set it to FALSE. When I run the application the events fire and the Exclusive mode is set and reset. This is where the weird stuff begins to happen:

  1. After the mouse up event and the exclusive mode is disabled, the window doesn't respond to mouse over events in the window decorations (e.g. the close button won't highlight and I can't click on it). I also can't exit the application by hitting ALT+F4. However, when I click on the window once or twice the regular window behaviour comes back.

  2. After the application is closed, windows explorer and other application windows react with the same behaviour. I have to left and right click multiple times to get them to go back to normal.

  3. Very rarely, the window will lose focus for some weird reason. And that throws everything into chaos with the exclusive state (the code is set up to unbind the device when the window is deactivated and restore it when it is activated again). As I said before, this is a very rare occurance, but is still very problematic.

When I set/reset the Exclusive mode using a key down and key up event, everything works perfectly and none of the above happens. And this is quite baffling.

I have tried this code on 2 computers, with different mice and one was running Windows 7 x64 and the other was running Windows 8.1 x64.

I have done a great deal of searching on this in the last few days and I have come up empty, so I'm wondering if anyone might have any thoughts on why it's behaving in this manner? Am I not setting the correct flags? Would calling RegisterRawInputDevices over and over like that cause issues?

Here's the code for the sample program that I'm using to test the issue:

_mouse = _input.CreatePointingDevice(_form);
_keyboard = _input.CreateKeyboard(_form);

_mouse.PointingDeviceDown += (sender, args) =>
                             {
                                 if ((args.Buttons & PointingDeviceButtons.Right) != PointingDeviceButtons.Right)
                                 {
                                     return;
                                 }

                                 _mouse.Exclusive = true;
                             };

_mouse.PointingDeviceMove += (sender, args) =>
                             {
                                 _form.Text = string.Format("{0}x{1}", args.Position.X, args.Position.Y);
                             };

_mouse.PointingDeviceUp += (sender, args) =>
                           {
                               if ((args.Buttons & PointingDeviceButtons.Right) != PointingDeviceButtons.Right)
                               {
                                   return;
                               }

                               _mouse.CursorVisible = true;
                               _mouse.Exclusive = false;
                           };

Here is the code I'm using to register and unregister the mouse:

/// <summary>
/// Function to bind the input device.
/// </summary>
protected override void BindDevice()
{
    BoundControl.MouseLeave -= Owner_MouseLeave;

    UnbindDevice();

    if (_messageFilter != null)
    {
        _messageFilter.RawInputPointingDeviceData -= GetRawData;
        _messageFilter.RawInputPointingDeviceData += GetRawData;
    }

    _device.UsagePage = HIDUsagePage.Generic;
    _device.Usage = (ushort)HIDUsage.Mouse;
    _device.Flags = RawInputDeviceFlags.None;

    // Enable background access.
    if (AllowBackground)
    {
        _device.Flags |= RawInputDeviceFlags.InputSink;
    }

    // Enable exclusive access.
    if (Exclusive)
    {
        _device.Flags |= RawInputDeviceFlags.CaptureMouse | RawInputDeviceFlags.NoLegacy;
    }

    _device.WindowHandle = BoundControl.Handle;

    // Attempt to register the device.
    if (!Win32API.RegisterRawInputDevices(_device))
    {
        throw new GorgonException(GorgonResult.DriverError, Resources.GORINP_RAW_CANNOT_BIND_POINTING_DEVICE);
    }

    if (!Exclusive)
    {
        OnWindowBound(BoundControl);
    }
}

    /// <summary>
    /// Function to unbind the input device.
    /// </summary>
    protected override void UnbindDevice()
    {
        if (_messageFilter != null)
        {
            _messageFilter.RawInputPointingDeviceData -= GetRawData;
        }

        _device.UsagePage = HIDUsagePage.Generic;
        _device.Usage = (ushort)HIDUsage.Mouse;
        _device.Flags = RawInputDeviceFlags.Remove;
        _device.WindowHandle = IntPtr.Zero;

        // Attempt to register the device.
        if (!Win32API.RegisterRawInputDevices(_device))
        {
            throw new GorgonException(GorgonResult.DriverError, Resources.GORINP_RAW_CANNOT_UNBIND_POINTING_DEVICE);
        }

        BoundControl.MouseLeave -= Owner_MouseLeave;
    }

Here is the code that processes the WM_INPUT message:

/// <summary>
/// Object representing a message loop filter.
/// </summary>
internal class MessageFilter
    : System.Windows.Forms.IMessageFilter
{
    #region Events.
    /// <summary>
    /// Event fired when a raw input keyboard event occours.
    /// </summary>
    public event EventHandler<RawInputKeyboardEventArgs> RawInputKeyboardData = null;
    /// <summary>
    /// Event fired when a pointing device event occurs.
    /// </summary>
    public event EventHandler<RawInputPointingDeviceEventArgs> RawInputPointingDeviceData = null;
    /// <summary>
    /// Event fired when an HID event occurs.
    /// </summary>
    public event EventHandler<RawInputHIDEventArgs> RawInputHIDData = null;
    #endregion

    #region Variables.
    private readonly int _headerSize = DirectAccess.SizeOf<RAWINPUTHEADER>();   // Size of the input data in bytes.
    #endregion

    #region IMessageFilter Members
    /// <summary>
    /// Filters out a message before it is dispatched.
    /// </summary>
    /// <param name="m">The message to be dispatched. You cannot modify this message.</param>
    /// <returns>
    /// true to filter the message and stop it from being dispatched; false to allow the message to continue to the next filter or control.
    /// </returns>
    public bool PreFilterMessage(ref System.Windows.Forms.Message m)
    {
        // Handle raw input messages.
        if ((WindowMessages)m.Msg != WindowMessages.RawInput)
        {
            return false;
        }

        unsafe
        {
            int dataSize = 0;

            // Get data size.           
            int result = Win32API.GetRawInputData(m.LParam, RawInputCommand.Input, IntPtr.Zero, ref dataSize, _headerSize);

            if (result == -1)
            {
                throw new GorgonException(GorgonResult.CannotRead, Resources.GORINP_RAW_CANNOT_READ_DATA);
            }

            // Get actual data.
            var rawInputPtr = stackalloc byte[dataSize];
            result = Win32API.GetRawInputData(m.LParam, RawInputCommand.Input, (IntPtr)rawInputPtr, ref dataSize, _headerSize);

            if ((result == -1) || (result != dataSize))
            {
                throw new GorgonException(GorgonResult.CannotRead, Resources.GORINP_RAW_CANNOT_READ_DATA);
            }

            var rawInput = (RAWINPUT*)rawInputPtr;

            switch (rawInput->Header.Type)
            {
                case RawInputType.Mouse:
                    if (RawInputPointingDeviceData != null)
                    {
                        RawInputPointingDeviceData(this,
                                                   new RawInputPointingDeviceEventArgs(rawInput->Header.Device, ref rawInput->Union.Mouse));
                    }
                    break;
                case RawInputType.Keyboard:
                    if (RawInputKeyboardData != null)
                    {
                        RawInputKeyboardData(this, new RawInputKeyboardEventArgs(rawInput->Header.Device, ref rawInput->Union.Keyboard));
                    }
                    break;
                default:
                    if (RawInputHIDData != null)
                    {
                        var HIDData = new byte[rawInput->Union.HID.Size * rawInput->Union.HID.Count];
                        var hidDataPtr = ((byte*)rawInput) + _headerSize + 8;

                        fixed (byte* buffer = &HIDData[0])
                        {
                            DirectAccess.MemoryCopy(buffer, hidDataPtr, HIDData.Length);
                        }

                        RawInputHIDData(this, new RawInputHIDEventArgs(rawInput->Header.Device, ref rawInput->Union.HID, HIDData));
                    }
                    break;
            }
        }

        return false;
    }
    #endregion
}

Here's the code that fires the mouse events after processing WM_INPUT:

/// <summary>
/// Function to retrieve and parse the raw pointing device data.
/// </summary>
/// <param name="sender">Sender of the event.</param>
/// <param name="e">Event data to examine.</param>
private void GetRawData(object sender, RawInputPointingDeviceEventArgs e)
{
    if ((BoundControl == null) || (BoundControl.Disposing))
    {
        return;
    }

    if ((_deviceHandle != IntPtr.Zero) && (_deviceHandle != e.Handle))
    {
        return;
    }

    if ((Exclusive) && (!Acquired))
    {
        // Attempt to recapture.
        if (BoundControl.Focused)
        {
            Acquired = true;
        }
        else
        {
            return;
        }
    }

    // Do nothing if we're outside and we have exclusive mode turned off.
    if (!Exclusive)
    {
        if (!WindowRectangle.Contains(BoundControl.PointToClient(System.Windows.Forms.Cursor.Position))) 
        {
            _outside = true;
            return;
        }

        if (_outside) 
        {
            // If we're back inside place position at the entry point.
            _outside = false;
            Position = BoundControl.PointToClient(System.Windows.Forms.Cursor.Position);
        }
    }

    // Get wheel data.
    if ((e.PointingDeviceData.ButtonFlags & RawMouseButtons.MouseWheel) != 0)
    {
        OnPointingDeviceWheelMove((short)e.PointingDeviceData.ButtonData);
    }

    // If we're outside of the delay, then restart double click cycle.
    if (_doubleClicker.Milliseconds > DoubleClickDelay)
    {
        _doubleClicker.Reset();
        _clickCount = 0;
    }

    // Get button data.
    if ((e.PointingDeviceData.ButtonFlags & RawMouseButtons.LeftDown) != 0)
    {
        BeginDoubleClick(PointingDeviceButtons.Left);
        OnPointingDeviceDown(PointingDeviceButtons.Left);
    }

    if ((e.PointingDeviceData.ButtonFlags & RawMouseButtons.RightDown) != 0)
    {
        BeginDoubleClick(PointingDeviceButtons.Right);
        OnPointingDeviceDown(PointingDeviceButtons.Right);
    }

    if ((e.PointingDeviceData.ButtonFlags & RawMouseButtons.MiddleDown) != 0)
    {
        BeginDoubleClick(PointingDeviceButtons.Middle);
        OnPointingDeviceDown(PointingDeviceButtons.Middle);
    }

    if ((e.PointingDeviceData.ButtonFlags & RawMouseButtons.Button4Down) != 0)
    {
        BeginDoubleClick(PointingDeviceButtons.Button4);
        OnPointingDeviceDown(PointingDeviceButtons.Button4);
    }

    if ((e.PointingDeviceData.ButtonFlags & RawMouseButtons.Button5Down) != 0)
    {
        BeginDoubleClick(PointingDeviceButtons.Button5);
        OnPointingDeviceDown(PointingDeviceButtons.Button5);
    }

    // If we have an 'up' event on the buttons, remove the flag.
    if ((e.PointingDeviceData.ButtonFlags & RawMouseButtons.LeftUp) != 0)
    {
        if (IsDoubleClick(PointingDeviceButtons.Left))
        {
            _clickCount += 1;
        }
        else
        {
            _doubleClicker.Reset();
            _clickCount = 0;
        }

        OnPointingDeviceUp(PointingDeviceButtons.Left, _clickCount);
    }

    if ((e.PointingDeviceData.ButtonFlags & RawMouseButtons.RightUp) != 0)
    {
        if (IsDoubleClick(PointingDeviceButtons.Right))
        {
            _clickCount += 1;
        }
        else
        {
            _doubleClicker.Reset();
            _clickCount = 0;
        }

        OnPointingDeviceUp(PointingDeviceButtons.Right, _clickCount);
    }

    if ((e.PointingDeviceData.ButtonFlags & RawMouseButtons.MiddleUp) != 0)
    {
        if (IsDoubleClick(PointingDeviceButtons.Middle))
        {
            _clickCount += 1;
        }
        else
        {
            _doubleClicker.Reset();
            _clickCount = 0;
        }

        OnPointingDeviceUp(PointingDeviceButtons.Middle, _clickCount);
    }

    if ((e.PointingDeviceData.ButtonFlags & RawMouseButtons.Button4Up) != 0)
    {
        if (IsDoubleClick(PointingDeviceButtons.Button4))
        {
            _clickCount += 1;
        }
        else
        {
            _doubleClicker.Reset();
            _clickCount = 0;
        }

        OnPointingDeviceUp(PointingDeviceButtons.Button4, _clickCount);
    }

    if ((e.PointingDeviceData.ButtonFlags & RawMouseButtons.Button5Up) != 0)
    {
        if (IsDoubleClick(PointingDeviceButtons.Button5))
        {
            _clickCount += 1;
        }
        else
        {
            _doubleClicker.Reset();
            _clickCount = 0;
        }

        OnPointingDeviceUp(PointingDeviceButtons.Button5, _clickCount);
    }

    // Fire events.
    RelativePosition = new PointF(e.PointingDeviceData.LastX, e.PointingDeviceData.LastY);
    OnPointingDeviceMove(new PointF(Position.X + e.PointingDeviceData.LastX, Position.Y + e.PointingDeviceData.LastY), false);
    UpdateCursorPosition();
}

Solution

  • Well, after days of pulling what little hair I have left out, I could find no rhyme or reason as to why this was happening. So, I devised a rather ugly hack to fake an exclusive mode.

    First I removed the NOLEGACY and CAPTUREMOUSE flags from the device registration and then I just locked the cursor to the center of the window that was receiving the input via Cursor.Position. I then modified my window message filter to throw away window messages like WM_MOUSEMOVE and WM_KEYDOWN so they wouldn't be intercepted by the window (except the system command that handles ALT+F4) while a device was in exclusive mode.

    While this is not the most elegant solution, it is working just as I wanted. However, if anyone does find a better way to handle this situation while still using the NOLEGACY/CAPTUREMOUSE flags, I'll gladly mark that as the correct answer.