How can I convert Win32 mouse messages to WPF mouse events?

2.3k views Asked by At

I have a Win32 (OpenGL) control that I need to embed in our WPF application. It must respond to, and propogate, mouse and keyboard events.

I've created a HwndHost derived instance to host the native window and have overridden the WndProc function in this class. To propogate win32 messages to WPF I handle specific mouse messages and map them to WPF events, then use the static InputManager class to raise them.

The problem is, when I go to handle them the mouse coordinates are messed up.

Heres a sample of the code I'm using to raise the events:

IntPtr MyHwndHost::WndProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, bool% handled)
{
  switch (msg)
  {
    case WM_LBUTTONDOWN:
    {
        MouseButtonEventArgs^ eventArgs = gcnew MouseButtonEventArgs(
          Mouse::PrimaryDevice,
          Environment::TickCount,
          MouseButton::Left);

        eventArgs->RoutedEvent = UIElement::PreviewMouseDownEvent;
        eventArgs->Source = this;

        // Raise the WPF event from the specified WPF event source.
        InputManager::Current->ProcessInput(eventArgs);

        eventArgs->RoutedEvent = UIElement::MouseDownEvent;
        InputManager::Current->ProcessInput(eventArgs);

        CommandManager::InvalidateRequerySuggested();

        handled = true;
        return IntPtr::Zero;
    }
    break;
  }

  return HwndHost::WndProc(hwnd, msg, wParam, lParam, handled);
}

When my WPF event handlers are triggered and I try to retrieve the mouse coordinates (e.g. e.GetPosition((IInputElement) sender)) I recieve the wrong values (and they are always the same values regardless of where the original event occurred within my control).

The values I get seem to be related to the screen location of the WPF window that hosts the application because they change when the window position changes, but they don't correspond to the actual location of the application window either.

I think this may have something to do with the internal state of the WPF InputManager, and the fact that the private field MouseDevice._inputSource is null when my events are raised, but my experiments with .net reflection haven't yielded any results there.

I don't really know what else to try. Keyboard support worked out of the box, it's just the mouse location support that I can't get working correctly.

3

There are 3 answers

0
Josh On BEST ANSWER

After another day spent in Reflector analysing the .net sourcecode in question I have found the solution. One must first prime the WPF InputManager by raising a PreviewInputReportEventArgs routed event.

Unfortunately, this event and the event argument structures required to raise it are all marked internal, leaving reflection the only way to raise this event. For anyone with the same problem, here's the C# code required to do so:

    void RaiseMouseInputReportEvent(Visual eventSource, int timestamp, int pointX, int pointY, int wheel)
    {
        Assembly targetAssembly = Assembly.GetAssembly(typeof(InputEventArgs));
        Type mouseInputReportType = targetAssembly.GetType("System.Windows.Input.RawMouseInputReport");

        Object mouseInputReport = mouseInputReportType.GetConstructors()[0].Invoke(new Object[] {
            InputMode.Foreground,
            timestamp,
            PresentationSource.FromVisual(eventSource),
            RawMouseActions.AbsoluteMove | RawMouseActions.Activate,
            pointX,
            pointY,
            wheel,
            IntPtr.Zero });

        mouseInputReportType
            .GetField("_isSynchronize", BindingFlags.NonPublic | BindingFlags.Instance)
            .SetValue(mouseInputReport, true);

        InputEventArgs inputReportEventArgs = (InputEventArgs) targetAssembly
            .GetType("System.Windows.Input.InputReportEventArgs")
            .GetConstructors()[0]
            .Invoke(new Object[] {
                Mouse.PrimaryDevice,
                mouseInputReport });

        inputReportEventArgs.RoutedEvent = (RoutedEvent) typeof(InputManager)
            .GetField("PreviewInputReportEvent", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static)
            .GetValue(null);

        InputManager.Current.ProcessInput((InputEventArgs) inputReportEventArgs);
    }

where:

  • timestamp is the system tick count when the mouse event occurred (usually Environment.TickCount)
  • pointX and pointY are the mouse coordinates, relative to the top level application window's client area
  • wheel is the mouse wheel delta

Note that it is only necessary to raise the "Preview" event. After raising this event the standard mouse events can be raised and WPF will return the correct mouse location coordinates when e.GetPosition() is called.

2
Nathan M On

You can get the absolute position of the mouse (in screen coordinates) at any time by using a p/Invoke function:

[StructLayout(LayoutKind.Sequential)]
private struct point {
    public int x;
    public int y;
}

[DllImport("user32.dll", EntryPoint="GetCursorPos")]
private static extern void GetCursorPos(ref point pt);

From there you should be able to easily calculate relative coordinates.

0
ghord On

The way I do it is with different approach. I keep transparent WPF Window overlayed on my HwndHost using AllowsTransparency property. I hook up interesting events on overlay window and redirect them to my control using RaiseEvent like this:

//in HwndHost code
private void Window_MouseDown(object sender, MouseButtonEventArgs e)
{
    this.RaiseEvent(e);
}

The only thing you need to do is to synchronize HwndHost and overlay window positions, which is not that hard.