IME switch can't work smoothly in WinUI3

水 知 485 Reputation points
2026-08-09T16:03:47.26+00:00

Hi community!

I'm working with a tool to make IME automatically switch to English when switched to serveral apps.

I've finished a hardly usable demo but it have some problems. In some cases the IME can't switch back when quitting those specific apps. Such as:

  • Switching apps with Alt + Tab hotkey
  • Switching apps with mouse pointer click on taskbar icons fast and frequently
  • Switching apps with mouse point click on the windows fast and frequently
  • Clicking the window of this tool, the IME status won't change back

These problems may not all appear on this version. Here's my code.

    public class IMEBlocker
    {
        // Event hook
        private IntPtr _foregroundHook = IntPtr.Zero;
        private IntPtr _focusHook = IntPtr.Zero;
        private Win32.WinEventDelegate? _foregroundDelegate;
        private Win32.WinEventDelegate? _focusDelegate;
        private readonly object _hookSync = new();

        private IntPtr _englishHkl = IntPtr.Zero;

        // Saved IME stat
        private IntPtr _savedHkl = IntPtr.Zero;
        private IntPtr _savedHwnd = IntPtr.Zero;
        private bool? _savedImeOpen = null;
        private bool _isInBlockedApp = false;

        // Last window stat
        private IntPtr _prevHwnd = IntPtr.Zero;
        private uint _prevThreadId = 0;
        private bool _prevWasBlocked = false;

        // Queue
        private readonly ConcurrentQueue<Action> _actionQueue = new();
        private int _isProcessingQueue = 0;

        public static IMEBlocker Current
        {
            get => LazyInitializer.Instance;
        }
        private static class LazyInitializer
        {
            static LazyInitializer()
            {
            }
            public static readonly IMEBlocker Instance = new();
        }

        #region MainFunctionHelper
        public void StartWatcher()
        {
            lock (_hookSync)
            {
                if (_foregroundHook != IntPtr.Zero && _focusHook != IntPtr.Zero)
                    return;

                try
                {
                    _englishHkl = Win32.LoadKeyboardLayout("00000409", 0);
                }
                catch { _englishHkl = IntPtr.Zero; }

                _foregroundDelegate = ForegroundWinEventProc;
                _foregroundHook = Win32.SetWinEventHook(
                    Win32.EVENT_SYSTEM_FOREGROUND,
                    Win32.EVENT_SYSTEM_FOREGROUND,
                    IntPtr.Zero,
                    _foregroundDelegate,
                    0, 0,
                    Win32.WINEVENT_OUTOFCONTEXT | Win32.WINEVENT_SKIPOWNPROCESS
                );

                _focusDelegate = FocusWinEventProc;
                _focusHook = Win32.SetWinEventHook(
                    Win32.EVENT_OBJECT_FOCUS,
                    Win32.EVENT_OBJECT_FOCUS,
                    IntPtr.Zero,
                    _focusDelegate,
                    0, 0,
                    Win32.WINEVENT_OUTOFCONTEXT | Win32.WINEVENT_SKIPOWNPROCESS
                );

                _savedHkl = IntPtr.Zero;
                _savedImeOpen = null;
                _isInBlockedApp = false;
                _prevHwnd = IntPtr.Zero;
                _prevThreadId = 0;
                _prevWasBlocked = false;
            }
        }

        public void StopWatcher()
        {
            lock (_hookSync)
            {
                if (_foregroundHook != IntPtr.Zero)
                {
                    Win32.UnhookWinEvent(_foregroundHook);
                    _foregroundHook = IntPtr.Zero;
                }

                if (_focusHook != IntPtr.Zero)
                {
                    Win32.UnhookWinEvent(_focusHook);
                    _focusHook = IntPtr.Zero;
                }

                _foregroundDelegate = null;
                _focusDelegate = null;

                _savedHkl = IntPtr.Zero;
                _savedImeOpen = null;
                _isInBlockedApp = false;

                // Empty queue
                while (_actionQueue.TryDequeue(out _))
                {
                }
            }
        }
        private void ForegroundWinEventProc(IntPtr hWinEventHook, uint eventType, IntPtr hwnd,
            int idObject, int idChild, uint dwEventThread, uint dwmsEventTime)
        {
            if (hwnd == IntPtr.Zero)
                return;

            EnqueueAction(() => HandleForegroundChange(hwnd));
        }

        private void FocusWinEventProc(IntPtr hWinEventHook, uint eventType, IntPtr hwnd,
            int idObject, int idChild, uint dwEventThread, uint dwmsEventTime)
        {
            if (hwnd == IntPtr.Zero)
                return;

            EnqueueAction(() =>
            {
                HandleForegroundChange(hwnd);
                //try
                //{
                //    if (!_isInBlockedApp)
                //        return;
                //    if (!Win32.IsWindow(hwnd))
                //        return;

                //    if (IsBlockedWindow(hwnd))
                //    {
                //        ForceEnglish(hwnd);
                //    }
                //}
                //catch (Exception ex)
                //{
                //}
            });
        }
        private void EnqueueAction(Action action)
        {
            if (action == null)
                return;

            _actionQueue.Enqueue(action);
            ProcessQueue();
        }

        private void ForceEnglish(IntPtr hwnd)
        {
            Debug.WriteLine("ForceEnglish");
            if (hwnd == IntPtr.Zero)
                return;
            if (_englishHkl == IntPtr.Zero)
                return;

            try
            {
                Win32.PostMessage(hwnd, Win32.WM_INPUTLANGCHANGEREQUEST, (IntPtr)1, _englishHkl);
            }
            catch (Exception ex)
            {

            }
        }

        private async void ProcessQueue()
        {
            if (Interlocked.CompareExchange(ref _isProcessingQueue, 1, 0) == 1)
                return;

            try
            {
                var actions = new List<Action>();
                while (_actionQueue.TryDequeue(out Action? action))
                {
                    actions.Add(action);
                }

                if (actions.Count == 0)
                    return;

                var lastAction = actions.Last();
                await Task.Run(() => lastAction());
            }
            finally
            {
                Interlocked.Exchange(ref _isProcessingQueue, 0);

                if (!_actionQueue.IsEmpty)
                {
                    ProcessQueue();
                }
            }
        }

        private bool IsBlockedWindow(IntPtr hwnd)
        {
            if (hwnd == IntPtr.Zero)
                return false;
            uint processId = Win32.GetWindowThreadProcessId(hwnd, out _);
            return IsBlockedProcess(processId);
        }

        private bool IsBlockedProcess(uint processId)
        {
            try
            {
                var proc = Process.GetProcessById((int)processId);
                var exe = proc.ProcessName + ".exe";
                bool result = IMEBlockerConfig.Current.TargetAppList.Any(x => x.AppName.ToLower() == exe.ToLower());
                Debug.WriteLine("IsBlockedProcess:" + result);
                return result;
            }
            catch
            {
                Debug.WriteLine("IsBlockedProcess: catch");
                return false;
            }
        }

        private void HandleForegroundChange(IntPtr hwnd)
        {
            Debug.WriteLine("HandleForegroundChange");
            try
            {
                lock (_hookSync)
                {
                    // Make sure hwnd is valid.
                    if (!Win32.IsWindow(hwnd))
                        return;

                    uint threadId = Win32.GetWindowThreadProcessId(hwnd, out uint processId);
                    bool isBlocked = IsBlockedProcess(processId);



                    if (isBlocked && !_isInBlockedApp)
                    {
                        Debug.WriteLine("HandleForegroundChange: Case 1");

                        if (_prevHwnd != IntPtr.Zero && !_prevWasBlocked)
                        {
                            SaveImeState(_prevHwnd);
                        }

                        ForceEnglish(hwnd);
                        _isInBlockedApp = true;
                    }

                    if (!isBlocked && _isInBlockedApp)
                    {
                        Debug.WriteLine("HandleForegroundChange: Case 2");

                        RestoreImeState(hwnd);
                        _isInBlockedApp = false;
                    }

                    if (isBlocked && _isInBlockedApp)
                    {
                        Debug.WriteLine("HandleForegroundChange: Case 3");

                        ForceEnglish(hwnd);
                    }

                    if (!isBlocked && !_isInBlockedApp && _savedHkl != IntPtr.Zero)
                    {
                        Debug.WriteLine("HandleForegroundChange: Case 4");

                        _savedHkl = IntPtr.Zero;
                        _savedImeOpen = null;
                    }

                    _prevHwnd = hwnd;
                    _prevThreadId = threadId;
                    _prevWasBlocked = isBlocked;
                }
            }
            catch (Exception ex)
            {
                Debug.WriteLine("HandleForegroundChange: Exception");

            }
        }

        private void SaveImeState(IntPtr hwnd)
        {
            Debug.WriteLine("SaveImeState");
            if (hwnd == IntPtr.Zero)
                return;
            try
            {
                var layout = GetWindowLayout(hwnd);
                if (layout != IntPtr.Zero)
                {
                    _savedHwnd = hwnd;
                    _savedHkl = layout;
                    _savedImeOpen = null;

                    var hImc = Win32.ImmGetContext(hwnd);
                    if (hImc != IntPtr.Zero)
                    {
                        _savedImeOpen = Win32.ImmGetOpenStatus(hImc);
                        Win32.ImmReleaseContext(hwnd, hImc);
                    }
                }
            }
            catch { }
        }


        private void RestoreImeState(IntPtr hwnd)
        {
            Debug.WriteLine("RestoreImeState");
            if (hwnd == IntPtr.Zero || _savedHkl == IntPtr.Zero)
            {
                Debug.WriteLine("RestoreImeState: IntPtrZero");
                return;
            }
            //if (_savedHwnd != IntPtr.Zero && _savedHwnd != hwnd)
            //{
            //    _savedHkl = IntPtr.Zero;
            //    _savedImeOpen = null;
            //    _savedHwnd = IntPtr.Zero;
            //    return;
            //}
            try
            {
                if (!Win32.IsWindow(hwnd))
                {
                    Debug.WriteLine("RestoreImeState: !IsWindow");
                    return;
                }

                Win32.PostMessage(hwnd, Win32.WM_INPUTLANGCHANGEREQUEST, (IntPtr)1, _savedHkl);
                if (_savedImeOpen != null)
                {
                    var hImc = Win32.ImmGetContext(hwnd);
                    if (hImc != IntPtr.Zero)
                    {
                        Win32.ImmSetOpenStatus(hImc, _savedImeOpen.Value);
                        Win32.ImmReleaseContext(hwnd, hImc);
                    }
                }
                Debug.WriteLine("RestoreImeState: Succeed");
            }
            catch
            {
                Debug.WriteLine("RestoreImeState: Catch");
            }
            finally
            {
                _savedHkl = IntPtr.Zero;
                _savedImeOpen = null;
                _savedHwnd = IntPtr.Zero;
            }
        }
        private IntPtr GetWindowLayout(IntPtr hwnd)
        {
            if (hwnd == IntPtr.Zero)
                return IntPtr.Zero;
            uint threadId = Win32.GetWindowThreadProcessId(hwnd, out _);
            return Win32.GetKeyboardLayout(threadId);
        }

        #endregion
    }
    internal class Win32
    {
        public const uint WM_INPUTLANGCHANGEREQUEST = 0x0050;

        public const uint EVENT_SYSTEM_FOREGROUND = 0x0003;
        public const uint EVENT_OBJECT_FOCUS = 0x8005;
        public const uint EVENT_OBJECT_NAMECHANGE = 0x800C;

        public const uint WINEVENT_OUTOFCONTEXT = 0x0000;
        public const uint WINEVENT_SKIPOWNPROCESS = 0x0002;

        public const int OBJID_WINDOW = 0x0000;
        public const int OBJID_CLIENT = 0xFFFF;

        [DllImport("user32.dll")]
        public static extern IntPtr GetForegroundWindow();

        [DllImport("user32.dll")]
        public static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId);

        [DllImport("user32.dll")]
        public static extern IntPtr GetKeyboardLayout(uint idThread);

        [DllImport("user32.dll", CharSet = CharSet.Unicode)]
        public static extern IntPtr LoadKeyboardLayout(string pwszKLID, uint Flags);

        [DllImport("user32.dll")]
        public static extern IntPtr SendMessage(IntPtr hWnd, uint Msg, IntPtr wParam, IntPtr lParam);
        [DllImport("user32.dll")]
        public static extern IntPtr PostMessage(IntPtr hWnd, uint Msg, IntPtr wParam, IntPtr lParam);

        [DllImport("user32.dll")]
        public static extern IntPtr GetFocus();

        [DllImport("user32.dll")]
        public static extern IntPtr GetParent(IntPtr hWnd);

        [DllImport("user32.dll")]
        public static extern bool IsWindow(IntPtr hWnd);

        [DllImport("user32.dll")]
        public static extern IntPtr SetWinEventHook(uint eventMin, uint eventMax, IntPtr hmodWinEventProc,
            WinEventDelegate lpfnWinEventProc, uint idProcess, uint idThread, uint dwFlags);

        [DllImport("user32.dll")]
        [return: MarshalAs(UnmanagedType.Bool)]
        public static extern bool UnhookWinEvent(IntPtr hWinEventHook);

        [DllImport("imm32.dll")]
        public static extern IntPtr ImmGetContext(IntPtr hWnd);

        [DllImport("imm32.dll")]
        [return: MarshalAs(UnmanagedType.Bool)]
        public static extern bool ImmGetOpenStatus(IntPtr hIMC);

        [DllImport("imm32.dll")]
        [return: MarshalAs(UnmanagedType.Bool)]
        public static extern bool ImmSetOpenStatus(IntPtr hIMC, bool fOpen);

        [DllImport("imm32.dll")]
        public static extern bool ImmReleaseContext(IntPtr hWnd, IntPtr hIMC);

        [DllImport("imm32.dll")]
        public static extern IntPtr ImmAssociateContext(IntPtr hWnd, IntPtr hIMC);
        [DllImport("user32.dll")]
        public static extern IntPtr ActivateKeyboardLayout(IntPtr hkl, uint flags);
        public const uint KLF_ACTIVATE = 0x0001;

        public delegate void WinEventDelegate(IntPtr hWinEventHook, uint eventType, IntPtr hwnd,
            int idObject, int idChild, uint dwEventThread, uint dwmsEventTime);


        [DllImport("imm32.dll")]
        public static extern IntPtr ImmCreateContext();

        [DllImport("imm32.dll")]
        public static extern bool ImmDestroyContext(IntPtr hIMC);

        [DllImport("kernel32.dll")]
        public static extern uint GetCurrentThreadId();

        [DllImport("user32.dll")]
        [return: MarshalAs(UnmanagedType.Bool)]
        public static extern bool AttachThreadInput(uint idAttach, uint idAttachTo, bool fAttach);

        [StructLayout(LayoutKind.Sequential)]
        public struct MSG
        {
            public IntPtr hwnd;
            public uint message;
            public UIntPtr wParam;
            public IntPtr lParam;
            public uint time;
            public System.Drawing.Point pt;
        }

        [DllImport("user32.dll")]
        [return: MarshalAs(UnmanagedType.Bool)]
        public static extern bool PeekMessage(out MSG lpMsg, IntPtr hWnd, uint wMsgFilterMin, uint wMsgFilterMax, uint wRemoveMsg);

        [DllImport("user32.dll", SetLastError = true)]
        public static extern IntPtr SendMessageTimeout(IntPtr hWnd, uint Msg, IntPtr wParam, IntPtr lParam, uint fuFlags, uint uTimeout, out IntPtr lpdwResult);

        public const uint SMTO_ABORTIFHUNG = 0x0002;
    }

