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!