feat: 点击 toast 激活原窗口(P1a)
- WindowActivator:ALT 模拟 + AttachThreadInput + SetForegroundWindow 组合技抢前台 - save 抓的前台句柄经 PipeMessage/ToastRequest 一路带到 toast - 点击 toast 主体激活目标窗口后再关闭 - Win32 互操作用 LibraryImport(AOT 友好) - 新增 acttest 自测子命令
This commit is contained in:
@@ -58,6 +58,7 @@ public partial class App : Application
|
||||
Message = message.Message,
|
||||
InputMode = message.InputMode,
|
||||
Sticky = message.Sticky,
|
||||
TargetHwnd = message.TargetHwnd,
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
@@ -52,6 +52,7 @@ public static class CliRunner
|
||||
Message = message,
|
||||
InputMode = false,
|
||||
Sticky = false,
|
||||
TargetHwnd = state?.Hwnd ?? 0,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -71,6 +72,7 @@ public static class CliRunner
|
||||
}
|
||||
|
||||
var (title, message) = Resolve(input);
|
||||
var state = StateStore.Load(input.SessionId);
|
||||
|
||||
return SendToHost(new PipeMessage
|
||||
{
|
||||
@@ -79,9 +81,36 @@ public static class CliRunner
|
||||
Message = message,
|
||||
InputMode = true,
|
||||
Sticky = true,
|
||||
TargetHwnd = state?.Hwnd ?? 0,
|
||||
});
|
||||
}
|
||||
|
||||
// 仅用于自测:acttest <hwnd>,尝试激活指定窗口并把结果写入临时日志
|
||||
public static int ActTest(string[] args)
|
||||
{
|
||||
if (args.Length < 2 || !long.TryParse(args[1], out var h))
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
|
||||
var before = Win32.GetForegroundWindow().ToInt64();
|
||||
var ok = WindowActivator.Activate(new IntPtr(h));
|
||||
var after = Win32.GetForegroundWindow().ToInt64();
|
||||
|
||||
try
|
||||
{
|
||||
System.IO.File.WriteAllText(
|
||||
System.IO.Path.Combine(System.IO.Path.GetTempPath(), "notify-acttest.log"),
|
||||
$"target={h} before={before} after={after} ok={ok}\n");
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 忽略
|
||||
}
|
||||
|
||||
return ok ? 0 : 1;
|
||||
}
|
||||
|
||||
// SessionEnd:清理会话状态
|
||||
public static int Cleanup()
|
||||
{
|
||||
|
||||
@@ -7,4 +7,58 @@ internal static partial class Win32
|
||||
{
|
||||
[LibraryImport("user32.dll")]
|
||||
internal static partial IntPtr GetForegroundWindow();
|
||||
|
||||
[LibraryImport("user32.dll")]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
internal static partial bool SetForegroundWindow(IntPtr hWnd);
|
||||
|
||||
[LibraryImport("user32.dll")]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
internal static partial bool AllowSetForegroundWindow(uint dwProcessId);
|
||||
|
||||
[LibraryImport("user32.dll")]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
internal static partial bool AttachThreadInput(uint idAttach, uint idAttachTo, [MarshalAs(UnmanagedType.Bool)] bool fAttach);
|
||||
|
||||
[LibraryImport("user32.dll")]
|
||||
internal static partial uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId);
|
||||
|
||||
[LibraryImport("user32.dll")]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
internal static partial bool BringWindowToTop(IntPtr hWnd);
|
||||
|
||||
[LibraryImport("user32.dll")]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
internal static partial bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int x, int y, int cx, int cy, uint uFlags);
|
||||
|
||||
[LibraryImport("user32.dll")]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
internal static partial bool ShowWindow(IntPtr hWnd, int nCmdShow);
|
||||
|
||||
[LibraryImport("user32.dll")]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
internal static partial bool IsIconic(IntPtr hWnd);
|
||||
|
||||
[LibraryImport("user32.dll")]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
internal static partial bool IsWindow(IntPtr hWnd);
|
||||
|
||||
[LibraryImport("user32.dll")]
|
||||
internal static partial void SwitchToThisWindow(IntPtr hWnd, [MarshalAs(UnmanagedType.Bool)] bool fAltTab);
|
||||
|
||||
[LibraryImport("user32.dll")]
|
||||
internal static partial void keybd_event(byte bVk, byte bScan, uint dwFlags, IntPtr dwExtraInfo);
|
||||
|
||||
[LibraryImport("kernel32.dll")]
|
||||
internal static partial uint GetCurrentThreadId();
|
||||
|
||||
// --- 常量 ---
|
||||
internal const uint ASFW_ANY = 0xFFFFFFFF;
|
||||
internal const int SW_RESTORE = 9;
|
||||
internal const int SW_SHOW = 5;
|
||||
internal const uint SWP_NOSIZE = 0x0001;
|
||||
internal const uint SWP_NOMOVE = 0x0002;
|
||||
internal const uint SWP_SHOWWINDOW = 0x0040;
|
||||
internal const byte VK_MENU = 0x12;
|
||||
internal const uint KEYEVENTF_KEYUP = 0x0002;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
using System;
|
||||
|
||||
namespace Notify.Interop;
|
||||
|
||||
/// <summary>
|
||||
/// 把目标窗口拉回前台
|
||||
///
|
||||
/// Windows 限制后台进程抢焦点,这里用一套组合技绕过:ALT 键模拟 +
|
||||
/// AttachThreadInput 把当前线程与前台/目标线程的输入队列挂接 +
|
||||
/// SetWindowPos/BringWindowToTop/SwitchToThisWindow/SetForegroundWindow 多管齐下
|
||||
/// </summary>
|
||||
public static class WindowActivator
|
||||
{
|
||||
public static bool Activate(IntPtr hwnd)
|
||||
{
|
||||
if (hwnd == IntPtr.Zero || !Win32.IsWindow(hwnd))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// 最小化的先还原
|
||||
if (Win32.IsIconic(hwnd))
|
||||
{
|
||||
Win32.ShowWindow(hwnd, Win32.SW_RESTORE);
|
||||
}
|
||||
|
||||
var foreground = Win32.GetForegroundWindow();
|
||||
var curThread = Win32.GetCurrentThreadId();
|
||||
var fgThread = Win32.GetWindowThreadProcessId(foreground, out _);
|
||||
var targetThread = Win32.GetWindowThreadProcessId(hwnd, out _);
|
||||
|
||||
// 模拟一次 ALT 抬起,满足 Windows 的"防焦点抢占"前置条件
|
||||
Win32.keybd_event(Win32.VK_MENU, 0, 0, IntPtr.Zero);
|
||||
Win32.keybd_event(Win32.VK_MENU, 0, Win32.KEYEVENTF_KEYUP, IntPtr.Zero);
|
||||
|
||||
if (fgThread != curThread)
|
||||
{
|
||||
Win32.AttachThreadInput(curThread, fgThread, true);
|
||||
}
|
||||
|
||||
if (targetThread != curThread && targetThread != fgThread)
|
||||
{
|
||||
Win32.AttachThreadInput(curThread, targetThread, true);
|
||||
}
|
||||
|
||||
Win32.AllowSetForegroundWindow(Win32.ASFW_ANY);
|
||||
Win32.SetWindowPos(hwnd, IntPtr.Zero, 0, 0, 0, 0, Win32.SWP_NOMOVE | Win32.SWP_NOSIZE | Win32.SWP_SHOWWINDOW);
|
||||
Win32.BringWindowToTop(hwnd);
|
||||
Win32.SwitchToThisWindow(hwnd, true);
|
||||
Win32.SetForegroundWindow(hwnd);
|
||||
Win32.ShowWindow(hwnd, Win32.SW_SHOW);
|
||||
|
||||
if (targetThread != curThread && targetThread != fgThread)
|
||||
{
|
||||
Win32.AttachThreadInput(curThread, targetThread, false);
|
||||
}
|
||||
|
||||
if (fgThread != curThread)
|
||||
{
|
||||
Win32.AttachThreadInput(curThread, fgThread, false);
|
||||
}
|
||||
|
||||
return Win32.GetForegroundWindow() == hwnd;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,4 +17,7 @@ public sealed class PipeMessage
|
||||
|
||||
// 触发该通知的会话 id,便于 Host 后续按会话激活窗口
|
||||
public string? SessionId { get; set; }
|
||||
|
||||
// 点击 toast 时要激活的目标窗口句柄,0 表示无
|
||||
public long TargetHwnd { get; set; }
|
||||
}
|
||||
|
||||
@@ -18,4 +18,9 @@ public sealed class ToastRequest
|
||||
/// true = 常驻:不自动消失,只能点击 / ✕ 关闭
|
||||
/// </summary>
|
||||
public bool Sticky { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 点击 toast 主体时要激活的窗口句柄,0 表示不激活
|
||||
/// </summary>
|
||||
public long TargetHwnd { get; init; }
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ internal static class Program
|
||||
"notify" => CliRunner.Notify(),
|
||||
"input" => CliRunner.Input(),
|
||||
"cleanup" => CliRunner.Cleanup(),
|
||||
"acttest" => CliRunner.ActTest(args),
|
||||
_ => RunHost(args),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ public sealed class ToastManager
|
||||
}
|
||||
|
||||
var vm = new ToastViewModel(request);
|
||||
var window = new ToastWindow(vm, settings, request.Sticky);
|
||||
var window = new ToastWindow(vm, settings, request.Sticky, request.TargetHwnd);
|
||||
window.Closed += OnToastClosed;
|
||||
|
||||
_active.Add(window);
|
||||
|
||||
@@ -20,16 +20,18 @@ public partial class ToastWindow : Window
|
||||
private readonly ToastSettings _settings;
|
||||
private readonly DispatcherTimer _dismissTimer;
|
||||
private readonly bool _sticky;
|
||||
private readonly long _targetHwnd;
|
||||
private bool _closing;
|
||||
|
||||
// 设计器需要的无参构造
|
||||
public ToastWindow() : this(new ToastViewModel(new ToastRequest { Title = "Title", Message = "Message" }), new ToastSettings(), false)
|
||||
public ToastWindow() : this(new ToastViewModel(new ToastRequest { Title = "Title", Message = "Message" }), new ToastSettings(), false, 0)
|
||||
{
|
||||
}
|
||||
|
||||
public ToastWindow(ToastViewModel vm, ToastSettings settings, bool sticky)
|
||||
public ToastWindow(ToastViewModel vm, ToastSettings settings, bool sticky, long targetHwnd)
|
||||
{
|
||||
_settings = settings;
|
||||
_targetHwnd = targetHwnd;
|
||||
// 常驻:请求显式 Sticky,或全局停留时长 <= 0
|
||||
_sticky = sticky || settings.DurationSeconds <= 0;
|
||||
InitializeComponent();
|
||||
@@ -119,9 +121,14 @@ public partial class ToastWindow : Window
|
||||
|
||||
private void OnBodyPressed(object? sender, PointerPressedEventArgs e)
|
||||
{
|
||||
// 左键点击主体:后续接入"激活原窗口 + 切回标签页",当前先关闭
|
||||
// 左键点击主体:激活原窗口后关闭
|
||||
if (e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
|
||||
{
|
||||
if (_targetHwnd != 0)
|
||||
{
|
||||
Notify.Interop.WindowActivator.Activate(new IntPtr(_targetHwnd));
|
||||
}
|
||||
|
||||
BeginClose();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user