using System.Runtime.InteropServices; using System.Windows; using System.Windows.Interop; using System.Windows.Threading; namespace CursorLang; /// /// Всплывающая подсказка у курсора с коротким именем раскладки. /// public partial class LayoutPopupWindow : Window { [StructLayout(LayoutKind.Sequential)] private struct Point { public int X; public int Y; } [DllImport("user32.dll")] private static extern bool GetCursorPos(out Point lpPoint); [DllImport("user32.dll", SetLastError = true)] private static extern int GetWindowLong(IntPtr hWnd, int nIndex); [DllImport("user32.dll")] private static extern int SetWindowLong(IntPtr hWnd, int nIndex, int dwNewLong); [DllImport("user32.dll")] private static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int X, int Y, int cx, int cy, uint uFlags); [DllImport("user32.dll")] private static extern IntPtr MonitorFromPoint(Point pt, uint dwFlags); [DllImport("shcore.dll")] private static extern int GetDpiForMonitor(IntPtr hMonitor, int dpiType, out uint dpiX, out uint dpiY); private const int GWL_EXSTYLE = -20; // Окно не забирает фокус у активного приложения private const int WS_EX_NOACTIVATE = 0x08000000; // И не попадает в Alt+Tab private const int WS_EX_TOOLWINDOW = 0x00000080; private const uint SWP_NOSIZE = 0x0001; private const uint SWP_NOZORDER = 0x0004; private const uint SWP_NOACTIVATE = 0x0010; private const uint MONITOR_DEFAULTTONEAREST = 2; private const int MDT_EFFECTIVE_DPI = 0; // Отступ от курсора, чтобы подсказка не оказалась под ним. // Задан в единицах WPF и пересчитывается в пиксели того монитора, // на котором сейчас курсор private const double CursorOffset = 16; private readonly DispatcherTimer _hideTimer; private Point _cursor; public LayoutPopupWindow() { InitializeComponent(); _hideTimer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(500) }; _hideTimer.Tick += (_, _) => { _hideTimer.Stop(); Hide(); }; } // Хэндл создан, но окно ещё не отрисовано: здесь и настраиваем стили, // и ставим окно на место, чтобы первый показ прошёл уже на нужном мониторе protected override void OnSourceInitialized(EventArgs e) { base.OnSourceInitialized(e); ApplyPassiveWindowStyles(); MoveToCursor(); } /// /// Показывает подсказку у курсора и скрывает её по таймеру. /// public void ShowAt(string text) { LayoutText.Text = text; ResizeToContent(); GetCursorPos(out _cursor); // Для уже созданного окна двигаем до показа; при самом первом вызове // хэндла ещё нет, и позиционирование выполнит OnSourceInitialized MoveToCursor(); Show(); // Перезапускаем таймер, чтобы быстрые переключения продлевали показ _hideTimer.Stop(); _hideTimer.Start(); } // Размер считаем сами, а не через SizeToContent: тот вычисляет его при создании // окна, когда содержимое ещё не измерено, и первый показ выходит шире текста. // Рамок у окна нет, поэтому его размер равен размеру содержимого private void ResizeToContent() { var content = (FrameworkElement)Content; content.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity)); Width = content.DesiredSize.Width; Height = content.DesiredSize.Height; } // Подсказка всплывает поверх чужих приложений, поэтому она не должна // ни активироваться сама, ни отбирать фокус ввода у активного окна private void ApplyPassiveWindowStyles() { IntPtr handle = new WindowInteropHelper(this).Handle; int exStyle = GetWindowLong(handle, GWL_EXSTYLE); SetWindowLong(handle, GWL_EXSTYLE, exStyle | WS_EX_NOACTIVATE | WS_EX_TOOLWINDOW); } // Позиционируем нативно, в физических пикселях: у мониторов разный масштаб, // а Window.Left/Top пересчитываются по DPI того монитора, где окно сейчас, // и на соседнем мониторе дают промах private void MoveToCursor() { IntPtr handle = new WindowInteropHelper(this).Handle; if (handle == IntPtr.Zero) { return; } int offset = (int)Math.Round(CursorOffset * GetScaleAt(_cursor)); SetWindowPos(handle, IntPtr.Zero, _cursor.X + offset, _cursor.Y + offset, 0, 0, SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE); } // Масштаб монитора, на котором находится точка private static double GetScaleAt(Point point) { IntPtr monitor = MonitorFromPoint(point, MONITOR_DEFAULTTONEAREST); if (GetDpiForMonitor(monitor, MDT_EFFECTIVE_DPI, out uint dpiX, out _) < 0) { return 1.0; } return dpiX / 96.0; } }