Keyboard input is not registered when Unity is embedded in a WPF application

Hello!

We embed the Unity process into a Windows Forms Panel, which is the child of a WindowsFormsHost, which again is the child of a WPF Grid Control.
The Unity process is started with the -parentHWND and the aforementioned Panel as arguments.

The old Input System worked fine.
But since we switched to the new Input System key press events are not fired for any key on the keyboard. Mouse input is still fine however.

Is this a bug? Is this intentional? Is there a way to make it work?
Help is greatly appreciated. Thank you!

(The same question has been asked in the general forum in April, but has never gotten any replies:
New input system package does not send key events when launching with the "-parentHWND" argument )

Hello,

I am experiencing exactly the same problem and am still wondering how to fix it.

I tried e.g. “WindowsFormsHost.EnableWindowsFormsInterop();” but that did not do the trick.

It’s been almost a year and the problem with the “new” input system and an embedded Unity process still persists.
Is there any update / tip / magic trick that could finally get us around this problem?

In case you haven’t already, please file a ticket with the Unity bug reporter. You can post the ticket number here and I will check on the status.

Was a bug ever filed for this? I’m also facing the same issue and would love to know the status of it.

Facing the same issue here as well. Any update on this issue @Rene-Damm ?

@ConanB @pohype please file a bug report for this, from top of my head I don’t remember anything in our bug tracker related to UWP embedding breaking keyboard input, it should work so if it doesn’t it’s most likely a bug, it would be awesome to have a repro case for our UWP folks.

This is not specifically related to UWP but to windows in general :slight_smile: I have created a bug report and the steps to reproduce it (ticket 1372661).

1 Like

I investigated a pretty much identical issue several months ago:

TLDR: when hosted in a WPF application, we’re not receiving WM_INPUT messages (and new input system is based on them). Manually forwarding these messages in the WPF app makes the input system work again.

@Tautvydas-Zilys thanks for your answer! In the case we have no control over the parent context (I embed Unity in an Electron window, Electron has little to no control on the win32 api), is there a way to forward those messages? I was thinking about making a side c++ process that forwards the WM_INPUT messages, but it seems a bit overkill for something as simple as inputs…

I don’t really know how Electron works, but this is how you can do it from C# code:

[StructLayout(LayoutKind.Sequential)]
public struct RAWINPUTDEVICE
{
   public ushort usUsagePage;
   public ushort usUsage;
   public uint dwFlags;
   public IntPtr hwndTarget;
}

const ushort HID_USAGE_PAGE_GENERIC = 0x01;
const ushort HID_USAGE_GENERIC_KEYBOARD = 0x06;

[DllImport("user32.dll", SetLastError = true)]
public static extern bool RegisterRawInputDevices([MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 0)] RAWINPUTDEVICE[] pRawInputDevices, int uiNumDevices, int cbSize);

[DllImport("user32.dll", SetLastError = true)]
private static extern IntPtr SetWindowLongPtrW(IntPtr hWnd, int nIndex, IntPtr dwNewLong);

private new delegate IntPtr WndProc(IntPtr hWnd, int msg, IntPtr wParam, IntPtr lParam);

private MulticastDelegate originalWndProc;
private WndProc myWndProc;

private IntPtr HookWndProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam)
{
   if (msg == WM_INPUT)
   {
       SendMessage(unityHWND, msg, wParam, lParam);
       return IntPtr.Zero;
   }

   return (IntPtr)originalWndProc.DynamicInvoke(new object[] { hwnd, msg, wParam, lParam });
}

private void SetupRawInput(IntPtr hostHWND)
{
    myWndProc = HookWndProc;

   var originalWndProcPtr = SetWindowLongPtrW(hostHWND, GWL_WNDPROC, Marshal.GetFunctionPointerForDelegate(myWndProc));
   if (originalWndProcPtr == null)
   {
       var errorCode = Marshal.GetLastWin32Error();
       throw new Win32Exception(errorCode, "Failed to overwrite the original wndproc");
   }

   originalWndProc = Marshal.GetDelegateForFunctionPointer<MulticastDelegate>(originalWndProcPtr);

   var rawInputDevices = new[]
   {
       new RAWINPUTDEVICE()
       {
           usUsagePage = HID_USAGE_PAGE_GENERIC,
           usUsage = HID_USAGE_GENERIC_KEYBOARD,
           dwFlags = 0,
           hwndTarget = hostHWND
       }
   };

   if (!RegisterRawInputDevices(rawInputDevices, 1, Marshal.SizeOf(typeof(RAWINPUTDEVICE))))
   {
       var lastError = Marshal.GetLastWin32Error();
       throw new Win32Exception(lastError, "Failed to register raw input devices");
   }
}

