lightweight variant (#1)
Reviewed-on: #1 Co-authored-by: Aleksandr Neychev <alexnejchev73@gmail.com>
This commit was merged in pull request #1.
This commit is contained in:
@@ -0,0 +1,161 @@
|
||||
using System.Drawing;
|
||||
using System.Runtime.InteropServices;
|
||||
using CursorLang.Core.Interop;
|
||||
|
||||
namespace CursorLang.Agent.Interop;
|
||||
|
||||
/// <summary>
|
||||
/// Plain GDI: a font, text, and an off-screen bitmap to draw them into.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// GDI rather than GDI+ on purpose. <c>System.Drawing.Common</c> would make the drawing
|
||||
/// code shorter, but it is a separate assembly with a native GDI+ library behind it, and
|
||||
/// how little this process weighs is the entire reason it exists apart from the window.
|
||||
/// Everything the popup needs — one rounded rectangle and one line of text — GDI can do
|
||||
/// on its own, and it draws the text with ClearType, exactly as Windows does everywhere else.
|
||||
/// </remarks>
|
||||
internal static class GdiNative
|
||||
{
|
||||
internal const int TRANSPARENT = 1;
|
||||
|
||||
internal const uint DT_SINGLELINE = 0x00000020;
|
||||
internal const uint DT_CENTER = 0x00000001;
|
||||
internal const uint DT_VCENTER = 0x00000004;
|
||||
internal const uint DT_CALCRECT = 0x00000400;
|
||||
internal const uint DT_NOPREFIX = 0x00000800;
|
||||
internal const uint DT_NOCLIP = 0x00000100;
|
||||
|
||||
private const int DEFAULT_CHARSET = 1;
|
||||
private const int OUT_TT_PRECIS = 4;
|
||||
private const int CLIP_DEFAULT_PRECIS = 0;
|
||||
private const int CLEARTYPE_QUALITY = 5;
|
||||
private const int DEFAULT_PITCH = 0;
|
||||
|
||||
/// <summary>
|
||||
/// The face the popup is written in.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// WPF is asked for "Segoe UI" at FontWeight SemiBold and resolves that to the
|
||||
/// seguisb.ttf face. To GDI that face is a family of its own — "Segoe UI Semibold" —
|
||||
/// and asking for the "Segoe UI" family at weight 600 lands on Bold instead, which
|
||||
/// is visibly heavier. So the family is named outright and the weight is left to the
|
||||
/// mapper: the family has one member and no synthetic emboldening happens.
|
||||
/// </remarks>
|
||||
private const string SemiBoldFace = "Segoe UI Semibold";
|
||||
|
||||
private const int FW_DONTCARE = 0;
|
||||
|
||||
/// <summary>
|
||||
/// A font of the given size in physical pixels.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The size in the settings is in WPF units, that is 1/96 inch, while GDI counts
|
||||
/// pixels — hence the multiplication by the monitor scale. The height is negative:
|
||||
/// that asks for the em size rather than the cell height, which is what a font size
|
||||
/// means everywhere else.
|
||||
/// </remarks>
|
||||
internal static IntPtr CreateFont(double wpfFontSize, double scale)
|
||||
{
|
||||
var height = (int)Math.Round(wpfFontSize * scale);
|
||||
|
||||
return CreateFontW(
|
||||
-height, 0, 0, 0, FW_DONTCARE,
|
||||
false, false, false,
|
||||
DEFAULT_CHARSET, OUT_TT_PRECIS, CLIP_DEFAULT_PRECIS, CLEARTYPE_QUALITY, DEFAULT_PITCH,
|
||||
SemiBoldFace);
|
||||
}
|
||||
|
||||
/// <summary>The size of a single line of text with the font selected into the context.</summary>
|
||||
internal static Size MeasureText(IntPtr deviceContext, string text)
|
||||
{
|
||||
var bounds = default(PopupWindowNative.Rect);
|
||||
DrawText(deviceContext, text, text.Length, ref bounds,
|
||||
DT_CALCRECT | DT_SINGLELINE | DT_NOPREFIX);
|
||||
|
||||
return new Size(bounds.Right - bounds.Left, bounds.Bottom - bounds.Top);
|
||||
}
|
||||
|
||||
/// <summary>A colour as GDI wants it: 0x00BBGGRR, the alpha carried elsewhere.</summary>
|
||||
internal static uint ToColorRef(Color color) =>
|
||||
(uint)(color.R | (color.G << 8) | (color.B << 16));
|
||||
|
||||
[DllImport("gdi32.dll", CharSet = CharSet.Unicode, EntryPoint = "CreateFontW")]
|
||||
private static extern IntPtr CreateFontW(int cHeight, int cWidth, int cEscapement, int cOrientation,
|
||||
int cWeight, bool bItalic, bool bUnderline, bool bStrikeOut,
|
||||
int iCharSet, int iOutPrecision, int iClipPrecision, int iQuality, int iPitchAndFamily,
|
||||
string pszFaceName);
|
||||
|
||||
/// <summary>
|
||||
/// A 32-bit surface to draw the popup into before anyone can see it.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Top-down — a negative height — so that the first row of <paramref name="bits"/>
|
||||
/// is the top row of the picture and the alpha fixing up afterwards can walk the
|
||||
/// memory straight through.
|
||||
/// </remarks>
|
||||
internal static IntPtr CreateSurface(IntPtr deviceContext, int width, int height, out IntPtr bits)
|
||||
{
|
||||
var header = new BitmapInfoHeader
|
||||
{
|
||||
biSize = Marshal.SizeOf<BitmapInfoHeader>(),
|
||||
biWidth = width,
|
||||
biHeight = -height,
|
||||
biPlanes = 1,
|
||||
biBitCount = 32,
|
||||
biCompression = BI_RGB,
|
||||
};
|
||||
|
||||
return CreateDIBSection(deviceContext, ref header, DIB_RGB_COLORS, out bits, IntPtr.Zero, 0);
|
||||
}
|
||||
|
||||
private const uint BI_RGB = 0;
|
||||
private const uint DIB_RGB_COLORS = 0;
|
||||
|
||||
[DllImport("gdi32.dll")]
|
||||
internal static extern IntPtr CreateCompatibleDC(IntPtr hdc);
|
||||
|
||||
[DllImport("gdi32.dll")]
|
||||
internal static extern bool DeleteDC(IntPtr hdc);
|
||||
|
||||
[DllImport("gdi32.dll")]
|
||||
private static extern IntPtr CreateDIBSection(IntPtr hdc, ref BitmapInfoHeader header, uint usage,
|
||||
out IntPtr bits, IntPtr section, uint offset);
|
||||
|
||||
[DllImport("gdi32.dll")]
|
||||
internal static extern IntPtr SelectObject(IntPtr hdc, IntPtr h);
|
||||
|
||||
[DllImport("gdi32.dll")]
|
||||
internal static extern bool DeleteObject(IntPtr ho);
|
||||
|
||||
[DllImport("gdi32.dll")]
|
||||
internal static extern int SetBkMode(IntPtr hdc, int mode);
|
||||
|
||||
[DllImport("gdi32.dll")]
|
||||
internal static extern uint SetTextColor(IntPtr hdc, uint color);
|
||||
|
||||
[DllImport("user32.dll", CharSet = CharSet.Unicode, EntryPoint = "DrawTextW")]
|
||||
internal static extern int DrawText(IntPtr hdc, string lpchText, int cchText,
|
||||
ref PopupWindowNative.Rect lprc, uint format);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
internal static extern IntPtr GetDC(IntPtr hWnd);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
internal static extern int ReleaseDC(IntPtr hWnd, IntPtr hDC);
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
private struct BitmapInfoHeader
|
||||
{
|
||||
public int biSize;
|
||||
public int biWidth;
|
||||
public int biHeight;
|
||||
public short biPlanes;
|
||||
public short biBitCount;
|
||||
public uint biCompression;
|
||||
public uint biSizeImage;
|
||||
public int biXPelsPerMeter;
|
||||
public int biYPelsPerMeter;
|
||||
public uint biClrUsed;
|
||||
public uint biClrImportant;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
using System.Runtime.InteropServices;
|
||||
using CursorLang.Core.Interop;
|
||||
|
||||
namespace CursorLang.Agent.Interop;
|
||||
|
||||
/// <summary>
|
||||
/// The system context menu — the one the tray icon raises.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// A menu built this way is drawn by Windows, so the theme and the language chosen in
|
||||
/// the application no longer reach it. That is the accepted price of leaving WPF: a WPF
|
||||
/// <c>ContextMenu</c> costs the whole rendering stack in the background process.
|
||||
/// </remarks>
|
||||
internal static class MenuNative
|
||||
{
|
||||
private const uint MF_STRING = 0x00000000;
|
||||
private const uint MF_SEPARATOR = 0x00000800;
|
||||
private const uint MF_GRAYED = 0x00000001;
|
||||
|
||||
private const uint TPM_LEFTALIGN = 0x0000;
|
||||
private const uint TPM_RIGHTBUTTON = 0x0002;
|
||||
private const uint TPM_RETURNCMD = 0x0100;
|
||||
|
||||
/// <summary>An item of the menu being built.</summary>
|
||||
/// <param name="Id">What <see cref="Track"/> gives back when the item is chosen.</param>
|
||||
/// <param name="Caption">The text, or <c>null</c> for a separator.</param>
|
||||
/// <param name="IsEnabled">A greyed item is shown but cannot be chosen.</param>
|
||||
internal readonly record struct Item(int Id, string? Caption, bool IsEnabled = true)
|
||||
{
|
||||
internal static Item Separator => new(0, null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Raises the menu at a screen point and returns the identifier of the chosen item,
|
||||
/// or zero when the user dismissed it.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <c>TPM_RETURNCMD</c> means the answer comes back from the call itself instead of
|
||||
/// as a <c>WM_COMMAND</c> later, which keeps the whole menu in one place. The call
|
||||
/// does not return until the user is done with the menu — that is how a modal menu
|
||||
/// works, and the message loop keeps running inside it.
|
||||
///
|
||||
/// The window is brought to the foreground first and poked with an empty message
|
||||
/// afterwards: without the first the menu never closes on a click elsewhere, and
|
||||
/// without the second it stays on screen after the choice is made. Both are
|
||||
/// long-standing quirks of a menu owned by a window the user cannot see.
|
||||
/// </remarks>
|
||||
internal static int Track(IntPtr owner, PopupWindowNative.Point at, IReadOnlyList<Item> items)
|
||||
{
|
||||
IntPtr menu = CreatePopupMenu();
|
||||
if (menu == IntPtr.Zero)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
foreach (Item item in items)
|
||||
{
|
||||
if (item.Caption is null)
|
||||
{
|
||||
AppendMenu(menu, MF_SEPARATOR, IntPtr.Zero, null);
|
||||
continue;
|
||||
}
|
||||
|
||||
uint flags = MF_STRING | (item.IsEnabled ? 0 : MF_GRAYED);
|
||||
AppendMenu(menu, flags, new IntPtr(item.Id), item.Caption);
|
||||
}
|
||||
|
||||
TrayIconNative.BringToForeground(owner);
|
||||
|
||||
int chosen = TrackPopupMenuEx(
|
||||
menu, TPM_LEFTALIGN | TPM_RIGHTBUTTON | TPM_RETURNCMD,
|
||||
at.X, at.Y, owner, IntPtr.Zero);
|
||||
|
||||
WindowNative.PostMessage(owner, WindowNative.WM_NULL, IntPtr.Zero, IntPtr.Zero);
|
||||
|
||||
return chosen;
|
||||
}
|
||||
finally
|
||||
{
|
||||
DestroyMenu(menu);
|
||||
}
|
||||
}
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern IntPtr CreatePopupMenu();
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern bool DestroyMenu(IntPtr hMenu);
|
||||
|
||||
[DllImport("user32.dll", CharSet = CharSet.Unicode, EntryPoint = "AppendMenuW")]
|
||||
private static extern bool AppendMenu(IntPtr hMenu, uint uFlags, IntPtr uIDNewItem, string? lpNewItem);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern int TrackPopupMenuEx(IntPtr hMenu, uint uFlags, int x, int y,
|
||||
IntPtr hwnd, IntPtr lptpm);
|
||||
}
|
||||
@@ -0,0 +1,229 @@
|
||||
using System.Runtime.InteropServices;
|
||||
using CursorLang.Core.Interop;
|
||||
|
||||
namespace CursorLang.Agent.Interop;
|
||||
|
||||
/// <summary>
|
||||
/// Win32 API for the notification area: the icon itself, the messages it sends
|
||||
/// and the icon image taken from the executable.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The icon is asked for at version 4 of the protocol. It is the only version that
|
||||
/// reports a request for the context menu as such — by the keyboard as well as by
|
||||
/// the right button — and passes the point of the click along with it. The
|
||||
/// notification then arrives in the low word of <c>lParam</c>, and the point in
|
||||
/// <c>wParam</c>, which is the opposite of the earlier versions.
|
||||
/// </remarks>
|
||||
internal static class TrayIconNative
|
||||
{
|
||||
/// <summary>The message the icon sends to its window. WM_APP is free for the app.</summary>
|
||||
internal const int CallbackMessage = 0x8000 + 1;
|
||||
|
||||
/// <summary>The user chose the icon: a click of the left button or Enter on it.</summary>
|
||||
internal const int SelectNotification = 0x0400;
|
||||
|
||||
/// <summary>The same by the space bar — Windows tells the two apart.</summary>
|
||||
internal const int KeySelectNotification = 0x0403;
|
||||
|
||||
/// <summary>The context menu is asked for: the right button or the menu key.</summary>
|
||||
internal const int ContextMenuNotification = 0x007B;
|
||||
|
||||
private const int NIM_ADD = 0x00000000;
|
||||
private const int NIM_DELETE = 0x00000002;
|
||||
private const int NIM_SETVERSION = 0x00000004;
|
||||
|
||||
private const uint NIF_MESSAGE = 0x00000001;
|
||||
private const uint NIF_ICON = 0x00000002;
|
||||
private const uint NIF_TIP = 0x00000004;
|
||||
private const uint NIF_SHOWTIP = 0x00000080;
|
||||
|
||||
private const uint NotifyIconVersion4 = 4;
|
||||
|
||||
private const uint IMAGE_ICON = 1;
|
||||
private const uint LR_DEFAULTCOLOR = 0x00000000;
|
||||
|
||||
private const int SM_CXSMICON = 49;
|
||||
private const int SM_CYSMICON = 50;
|
||||
|
||||
/// <summary>The resource the .NET build puts the application icon under.</summary>
|
||||
private const int ApplicationIconResource = 32512;
|
||||
|
||||
/// <summary>
|
||||
/// Explorer says it has restarted this way. The icons of every application are
|
||||
/// gone by then and have to be put back.
|
||||
/// </summary>
|
||||
internal static int TaskbarCreatedMessage { get; } = RegisterWindowMessage("TaskbarCreated");
|
||||
|
||||
/// <summary>Puts the icon into the notification area.</summary>
|
||||
internal static bool Add(IntPtr window, int id, IntPtr icon, string tooltip)
|
||||
{
|
||||
NotifyIconData data = Describe(window, id, icon, tooltip);
|
||||
|
||||
if (!Shell_NotifyIcon(NIM_ADD, ref data))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// The version is asked for after the icon is added and applies to it alone
|
||||
data.uVersion = NotifyIconVersion4;
|
||||
Shell_NotifyIcon(NIM_SETVERSION, ref data);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>Takes the icon away. A forgotten icon stays in the tray until hovered.</summary>
|
||||
internal static void Remove(IntPtr window, int id)
|
||||
{
|
||||
var data = new NotifyIconData
|
||||
{
|
||||
cbSize = Marshal.SizeOf<NotifyIconData>(),
|
||||
hWnd = window,
|
||||
uID = (uint)id,
|
||||
};
|
||||
|
||||
Shell_NotifyIcon(NIM_DELETE, ref data);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The icon of the application at the size the tray asks for.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The image comes from the executable itself, so the tray shows what the user
|
||||
/// sees in Explorer. The build puts the icon under the standard resource; should
|
||||
/// it end up elsewhere, the first icon of the file is taken, and failing that —
|
||||
/// the icon Windows gives to an application without one. An icon is needed
|
||||
/// either way: without it the tray shows an empty spot.
|
||||
/// </remarks>
|
||||
internal static IntPtr LoadApplicationIcon()
|
||||
{
|
||||
int width = GetSystemMetrics(SM_CXSMICON);
|
||||
int height = GetSystemMetrics(SM_CYSMICON);
|
||||
|
||||
IntPtr icon = LoadImage(
|
||||
GetModuleHandle(null), ApplicationIconResource, IMAGE_ICON, width, height, LR_DEFAULTCOLOR);
|
||||
|
||||
if (icon == IntPtr.Zero && Environment.ProcessPath is { Length: > 0 } path)
|
||||
{
|
||||
icon = ExtractIconEx(path, 0, out IntPtr large, out IntPtr small, 1) > 0 ? small : IntPtr.Zero;
|
||||
|
||||
if (large != IntPtr.Zero)
|
||||
{
|
||||
DestroyIcon(large);
|
||||
}
|
||||
}
|
||||
|
||||
return icon != IntPtr.Zero ? icon : LoadIcon(IntPtr.Zero, ApplicationIconResource);
|
||||
}
|
||||
|
||||
/// <summary>Releases an icon loaded by <see cref="LoadApplicationIcon"/>.</summary>
|
||||
internal static void ReleaseIcon(IntPtr icon)
|
||||
{
|
||||
if (icon != IntPtr.Zero)
|
||||
{
|
||||
DestroyIcon(icon);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>The notification the icon has sent: it sits in the low word of lParam.</summary>
|
||||
internal static int NotificationOf(IntPtr lParam) => (int)(lParam.ToInt64() & 0xFFFF);
|
||||
|
||||
/// <summary>
|
||||
/// The point of the click. Version 4 of the protocol reports it in screen pixels
|
||||
/// in <c>wParam</c> — exactly what <c>TrackPopupMenuEx</c> expects.
|
||||
/// </summary>
|
||||
internal static PopupWindowNative.Point PointOf(IntPtr wParam) => new()
|
||||
{
|
||||
X = (short)(wParam.ToInt64() & 0xFFFF),
|
||||
Y = (short)((wParam.ToInt64() >> 16) & 0xFFFF),
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Brings the window to the foreground.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Windows takes a menu down when its owner window loses the foreground. A tray
|
||||
/// icon belongs to a window that is never shown, so the foreground has to be
|
||||
/// asked for by hand — otherwise the menu stays on screen after the user has
|
||||
/// clicked past it.
|
||||
/// </remarks>
|
||||
internal static void BringToForeground(IntPtr window) => SetForegroundWindow(window);
|
||||
|
||||
private static NotifyIconData Describe(IntPtr window, int id, IntPtr icon, string tooltip) => new()
|
||||
{
|
||||
cbSize = Marshal.SizeOf<NotifyIconData>(),
|
||||
hWnd = window,
|
||||
uID = (uint)id,
|
||||
uFlags = NIF_MESSAGE | NIF_ICON | NIF_TIP | NIF_SHOWTIP,
|
||||
uCallbackMessage = CallbackMessage,
|
||||
hIcon = icon,
|
||||
szTip = tooltip,
|
||||
};
|
||||
|
||||
[DllImport("shell32.dll", CharSet = CharSet.Unicode)]
|
||||
private static extern bool Shell_NotifyIcon(int dwMessage, ref NotifyIconData lpData);
|
||||
|
||||
[DllImport("user32.dll", CharSet = CharSet.Unicode)]
|
||||
private static extern int RegisterWindowMessage(string lpString);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern IntPtr LoadImage(IntPtr hInst, IntPtr name, uint type, int cx, int cy, uint fuLoad);
|
||||
|
||||
private static IntPtr LoadImage(IntPtr hInst, int resource, uint type, int cx, int cy, uint fuLoad) =>
|
||||
LoadImage(hInst, new IntPtr(resource), type, cx, cy, fuLoad);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern IntPtr LoadIcon(IntPtr hInstance, IntPtr lpIconName);
|
||||
|
||||
private static IntPtr LoadIcon(IntPtr hInstance, int resource) =>
|
||||
LoadIcon(hInstance, new IntPtr(resource));
|
||||
|
||||
[DllImport("shell32.dll", CharSet = CharSet.Unicode)]
|
||||
private static extern int ExtractIconEx(string lpszFile, int nIconIndex,
|
||||
out IntPtr phiconLarge, out IntPtr phiconSmall, int nIcons);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern bool DestroyIcon(IntPtr hIcon);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern int GetSystemMetrics(int nIndex);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern bool SetForegroundWindow(IntPtr hWnd);
|
||||
|
||||
[DllImport("kernel32.dll", CharSet = CharSet.Unicode)]
|
||||
private static extern IntPtr GetModuleHandle(string? lpModuleName);
|
||||
|
||||
/// <summary>
|
||||
/// NOTIFYICONDATAW. The whole structure is described even though only its first
|
||||
/// half is used: Windows reads its size and refuses one it does not know.
|
||||
/// </summary>
|
||||
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
|
||||
private struct NotifyIconData
|
||||
{
|
||||
public int cbSize;
|
||||
public IntPtr hWnd;
|
||||
public uint uID;
|
||||
public uint uFlags;
|
||||
public int uCallbackMessage;
|
||||
public IntPtr hIcon;
|
||||
|
||||
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)]
|
||||
public string szTip;
|
||||
|
||||
public uint dwState;
|
||||
public uint dwStateMask;
|
||||
|
||||
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 256)]
|
||||
public string szInfo;
|
||||
|
||||
/// <summary>A timeout in the older versions and the protocol version here.</summary>
|
||||
public uint uVersion;
|
||||
|
||||
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 64)]
|
||||
public string szInfoTitle;
|
||||
|
||||
public uint dwInfoFlags;
|
||||
public Guid guidItem;
|
||||
public IntPtr hBalloonIcon;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
using System.Runtime.InteropServices;
|
||||
using CursorLang.Core.Interop;
|
||||
|
||||
namespace CursorLang.Agent.Interop;
|
||||
|
||||
/// <summary>
|
||||
/// The Win32 pieces a window needs when there is no framework to make one:
|
||||
/// the class, the window itself, the message loop.
|
||||
/// </summary>
|
||||
internal static class WindowNative
|
||||
{
|
||||
/// <summary>The window procedure. Windows keeps the only reference to it.</summary>
|
||||
internal delegate IntPtr WindowProc(IntPtr hWnd, uint message, IntPtr wParam, IntPtr lParam);
|
||||
|
||||
internal const int WS_POPUP = unchecked((int)0x80000000);
|
||||
|
||||
internal const int WS_EX_LAYERED = 0x00080000;
|
||||
internal const int WS_EX_TOOLWINDOW = 0x00000080;
|
||||
internal const int WS_EX_NOACTIVATE = 0x08000000;
|
||||
internal const int WS_EX_TRANSPARENT = 0x00000020;
|
||||
internal const int WS_EX_TOPMOST = 0x00000008;
|
||||
|
||||
internal const int SW_HIDE = 0;
|
||||
internal const int SW_SHOWNOACTIVATE = 4;
|
||||
|
||||
internal const uint WM_DESTROY = 0x0002;
|
||||
internal const uint WM_CLOSE = 0x0010;
|
||||
internal const uint WM_QUIT = 0x0012;
|
||||
internal const uint WM_NULL = 0x0000;
|
||||
internal const uint WM_ENDSESSION = 0x0016;
|
||||
|
||||
/// <summary>WM_APP and up belong to the application; the tray takes WM_APP + 1.</summary>
|
||||
internal const uint WM_APP = 0x8000;
|
||||
|
||||
/// <summary>
|
||||
/// Registers a window class. A class already there is not an error: the name is
|
||||
/// unique per window kind, and a second agent in the same process would meet its
|
||||
/// own registration.
|
||||
/// </summary>
|
||||
internal static void RegisterClass(string className, WindowProc windowProc)
|
||||
{
|
||||
var description = new WindowClass
|
||||
{
|
||||
cbSize = Marshal.SizeOf<WindowClass>(),
|
||||
lpfnWndProc = windowProc,
|
||||
hInstance = GetModuleHandle(null),
|
||||
lpszClassName = className,
|
||||
};
|
||||
|
||||
if (RegisterClassEx(ref description) == 0 &&
|
||||
Marshal.GetLastWin32Error() != ErrorClassAlreadyExists)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"RegisterClassExW failed for '{className}': {Marshal.GetLastWin32Error()}");
|
||||
}
|
||||
}
|
||||
|
||||
private const int ErrorClassAlreadyExists = 1410;
|
||||
|
||||
/// <summary>Creates a window of a registered class. It is not shown.</summary>
|
||||
internal static IntPtr CreateWindow(string className, string title, int style, int exStyle)
|
||||
{
|
||||
IntPtr window = CreateWindowEx(
|
||||
exStyle, className, title, style,
|
||||
0, 0, 0, 0,
|
||||
IntPtr.Zero, IntPtr.Zero, GetModuleHandle(null), IntPtr.Zero);
|
||||
|
||||
if (window == IntPtr.Zero)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"CreateWindowExW failed for '{className}': {Marshal.GetLastWin32Error()}");
|
||||
}
|
||||
|
||||
return window;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Puts a finished picture into a layered window, together with where it goes and
|
||||
/// how see-through it is.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// One call replaces moving the window, resizing it, painting it and setting its
|
||||
/// opacity, and it works while the window is still hidden. That is the point: the
|
||||
/// content is ready before anyone can see the window, so it can never be shown
|
||||
/// holding the picture of the previous time.
|
||||
/// </remarks>
|
||||
internal static bool SetContent(
|
||||
IntPtr window, PopupWindowNative.Point at, Size size, IntPtr sourceDc, byte alpha)
|
||||
{
|
||||
var source = new PopupWindowNative.Point { X = 0, Y = 0 };
|
||||
var blend = new BlendFunction
|
||||
{
|
||||
BlendOp = AC_SRC_OVER,
|
||||
SourceConstantAlpha = alpha,
|
||||
AlphaFormat = AC_SRC_ALPHA,
|
||||
};
|
||||
|
||||
return UpdateLayeredWindow(
|
||||
window, IntPtr.Zero, ref at, ref size, sourceDc, ref source, 0, ref blend, ULW_ALPHA);
|
||||
}
|
||||
|
||||
private const byte AC_SRC_OVER = 0;
|
||||
private const byte AC_SRC_ALPHA = 1;
|
||||
private const uint ULW_ALPHA = 0x00000002;
|
||||
|
||||
/// <summary>
|
||||
/// BLENDFUNCTION. <c>BlendFlags</c> is never assigned and must stay all the same:
|
||||
/// Windows reads the structure by its layout, and dropping a byte from the middle
|
||||
/// of it would shift everything after.
|
||||
/// </summary>
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
private struct BlendFunction
|
||||
{
|
||||
public byte BlendOp;
|
||||
public byte BlendFlags;
|
||||
public byte SourceConstantAlpha;
|
||||
public byte AlphaFormat;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
internal struct Size
|
||||
{
|
||||
public int Width;
|
||||
public int Height;
|
||||
}
|
||||
|
||||
[DllImport("user32.dll", SetLastError = true)]
|
||||
private static extern bool UpdateLayeredWindow(IntPtr hWnd, IntPtr hdcDst,
|
||||
ref PopupWindowNative.Point pptDst, ref Size psize, IntPtr hdcSrc,
|
||||
ref PopupWindowNative.Point pptSrc, uint crKey, ref BlendFunction pblend, uint dwFlags);
|
||||
|
||||
[DllImport("user32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
|
||||
private static extern ushort RegisterClassEx(ref WindowClass lpwcx);
|
||||
|
||||
[DllImport("user32.dll", CharSet = CharSet.Unicode, SetLastError = true, EntryPoint = "CreateWindowExW")]
|
||||
private static extern IntPtr CreateWindowEx(int dwExStyle, string lpClassName, string lpWindowName,
|
||||
int dwStyle, int x, int y, int nWidth, int nHeight,
|
||||
IntPtr hWndParent, IntPtr hMenu, IntPtr hInstance, IntPtr lpParam);
|
||||
|
||||
[DllImport("user32.dll", CharSet = CharSet.Unicode, EntryPoint = "DefWindowProcW")]
|
||||
internal static extern IntPtr DefWindowProc(IntPtr hWnd, uint message, IntPtr wParam, IntPtr lParam);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
internal static extern bool DestroyWindow(IntPtr hWnd);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
internal static extern bool ShowWindow(IntPtr hWnd, int nCmdShow);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
internal static extern bool IsWindowVisible(IntPtr hWnd);
|
||||
|
||||
[DllImport("user32.dll", CharSet = CharSet.Unicode, EntryPoint = "PostMessageW")]
|
||||
internal static extern bool PostMessage(IntPtr hWnd, uint message, IntPtr wParam, IntPtr lParam);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
internal static extern void PostQuitMessage(int nExitCode);
|
||||
|
||||
[DllImport("kernel32.dll", CharSet = CharSet.Unicode)]
|
||||
private static extern IntPtr GetModuleHandle(string? lpModuleName);
|
||||
|
||||
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
|
||||
private struct WindowClass
|
||||
{
|
||||
public int cbSize;
|
||||
public uint style;
|
||||
|
||||
[MarshalAs(UnmanagedType.FunctionPtr)]
|
||||
public WindowProc lpfnWndProc;
|
||||
|
||||
public int cbClsExtra;
|
||||
public int cbWndExtra;
|
||||
public IntPtr hInstance;
|
||||
public IntPtr hIcon;
|
||||
public IntPtr hCursor;
|
||||
public IntPtr hbrBackground;
|
||||
public string? lpszMenuName;
|
||||
public string lpszClassName;
|
||||
public IntPtr hIconSm;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user