182 lines
7.0 KiB
C#
182 lines
7.0 KiB
C#
using System.Windows;
|
|
// ====
|
|
using System.Windows.Interop;
|
|
using System.Windows.Threading;
|
|
using System.Runtime.InteropServices;
|
|
|
|
namespace CursorLang;
|
|
|
|
/// <summary>
|
|
/// Interaction logic for MainWindow.xaml
|
|
/// </summary>
|
|
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);
|
|
}
|
|
} |