using System.Runtime.InteropServices; namespace CursorLang.Interop; /// /// Win32 API для окна-подсказки: стили, позиционирование у курсора /// и масштаб монитора, на котором курсор находится. /// internal static class PopupWindowNative { [StructLayout(LayoutKind.Sequential)] internal struct Point { public int X; public int Y; } [DllImport("user32.dll")] private static extern bool GetCursorPos(out Point lpPoint); [DllImport("user32.dll")] private static extern IntPtr GetForegroundWindow(); [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); [DllImport("user32.dll")] private static extern IntPtr MonitorFromWindow(IntPtr hWnd, uint dwFlags); [DllImport("user32.dll")] private static extern bool GetMonitorInfo(IntPtr hMonitor, ref MonitorInfo lpmi); [StructLayout(LayoutKind.Sequential)] internal struct Rect { public int Left; public int Top; public int Right; public int Bottom; } [StructLayout(LayoutKind.Sequential)] private struct MonitorInfo { public int cbSize; public Rect rcMonitor; public Rect rcWork; public uint dwFlags; } 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; internal static Point GetCursorPosition() { GetCursorPos(out Point cursor); return cursor; } /// /// Подсказка всплывает поверх чужих приложений, поэтому она не должна /// ни активироваться сама, ни отбирать фокус ввода у активного окна. /// internal static void MakePassive(IntPtr hWnd) { int exStyle = GetWindowLong(hWnd, GWL_EXSTYLE); SetWindowLong(hWnd, GWL_EXSTYLE, exStyle | WS_EX_NOACTIVATE | WS_EX_TOOLWINDOW); } /// /// Двигает окно в точку экрана, не меняя размер и порядок окон. /// Координаты — физические пиксели: у мониторов разный масштаб, а /// Window.Left/Top пересчитываются по DPI того монитора, где окно сейчас, /// и на соседнем мониторе дают промах. /// internal static void MoveTo(IntPtr hWnd, int x, int y) { SetWindowPos(hWnd, IntPtr.Zero, x, y, 0, 0, SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE); } /// /// Задаёт положение и размер окна в физических пикселях. /// /// /// Размер выставляется именно так, а не через Width/Height: при первом показе /// окно ещё подчиняется системному минимальному размеру окна (SM_CXMIN×SM_CYMIN) /// и подсказка выходит заметно крупнее текста. К моменту вызова окно уже /// показано и стало popup-окном, на которое это ограничение не действует. /// internal static void SetBounds(IntPtr hWnd, int x, int y, int width, int height) { SetWindowPos(hWnd, IntPtr.Zero, x, y, width, height, SWP_NOZORDER | SWP_NOACTIVATE); } /// Масштаб монитора, на котором находится точка (1.0 при 96 DPI). internal static double GetScaleAt(Point point) => GetScaleOf(MonitorFromPoint(point, MONITOR_DEFAULTTONEAREST)); /// /// Рабочая область монитора с активным окном — без панели задач — и его масштаб. /// Именно на этом мониторе пользователь сейчас работает. /// internal static (Rect WorkArea, double Scale) GetActiveMonitorWorkArea() { IntPtr monitor = MonitorFromWindow(GetForegroundWindow(), MONITOR_DEFAULTTONEAREST); var info = new MonitorInfo { cbSize = Marshal.SizeOf() }; if (!GetMonitorInfo(monitor, ref info)) { return (new Rect(), 1.0); } return (info.rcWork, GetScaleOf(monitor)); } private static double GetScaleOf(IntPtr monitor) { if (GetDpiForMonitor(monitor, MDT_EFFECTIVE_DPI, out uint dpiX, out _) < 0) { return 1.0; } return dpiX / 96.0; } }