How can I convert Win32 mouse messages to WPF mouse events?
Asked Answered
C

3

12

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.

Cott answered 21/1, 2014 at 3:35 Comment(4)
I can't see where you are assigning the mouse coordinates (which are encoded in the LPARAM if memory serves me) to the eventArgs before you forward it on. And looking at the definition of MouseButtonEventArgs I can't see a way to inject the mouse coords into it.Graber
Does it work if you forward the message to the parent window and let WPF worry about translating it into a WPF event?Rex
@Rex - Unfortunately it doesn't.Cott
@Graber - You are correct, there is no way to inject the mouse coordinates. WPF determines them itself in internal classes.Cott
C
8

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.

Cott answered 22/1, 2014 at 7:1 Comment(0)
W
0

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.

Weekender answered 30/6, 2015 at 19:14 Comment(0)
E
-1

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.

Excurrent answered 21/1, 2014 at 5:20 Comment(2)
I was aware of that, however it requires every event handler in the application to opt in to this behaviour. That's what I'll do if I have to but it's not an ideal way forward. This OpenGL control is replacing an existing control so doing that would also require me to update a bunch of existing event handlers, which I'm reluctant to do.Cott
Sorry I couldn't be of more help, then. Hopefully someone will come along with a better answer.Excurrent

© 2022 - 2024 — McMap. All rights reserved.