.netwpfwinapihwndhost

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


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.


Solution

  • 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:

    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.