What you need to basically do is call RegisterRawInputDevices() on the parent window, and then forward WM_INPUT messages to the Unity child window.

@Tautvydas-Zilys thank you! I managed to make it work with the Window form example found in the documentation of -parentHWND.
Although, I’m afraid this still doesn’t solve the issue for non C# integrations, especially for Electron integrations in which you can hardly call RegisterRawInputDevices or any win32 api.
Do you know if there’s a workaround to achieve this without having to call win32 bindings from the parent window (maybe by doing it directly from the Unity child app, or something else ?).

Just for those coming after me, here’s the full code (Form1.cs) of the doc example to update for the new input system to work, you’ll also have to compile to x64 (i think SetWindowLongPtrW doesn’t work on 32 bit systems). It would be nice to update the doc zip file :).

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading;
using System.Windows.Forms;
using System.Diagnostics;
using System.Windows.Forms.VisualStyles;

namespace Container
{
    public partial class Form1 : Form
    {
        [DllImport("User32.dll")]
        static extern bool MoveWindow(IntPtr handle, int x, int y, int width, int height, bool redraw);

        internal delegate int WindowEnumProc(IntPtr hwnd, IntPtr lparam);
        [DllImport("user32.dll")]
        internal static extern bool EnumChildWindows(IntPtr hwnd, WindowEnumProc func, IntPtr lParam);

        [DllImport("user32.dll")]
        static extern int SendMessage(IntPtr hWnd, int msg, IntPtr wParam, IntPtr lParam);

        private Process process;
        private IntPtr unityHWND = IntPtr.Zero;

        private const int WM_ACTIVATE = 0x0006;
        private const int WM_INPUT = 0x00FF;
        private const int GWLP_WNDPROC = -4;
        private readonly IntPtr WA_ACTIVE = new IntPtr(1);
        private readonly IntPtr WA_INACTIVE = new IntPtr(0);

        [StructLayout(LayoutKind.Sequential)]
        public struct RAWINPUTDEVICE
        {
            public ushort usUsagePage;
            public ushort usUsage;
            public uint dwFlags;
            public IntPtr hwndTarget;
        }

        const ushort HID_USAGE_PAGE_GENERIC = 0x01;
        const ushort HID_USAGE_GENERIC_KEYBOARD = 0x06;

        [DllImport("user32.dll", SetLastError = true)]
        public static extern bool RegisterRawInputDevices([MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 0)] RAWINPUTDEVICE[] pRawInputDevices, int uiNumDevices, int cbSize);

        [DllImport("user32.dll", SetLastError = true)]
        private static extern IntPtr SetWindowLongPtrW(IntPtr hWnd, int nIndex, IntPtr dwNewLong);

        private new delegate IntPtr WndProc(IntPtr hWnd, int msg, IntPtr wParam, IntPtr lParam);

        private MulticastDelegate originalWndProc;
        private WndProc myWndProc;

        public Form1()
        {
            InitializeComponent();

            try
            {
                process = new Process();
                process.StartInfo.FileName = "Child.exe";
                process.StartInfo.Arguments = "-parentHWND " + panel1.Handle.ToInt32() + " " + Environment.CommandLine;
                process.StartInfo.UseShellExecute = true;
                process.StartInfo.CreateNoWindow = true;

                process.Start();

                process.WaitForInputIdle();
                EnumChildWindows(panel1.Handle, WindowEnum, IntPtr.Zero);
                SetupRawInput(panel1.Handle);
                unityHWNDLabel.Text = "Unity HWND: 0x" + unityHWND.ToString("X8");
            }
            catch (Exception ex)
            {
                MessageBox.Show(ex.Message + ".\nCheck if Container.exe is placed next to Child.exe.");
            }

        }

        private void ActivateUnityWindow()
        {
            SendMessage(unityHWND, WM_ACTIVATE, WA_ACTIVE, IntPtr.Zero);
        }

        private void DeactivateUnityWindow()
        {
            SendMessage(unityHWND, WM_ACTIVATE, WA_INACTIVE, IntPtr.Zero);
        }

        private int WindowEnum(IntPtr hwnd, IntPtr lparam)
        {
            unityHWND = hwnd;
            ActivateUnityWindow();
            return 0;
        }