Plus, IMEBlockerConfig.Current.TargetAppList is a public ObservableCollection<TargetAppListCardItemDTO>. Here is code of the DTO

    public class TargetAppListCardItemDTO
    {
        public string AppName { get; set; } = string.Empty;
        public bool IsEnabled { get; set; } = true;
    }

I also tried using PostMessage to replace SendMessage but doesn't works.

I wonder why these problems happens and how to fix that. Please help!

Windows development | WinUI
0 comments No comments

Answer accepted by question author
Jay Pham (WICLOUD CORPORATION) 4,435 Reputation points Microsoft External Staff Moderator
2026-08-10T01:55:34.5+00:00

Hello @水 知 ,

I reviewed the code and I do not currently see evidence that this is a WinUI 3-specific defect. The behavior is mainly caused by how the utility observes foreground changes and accesses IME state across process and thread boundaries.

There are three important issues in the current implementation:

  1. Both hooks include WINEVENT_SKIPOWNPROCESS. This flag prevents the hook from receiving events generated by your own process. Therefore, clicking the utility window cannot trigger the restore path. Remove this flag if the utility's activation must also be observed.
  2. SaveImeState and RestoreImeState call ImmGetContext, ImmGetOpenStatus, and ImmSetOpenStatus for windows owned by other threads or processes. Windows performs thread ownership checks for IMM handles. These calls can fail with ERROR_INVALID_ACCESS, especially because they are executed from Task.Run.
  3. Foreground events are handled asynchronously, intermediate events are discarded, and the queued HWND is not checked against the current foreground window. During rapid Alt+Tab or mouse switching, the code can therefore act on an HWND that is no longer active.

