using System.Windows; // ==== using System.Windows.Interop; using System.Windows.Threading; using System.Runtime.InteropServices; namespace CursorLang; /// /// Interaction logic for MainWindow.xaml /// public partial class MainWindow : Window { // Импорт нужных Win32 API [DllImport("user32.dll")] private static extern int RegisterWindowMessage(string lpString); [DllImport("user32.dll")] private static extern bool RegisterShellHookWindow(IntPtr hWnd); [DllImport("user32.dll")] private static extern bool DeregisterShellHookWindow(IntPtr hWnd); [DllImport("user32.dll")] private static extern IntPtr GetForegroundWindow(); [DllImport("user32.dll")] private static extern uint GetWindowThreadProcessId(IntPtr hWnd, IntPtr lpdwProcessId); [DllImport("user32.dll")] private static extern IntPtr GetKeyboardLayout(uint idThread); // Константы оболочки private const int HShellLanguage = 8; // Сообщение о смене языка ввода в самом окне private const int WmInputLangChange = 0x0051; private int _shellHookMessageId; private IntPtr _windowHandle; private LayoutPopupWindow? _popup; // Опрос активного окна: HShellLanguage в Windows 10/11 не приходит, а событийных // способов узнать о смене раскладки в чужом процессе из managed-кода нет private readonly DispatcherTimer _pollTimer = new() { Interval = TimeSpan.FromMilliseconds(150) }; private int _lastLocaleId = -1; private IntPtr _lastForegroundWindow; public MainWindow() { InitializeComponent(); } protected override void OnSourceInitialized(EventArgs e) { base.OnSourceInitialized(e); // 1. Получаем хэндл нашего окна (для WinForms используйте this.Handle) _windowHandle = new WindowInteropHelper(this).Handle; // 2. Регистрируем уникальное сообщение SHELLHOOK _shellHookMessageId = RegisterWindowMessage("SHELLHOOK"); // 3. Подписываем наше окно на получение событий оболочки RegisterShellHookWindow(_windowHandle); // 4. Добавляем обработчик сообщений окна (WndProc) HwndSource? source = HwndSource.FromHwnd(_windowHandle); if (source != null) source.AddHook(WndProc); // 5. Готовим всплывающее окно у курсора _popup = new LayoutPopupWindow(); // 6. Показываем раскладку, активную на момент запуска (без всплывающего окна) _lastForegroundWindow = GetForegroundWindow(); HandleLayout(GetLayoutOf(_lastForegroundWindow), showPopup: false); // 7. Запускаем опрос активного окна _pollTimer.Tick += PollForegroundLayout; _pollTimer.Start(); } // Раскладка потока, которому принадлежит окно private static int GetLayoutOf(IntPtr hWnd) { uint threadId = GetWindowThreadProcessId(hWnd, IntPtr.Zero); return (int)GetKeyboardLayout(threadId).ToInt64() & 0xFFFF; } private void PollForegroundLayout(object? sender, EventArgs e) { IntPtr foreground = GetForegroundWindow(); if (foreground == IntPtr.Zero) { return; } // Смена активного приложения сама по себе не является переключением // раскладки пользователем, поэтому подсказку в этом случае не показываем bool appSwitched = foreground != _lastForegroundWindow; _lastForegroundWindow = foreground; HandleLayout(GetLayoutOf(foreground), showPopup: !appSwitched); } private static System.Globalization.CultureInfo? GetCulture(int localeId) { try { return new System.Globalization.CultureInfo(localeId); } catch (System.Globalization.CultureNotFoundException) { return null; } } // Обновление лейбла с текущей раскладкой private void UpdateLabel(int localeId) { var culture = GetCulture(localeId); LayoutLabel.Content = culture is null ? $"0x{localeId:X4}" : $"{culture.TwoLetterISOLanguageName.ToUpperInvariant()} — {culture.NativeName}"; } // Единая точка обработки: лейбл в окне и подсказка у курсора. // Повторные сообщения об одной и той же раскладке отсекаются, // так как событие может прийти и от хука, и от таймера private void HandleLayout(int localeId, bool showPopup) { if (localeId == _lastLocaleId) { return; } _lastLocaleId = localeId; UpdateLabel(localeId); if (showPopup) { var culture = GetCulture(localeId); string shortName = culture?.TwoLetterISOLanguageName.ToUpperInvariant() ?? $"0x{localeId:X4}"; _popup?.ShowAt(shortName); } } // Перехватчик сообщений Windows private IntPtr WndProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled) { // Если пришло сообщение от Shell if (msg == _shellHookMessageId) { // Проверяем, что это именно событие смены языка if (wParam.ToInt32() == HShellLanguage) { // Раскладка изменилась! // lParam содержит дескриптор клавиатуры (HKL), можно извлечь язык напрямую, // либо вызвать метод из прошлого ответа для активного окна int localeId = (int)lParam.ToInt64() & 0xFFFF; // Здесь показываем UI возле курсора HandleLayout(localeId, showPopup: true); } } // Смена языка, когда фокус ввода находится в нашем окне: // shell hook в этом случае события не присылает else if (msg == WmInputLangChange) { HandleLayout((int)lParam.ToInt64() & 0xFFFF, showPopup: true); } return IntPtr.Zero; } protected override void OnClosed(EventArgs e) { // Обязательно отписываемся при закрытии _pollTimer.Stop(); DeregisterShellHookWindow(_windowHandle); _popup?.Close(); base.OnClosed(e); } }