        private void panel1_Resize(object sender, EventArgs e)
        {
            MoveWindow(unityHWND, 0, 0, panel1.Width, panel1.Height, true);
            ActivateUnityWindow();
        }

        // Close Unity application
        private void Form1_FormClosed(object sender, FormClosedEventArgs e)
        {
            try
            {
                process.CloseMainWindow();

                Thread.Sleep(1000);
                while (process.HasExited == false)
                    process.Kill();
            }
            catch (Exception)
            {
              
            }
        }

        private void Form1_Activated(object sender, EventArgs e)
        {
            ActivateUnityWindow();
        }

        private void Form1_Deactivate(object sender, EventArgs e)
        {
            DeactivateUnityWindow();
        }
        private IntPtr HookWndProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam)
        {
            if (msg == WM_INPUT)
            {
                SendMessage(unityHWND, msg, wParam, lParam);
                return IntPtr.Zero;
            }

            return (IntPtr)originalWndProc.DynamicInvoke(new object[] { hwnd, msg, wParam, lParam });
        }

        private void SetupRawInput(IntPtr hostHWND)
        {
            myWndProc = HookWndProc;

            var originalWndProcPtr = SetWindowLongPtrW(hostHWND, GWLP_WNDPROC , Marshal.GetFunctionPointerForDelegate(myWndProc));
            if (originalWndProcPtr == null)
            {
                var errorCode = Marshal.GetLastWin32Error();
                throw new Win32Exception(errorCode, "Failed to overwrite the original wndproc");
            }

            Type lel = typeof(MulticastDelegate);
            originalWndProc = (MulticastDelegate)Marshal.GetDelegateForFunctionPointer(originalWndProcPtr, lel);

            var rawInputDevices = new[]
            {
       new RAWINPUTDEVICE()
       {
           usUsagePage = HID_USAGE_PAGE_GENERIC,
           usUsage = HID_USAGE_GENERIC_KEYBOARD,
           dwFlags = 0,
           hwndTarget = hostHWND
       }
   };

            if (!RegisterRawInputDevices(rawInputDevices, 1, Marshal.SizeOf(typeof(RAWINPUTDEVICE))))
            {
                var lastError = Marshal.GetLastWin32Error();
                throw new Win32Exception(lastError, "Failed to register raw input devices");
            }
        }
    }
}
1 Like

Starting Unity 2021.2, perhaps you could try using this technique? https://docs.unity3d.com/2021.2/Documentation/ScriptReference/Windows.Input.ForwardRawInput.html

I believe raw input messages don’t arrive in -parentHwnd case due to Unity window becoming a child window. But if you create another window that’s in invisible in the Unity process and set up raw input events for that, you should be able to receive them.

@Tautvydas-Zilys alright I got this working with a bit of change! Initially, the code in the Windows.Input.ForwardRawInput.html documentation wasn’t working with my electron integration, but by adding the RIDEV_INPUTSINK flag on the RAWINPUTDEVICE it works like a charm!
RIDEV_INPUTSINK allows the created window (which is invisible) to receive input even when it’s in the foreground (which happens when you focus on the Unity window).

Essentially, this consists in modifying the example code to add the dwFlags (to make this work you also have to enable unsafe code in the player settings and add the doc example Monobehaviour on a gameobject):

