监控、OSD

This commit is contained in:
zhongluofeng
2026-07-28 18:31:39 +08:00
parent 7e6149355e
commit 3d3096b2a3
11 changed files with 3475 additions and 107 deletions
+12
View File
@@ -6,6 +6,7 @@ mod logger;
mod mihomo_manager;
mod monitor_kernel;
mod network_monitor;
mod osd_window;
mod process_manager;
use download_engine::{
@@ -31,6 +32,10 @@ use monitor_kernel::{
monitor_stop, MonitorKernel,
};
use network_monitor::{network_monitor_status, NetworkMonitor};
use osd_window::{
osd_apply_overlay_style, osd_begin_drag, osd_set_click_through, osd_set_topmost,
osd_start_drag_watch, osd_start_topmost_watch, osd_stop_watch,
};
use process_manager::{
get_all_process_status, get_process_status, start_monitoring_thread, start_process,
stop_all_processes, stop_process, ProcessManager,
@@ -122,6 +127,13 @@ pub fn run() {
monitor_get_hardware_config,
monitor_set_hardware_config,
network_monitor_status,
osd_apply_overlay_style,
osd_begin_drag,
osd_set_click_through,
osd_set_topmost,
osd_start_drag_watch,
osd_start_topmost_watch,
osd_stop_watch,
downloader_get_tasks,
downloader_add_task,
downloader_pause_task,
+366
View File
@@ -0,0 +1,366 @@
//! OSD 悬浮窗 Windows 原生窗口管理
//!
//! 提供:
//! - NoActivate(不获取焦点):设置 WS_EX_NOACTIVATE 扩展样式
//! - 右键长按拖动:轮询 GetAsyncKeyState 检测右键长按,触发拖动事件
//! - 任务栏覆盖检测:轮询 GetForegroundWindow,检测系统 UI 出现时暂时取消置顶
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, OnceLock};
use std::thread;
use std::time::{Duration, Instant};
use tauri::{AppHandle, Emitter};
/// 右键拖动监视线程停止标志(全局,仅一个 OSD 悬浮窗)
static DRAG_STOP: OnceLock<Arc<AtomicBool>> = OnceLock::new();
/// 任务栏覆盖监视线程停止标志
static TOPMOST_STOP: OnceLock<Arc<AtomicBool>> = OnceLock::new();
fn drag_stop() -> &'static Arc<AtomicBool> {
DRAG_STOP.get_or_init(|| Arc::new(AtomicBool::new(true)))
}
fn topmost_stop() -> &'static Arc<AtomicBool> {
TOPMOST_STOP.get_or_init(|| Arc::new(AtomicBool::new(true)))
}
#[cfg(windows)]
mod win_api {
use tauri::{AppHandle, Manager};
use windows_sys::Win32::Foundation::{POINT, RECT};
use windows_sys::Win32::UI::Input::KeyboardAndMouse::{GetAsyncKeyState, VK_RBUTTON};
use windows_sys::Win32::UI::WindowsAndMessaging::{
GetClassNameW, GetCursorPos, GetForegroundWindow, GetWindowLongPtrW,
GetWindowRect, SendMessageW, SetWindowLongPtrW, SetWindowPos,
GWL_EXSTYLE, HTCAPTION, HWND_NOTOPMOST, HWND_TOPMOST, SWP_NOACTIVATE, SWP_NOMOVE,
SWP_NOSIZE, SWP_SHOWWINDOW, WM_NCLBUTTONDOWN, WS_EX_NOACTIVATE, WS_EX_TOOLWINDOW,
WS_EX_TRANSPARENT,
};
/// windows-sys 的 HWND 类型别名(isize
pub type Hwnd = isize;
/// 从 Tauri 窗口标签获取原生 HWND
pub fn get_hwnd(label: &str, app: &AppHandle) -> Option<Hwnd> {
let win = app.get_webview_window(label)?;
let hwnd = win.hwnd().ok()?;
// Tauri 的 hwnd() 返回 windows::Win32::Foundation::HWND(pub *mut c_void)
// 转为 isize 用于 windows-sys 调用
Some(hwnd.0 as isize)
}
/// 设置窗口扩展样式:WS_EX_NOACTIVATE | WS_EX_TOOLWINDOW
pub fn apply_no_activate(hwnd: Hwnd) {
unsafe {
let ex = GetWindowLongPtrW(hwnd, GWL_EXSTYLE);
let new_ex = ex | (WS_EX_NOACTIVATE as isize) | (WS_EX_TOOLWINDOW as isize);
SetWindowLongPtrW(hwnd, GWL_EXSTYLE, new_ex);
}
}
/// 设置点击穿透(WS_EX_TRANSPARENT
pub fn set_click_through(hwnd: Hwnd, enabled: bool) {
unsafe {
let ex = GetWindowLongPtrW(hwnd, GWL_EXSTYLE);
let new_ex = if enabled {
ex | (WS_EX_TRANSPARENT as isize)
} else {
ex & !(WS_EX_TRANSPARENT as isize)
};
SetWindowLongPtrW(hwnd, GWL_EXSTYLE, new_ex);
}
}
/// 获取鼠标光标位置
pub fn get_cursor_pos() -> Option<POINT> {
let mut pt = POINT { x: 0, y: 0 };
unsafe {
if GetCursorPos(&mut pt) != 0 {
Some(pt)
} else {
None
}
}
}
/// 获取窗口矩形
pub fn get_window_rect(hwnd: Hwnd) -> Option<RECT> {
let mut rect = RECT {
left: 0,
top: 0,
right: 0,
bottom: 0,
};
unsafe {
if GetWindowRect(hwnd, &mut rect) != 0 {
Some(rect)
} else {
None
}
}
}
/// 判断点是否在窗口矩形内
pub fn point_in_rect(pt: POINT, rect: RECT) -> bool {
pt.x >= rect.left && pt.x <= rect.right && pt.y >= rect.top && pt.y <= rect.bottom
}
/// 判断右键是否按下
pub fn is_rbutton_down() -> bool {
// GetAsyncKeyState 返回 i16,最高位为 1 表示按下
// 转 u16 后与 0x8000 进行按位与,避免 i16 字面量溢出
unsafe { (GetAsyncKeyState(VK_RBUTTON as i32) as u16 & 0x8000) != 0 }
}
/// 获取前景窗口
pub fn get_foreground_window() -> Hwnd {
unsafe { GetForegroundWindow() }
}
/// 获取窗口类名
pub fn get_class_name(hwnd: Hwnd) -> Option<String> {
let mut buf = [0u16; 256];
unsafe {
let len = GetClassNameW(hwnd, buf.as_mut_ptr(), buf.len() as i32);
if len > 0 {
Some(String::from_utf16_lossy(&buf[..len as usize]))
} else {
None
}
}
}
/// 设置窗口置顶状态
pub fn set_topmost(hwnd: Hwnd, topmost: bool) {
unsafe {
let insert_after = if topmost {
HWND_TOPMOST
} else {
HWND_NOTOPMOST
};
SetWindowPos(
hwnd,
insert_after,
0,
0,
0,
0,
SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE | SWP_SHOWWINDOW,
);
}
}
/// 启动原生拖动:发送 WM_NCLBUTTONDOWN(HTCAPTION) 进入标题栏拖动
///
/// 调用前应已关闭 WS_EX_TRANSPARENT(点击穿透),否则窗口无法接收后续鼠标事件。
/// SendMessageW 会阻塞直到拖动结束,应在独立线程调用。
pub fn begin_drag(hwnd: Hwnd) {
unsafe {
// 获取当前鼠标位置,编码为 LPARAM(低字 x,高字 y)
let pt = get_cursor_pos().unwrap_or(POINT { x: 0, y: 0 });
let lparam = ((pt.y as isize) << 16) | (pt.x as isize & 0xFFFF);
SendMessageW(hwnd, WM_NCLBUTTONDOWN, HTCAPTION as usize, lparam);
}
}
/// 判断窗口类名是否为系统 UI(任务栏、开始菜单、通知区域等)
pub fn is_system_ui_class(class_name: &str) -> bool {
matches!(
class_name,
"Shell_TrayWnd" // 任务栏
| "Shell_SecondaryTrayWnd" // 副任务栏
| "Windows.UI.Core.CoreWindow" // UWP 系统弹窗(开始菜单、通知中心、日历等)
| "XamlExplorerHostIslandWindow" // Win11 系统弹窗容器
| "TopLevelWindowForOverflowSink" // Win11 系统托盘溢出
| "TopLevelWindowForHiddenRegion" // Win11 系统隐藏区域
| "Windows.UI.Shell.ShellFlyoutWindow" // Win11 Shell 弹出
)
}
}
/// 应用 OSD 悬浮窗的原生样式(NoActivate + ToolWindow
#[tauri::command]
pub fn osd_apply_overlay_style(label: String, app: AppHandle) -> Result<(), String> {
#[cfg(windows)]
{
let hwnd = win_api::get_hwnd(&label, &app)
.ok_or_else(|| format!("窗口 {} 不存在", label))?;
win_api::apply_no_activate(hwnd);
eprintln!("[osd] 已应用 NoActivate 样式到窗口 {}", label);
}
Ok(())
}
/// 启动右键长按拖动监视
///
/// 轮询检测右键按下,持续超过 400ms 且鼠标在 OSD 窗口上方时,
/// 发出 `osd-start-drag` 事件通知前端临时关闭点击穿透并开始拖动。
/// 右键释放后发出 `osd-end-drag` 事件。
#[tauri::command]
pub fn osd_start_drag_watch(label: String, app: AppHandle) -> Result<(), String> {
// 停止旧线程,等待退出
drag_stop().store(true, Ordering::SeqCst);
thread::sleep(Duration::from_millis(50));
let stop_flag = drag_stop().clone();
stop_flag.store(false, Ordering::SeqCst);
let app_handle = app.clone();
thread::spawn(move || {
let mut rbutton_was_down = false;
let mut press_start: Option<Instant> = None;
let mut drag_emitted = false;
loop {
if stop_flag.load(Ordering::SeqCst) {
break;
}
#[cfg(windows)]
{
let rbutton_down = win_api::is_rbutton_down();
if rbutton_down && !rbutton_was_down {
// 右键刚按下
press_start = Some(Instant::now());
drag_emitted = false;
} else if rbutton_down && rbutton_was_down {
// 右键持续按住,检测长按
if !drag_emitted {
if let Some(start) = press_start {
if start.elapsed() >= Duration::from_millis(400) {
if let Some(hwnd) = win_api::get_hwnd(&label, &app_handle) {
if let (Some(pt), Some(rect)) =
(win_api::get_cursor_pos(), win_api::get_window_rect(hwnd))
{
if win_api::point_in_rect(pt, rect) {
let _ = app_handle.emit("osd-start-drag", ());
drag_emitted = true;
}
}
}
}
}
}
} else if !rbutton_down && rbutton_was_down {
// 右键释放
if drag_emitted {
let _ = app_handle.emit("osd-end-drag", ());
}
press_start = None;
drag_emitted = false;
}
rbutton_was_down = rbutton_down;
}
thread::sleep(Duration::from_millis(30));
}
});
Ok(())
}
/// 启动任务栏覆盖监视
///
/// 轮询检测前景窗口变化,当系统 UI(任务栏弹窗、开始菜单等)变为前景时,
/// 发出 `osd-system-ui-active` 事件通知前端暂时取消置顶;
/// 系统 UI 关闭后发出 `osd-system-ui-inactive` 事件恢复置顶。
#[tauri::command]
pub fn osd_start_topmost_watch(app: AppHandle) -> Result<(), String> {
topmost_stop().store(true, Ordering::SeqCst);
thread::sleep(Duration::from_millis(50));
let stop_flag = topmost_stop().clone();
stop_flag.store(false, Ordering::SeqCst);
let app_handle = app.clone();
thread::spawn(move || {
let mut last_foreground: isize = 0;
let mut system_ui_active = false;
loop {
if stop_flag.load(Ordering::SeqCst) {
break;
}
#[cfg(windows)]
{
let fg = win_api::get_foreground_window();
if fg != last_foreground {
last_foreground = fg;
if let Some(class) = win_api::get_class_name(fg) {
if win_api::is_system_ui_class(&class) {
if !system_ui_active {
system_ui_active = true;
let _ = app_handle.emit("osd-system-ui-active", ());
}
} else if system_ui_active {
system_ui_active = false;
let _ = app_handle.emit("osd-system-ui-inactive", ());
}
} else if system_ui_active {
system_ui_active = false;
let _ = app_handle.emit("osd-system-ui-inactive", ());
}
}
}
thread::sleep(Duration::from_millis(150));
}
});
Ok(())
}
/// 停止所有 OSD 监视线程
#[tauri::command]
pub fn osd_stop_watch() {
drag_stop().store(true, Ordering::SeqCst);
topmost_stop().store(true, Ordering::SeqCst);
}
/// 设置点击穿透(Rust 侧原生 WS_EX_TRANSPARENT,比 JS setIgnoreCursorEvents 更可靠)
#[tauri::command]
pub fn osd_set_click_through(label: String, enabled: bool, app: AppHandle) -> Result<(), String> {
#[cfg(windows)]
{
let hwnd = win_api::get_hwnd(&label, &app)
.ok_or_else(|| format!("窗口 {} 不存在", label))?;
win_api::set_click_through(hwnd, enabled);
}
Ok(())
}
/// 设置窗口置顶状态(Rust 侧原生 SetWindowPos
#[tauri::command]
pub fn osd_set_topmost(label: String, topmost: bool, app: AppHandle) -> Result<(), String> {
#[cfg(windows)]
{
let hwnd = win_api::get_hwnd(&label, &app)
.ok_or_else(|| format!("窗口 {} 不存在", label))?;
win_api::set_topmost(hwnd, topmost);
}
Ok(())
}
/// 启动原生拖动(右键长按触发)
///
/// 同步关闭点击穿透(WS_EX_TRANSPARENT),然后在独立线程中调用
/// `SendMessage(WM_NCLBUTTONDOWN, HTCAPTION)` 进入原生标题栏拖动模式。
/// 相比前端 `startDragging()`,此方式在 WS_EX_NOACTIVATE 窗口上更可靠。
#[tauri::command]
pub fn osd_begin_drag(label: String, app: AppHandle) -> Result<(), String> {
#[cfg(windows)]
{
let hwnd = win_api::get_hwnd(&label, &app)
.ok_or_else(|| format!("窗口 {} 不存在", label))?;
// 先同步关闭穿透,确保窗口能接收后续鼠标事件
win_api::set_click_through(hwnd, false);
// SendMessageW 会阻塞直到拖动结束,在独立线程中调用避免阻塞 IPC
thread::spawn(move || {
win_api::begin_drag(hwnd);
});
}
Ok(())
}