I recommend the following changes:

  • Use EVENT_SYSTEM_FOREGROUND as the application-switch signal and remove WINEVENT_SKIPOWNPROCESS if your own window must be detected.
  • When processing an event, call GetForegroundWindow() again instead of relying on the queued HWND.
  • Use GetGUIThreadInfo to obtain the actual focused HWND before posting WM_INPUTLANGCHANGEREQUEST.
  • Do not use IMM APIs to read or update another thread's input context. Let Windows retain each application's IME state, and only request English when a configured application becomes foreground.
  • Enable the Windows option "Let me use a different input method for each app window" if per-application input-method selection is required.
  • Log API return values and Marshal.GetLastWin32Error() instead of suppressing all failures.

Please note that WM_INPUTLANGCHANGEREQUEST is a request to the focused window. The target application may accept it through DefWindowProc or reject it. Therefore, an external utility cannot guarantee exact IME open/conversion-state control for every application. If exact control is required, the IME operation must run in the target application's UI thread or through an integration supported by that application.

Microsoft documentation:

If the issue remains after these changes, please provide a minimal reproducible project, the Windows version, Windows App SDK version, selected IME, and logs containing the event HWND, current foreground HWND, target thread ID, and Win32 error code.

If you found my response helpful or informative, I would greatly appreciate it if you could provide feedback by interacting with the system or leaving a comment below.

Thank you.

Was this answer helpful?

1 person found this answer helpful.

1 additional answer

Sort by: Most helpful
  1. Jay Pham (WICLOUD CORPORATION) 4,435 Reputation points Microsoft External Staff Moderator
    2026-08-10T00:37:52.1566667+00:00

    Hello @水 知 ,

    I am currently working the issue and will provide an update soon. Thank you for your patience.

    Was this answer helpful?


Your answer

Answers can be marked as 'Accepted' by the question author and 'Recommended' by moderators, which helps users know the answer solved the author's problem.