监控、OSD
This commit is contained in:
@@ -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(())
|
||||
}
|
||||
Reference in New Issue
Block a user