150 lines
5.8 KiB
C#
150 lines
5.8 KiB
C#
using System.Runtime.InteropServices;
|
|
using System.Windows;
|
|
using System.Windows.Interop;
|
|
using System.Windows.Threading;
|
|
|
|
namespace CursorLang;
|
|
|
|
/// <summary>
|
|
/// Всплывающая подсказка у курсора с коротким именем раскладки.
|
|
/// </summary>
|
|
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();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Показывает подсказку у курсора и скрывает её по таймеру.
|
|
/// </summary>
|
|
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;
|
|
}
|
|
}
|