ref RAWINPUTDEVICE keyboard = ref m_Devices[1];
(...
keyboard.dwFlags = 0x00000100;

With this technique, inputs will always be detected and forwarded to the unity child window. You have to control focus yourself by allowing or not the Update loop to run (I personally do this when focusing inside/outside of HTML using a custom IPC between Electron and Unity).

@Tautvydas-Zilys do you know if this will be backported to the stable versions of Unity (2020 etc.)?

In any case, this opens the door to native integrations of Unity with several UI toolkits (WPF, Qt, Electron) and brings a lot of value. Thanks a lot to all of those who helped!

No, but on older releases you can just forward WM_INPUT event directly using “SendMessage” function. It doesn’t work as of 2021.2 due to this: https://discussions.unity.com/t/856247

I’m surprised you need RIDEV_INPUTSINK flag. It shouldn’t be needed if the focused window belongs to the same process as the one you register raw input on. Also be careful with that flag. AV software likes to flag it as a keylogger.

@Tautvydas-Zilys good to know for the AV flagging, I’ve changed nothing in the doc example except RIDEV_INPUTSINK because I noticed that the inputs were only working when focusing on the created window (after adding the WS_VISIBLE flag to CreateWindowEx at first, to make the window visible and focusable for tests purposes).

Regarding making this work with Unity 2020, I have tried with SendMessage and it works like a charm (still need RIDEV_INPUTSINK though). The only tricky thing is to gather the unityHWND from Unity itself ( see this post ).

For those coming after me, you’ll have to replace this chunk of code to have it work in Unity 2020:

    [DllImport("user32.dll")]
    static extern int SendMessage(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam);
    static IntPtr unityHWND = IntPtr.Zero;

....

Awake(){
 // Here write some code to gather the unity HWND, there are several ways to achieve this
 // This is mandatory for this code to work
 // Personally I've done it using EnumThreadWindows native method, but I'm not sure this is recommended, therefore I have not included it there
 // See https://discussions.unity.com/t/699477
unityHWND = ...;
}

...

// Update the example method with SendMessage
    [MonoPInvokeCallback(typeof(WndProcDelegate))]
    static IntPtr WndProc(IntPtr window, uint message, IntPtr wParam, IntPtr lParam)
    {
        try
        {
            if (message == WM_INPUT)
            {
                SendMessage(unityHWND, message, wParam, lParam);
                // ProcessRawInputMessage(lParam);
            }

            return DefWindowProcW(window, message, wParam, lParam);
        }
        catch (Exception e)
        {
            // Never let exception escape to native code as that will crash the app
            Debug.LogException(e);
            return IntPtr.Zero;
        }
    }

Once again @Tautvydas-Zilys thanks for your help, it’s really appreciated :slight_smile:

Hi,
I had the same issue of the keyboard inputs not being detected and tried adding this code but I get the following error on line 168 of the attached code below:

System.ArgumentException: 'Object of type 'System.Int32' cannot be converted to type 'Interop+User32+WM'.'

Attached code (unity integrated into WPF app):

using System;
using System.ComponentModel;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Threading;
using System.Windows;
using System.Windows.Interop;

namespace Telekinesis.UnityApp
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        [DllImport("User32.dll")]
        static extern bool MoveWindow(IntPtr handle, int x, int y, int width, int height, bool redraw);

        internal delegate int WindowEnumProc(IntPtr hwnd, IntPtr lparam);
        [DllImport("user32.dll")]
        internal static extern bool EnumChildWindows(IntPtr hwnd, WindowEnumProc func, IntPtr lParam);

        [DllImport("user32.dll")]
        static extern int SendMessage(IntPtr hWnd, int msg, IntPtr wParam, IntPtr lParam);

        private Process _process;
        private IntPtr _unityHWND = IntPtr.Zero;
        bool initialized = false;

        private const int GWLP_WNDPROC = -4;
        private const int WM_INPUT = 0x00FF;
        private const int WM_ACTIVATE = 0x0006;
        private readonly IntPtr WA_ACTIVE = new IntPtr(1);
        private readonly IntPtr WA_INACTIVE = new IntPtr(0);

        // To capture key presses and send to unity
        [StructLayout(LayoutKind.Sequential)]
        public struct RAWINPUTDEVICE
        {
            public ushort usUsagePage;
            public ushort usUsage;
            public uint dwFlags;
            public IntPtr hwndTarget;
        }
        const ushort HID_USAGE_PAGE_GENERIC = 0x01;
        const ushort HID_USAGE_GENERIC_KEYBOARD = 0x06;

        [DllImport("user32.dll", SetLastError = true)]
        public static extern bool RegisterRawInputDevices([MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 0)] RAWINPUTDEVICE[] pRawInputDevices, int uiNumDevices, int cbSize);
        [DllImport("user32.dll", SetLastError = true)]
        private static extern IntPtr SetWindowLongPtrW(IntPtr hWnd, int nIndex, IntPtr dwNewLong);

        private new delegate IntPtr WndProc(IntPtr hWnd, int msg, IntPtr wParam, IntPtr lParam);

        private MulticastDelegate originalWndProc;
        private WndProc myWndProc;


        public MainWindow()
        {
            InitializeComponent();

            System.Windows.Threading.DispatcherTimer dispatcherTimer = new System.Windows.Threading.DispatcherTimer();
            dispatcherTimer.Tick += attemptInit;
            dispatcherTimer.Interval = new TimeSpan(0, 0, 1);
            dispatcherTimer.Start();

        }

        void attemptInit(object sender, EventArgs e)
        {

            if (initialized)
                return;

            //HwndSource source = (HwndSource)HwndSource.FromVisual(UnityGrid);

            //IntPtr hWnd = source.Handle;
            IntPtr unityHandle = UnityGrid.Handle;

            try
            {
                _process = new Process();
                _process.StartInfo.FileName = "UnityWindow.exe";
                _process.StartInfo.Arguments = "-parentHWND " + unityHandle.ToInt32() + " " + Environment.CommandLine;
                _process.StartInfo.UseShellExecute = true;
                _process.StartInfo.CreateNoWindow = true;

                _process.Start();

                _process.WaitForInputIdle();
                // Doesn't work for some reason ?!
                //hWnd = _process.MainWindowHandle;
                EnumChildWindows(unityHandle, WindowEnum, IntPtr.Zero);
                SetupRawInput(unityHandle);

                Debug.WriteLine("Unity HWND: 0x" + _unityHWND.ToString("X8"));

                UnityContentResize(this, EventArgs.Empty);
                PresentationSource s = PresentationSource.FromVisual(Application.Current.MainWindow);

                initialized = true;
            }
            catch (Exception ex)
            {
                MessageBox.Show(ex.Message + ".\nCheck if Container.exe is placed next to UnityGame.exe.");
            }
        }

        private void ActivateUnityWindow()
        {
            SendMessage(_unityHWND, WM_ACTIVATE, WA_ACTIVE, IntPtr.Zero);
        }

        private void DeactivateUnityWindow()
        {
            SendMessage(_unityHWND, WM_ACTIVATE, WA_INACTIVE, IntPtr.Zero);
        }

        private int WindowEnum(IntPtr hwnd, IntPtr lparam)
        {
            _unityHWND = hwnd;
            ActivateUnityWindow();
            return 0;
        }

        private void UnityContentResize(object sender, EventArgs e)
        {
            MoveWindow(_unityHWND, 0, 0, (int)UnityGrid.Width, (int)UnityGrid.Height, true);
            Debug.WriteLine("RESIZED UNITY WINDOW TO: " + (int)UnityGrid.Width + "x" + (int)UnityGrid.Height);
            ActivateUnityWindow();
        }

        // Close Unity application
        private void ApplicationExit(object sender, EventArgs e)
        {
            try
            {
                _process.CloseMainWindow();

                Thread.Sleep(1000);
                while (!_process.HasExited)
                    _process.Kill();
            }
            catch (Exception)
            {
            }
        }

        private void UnityContentActivate(object sender, EventArgs e)
        {
            ActivateUnityWindow();
        }

        private void UnityContentDeactivate(object sender, EventArgs e)
        {
            DeactivateUnityWindow();
        }

        private IntPtr HookWndProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam)
        {
            if (msg == WM_INPUT)
            {
                SendMessage(_unityHWND, msg, wParam, lParam);
                return IntPtr.Zero;
            }

            return (IntPtr)originalWndProc.DynamicInvoke(new object[] { hwnd, msg, wParam, lParam });
        }

        private void SetupRawInput(IntPtr hostHWND)
        {
            myWndProc = HookWndProc;

            var originalWndProcPtr = SetWindowLongPtrW(hostHWND, GWLP_WNDPROC, Marshal.GetFunctionPointerForDelegate(myWndProc));
            if (originalWndProcPtr == null)
            {
                var errorCode = Marshal.GetLastWin32Error();
                throw new Win32Exception(errorCode, "Failed to overwrite the original wndproc");
            }

            Type lel = typeof(MulticastDelegate);
            originalWndProc = (MulticastDelegate)Marshal.GetDelegateForFunctionPointer(originalWndProcPtr, lel);

            var rawInputDevices = new[]
            {
               new RAWINPUTDEVICE()
               {
                   usUsagePage = HID_USAGE_PAGE_GENERIC,
                   usUsage = HID_USAGE_GENERIC_KEYBOARD,
                   dwFlags = 0,
                   hwndTarget = hostHWND
               }
            };

            if (!RegisterRawInputDevices(rawInputDevices, 1, Marshal.SizeOf(typeof(RAWINPUTDEVICE))))
            {
                var lastError = Marshal.GetLastWin32Error();
                throw new Win32Exception(lastError, "Failed to register raw input devices");
            }
        }
    }
}

@Tautvydas-Zilys Do you have any insights as to what may be going wrong?

Yeah you don’t define your own WndProc delegate so it ends up using the wrong definition.

I’m a little new to this, how would I go about defining my WndProc delegate? Could you provide a small code example?

The example I posted had this line:

private new delegate IntPtr WndProc(IntPtr hWnd, int msg, IntPtr wParam, IntPtr lParam);

You seem to have removed it.