不抢焦点的浮窗
不抢焦点的浮窗
非聚焦的浮窗
参考: GPT-5.2-Codex
背景
Tauri windows 上有没什么方法显示一个小控件,上面有按钮可以点,鼠标聚焦和点击上面的按钮时不会影响原聚焦 (游标正常闪烁,可以正常输入和移动游标)
像夸克的划词工具就有这样的特性
方案
在 Windows 上这类需求一般叫 “不抢焦点的浮窗/工具窗”。Tauri 默认创建的窗口一旦可点击就会获取焦点,所以需要用 原生 Windows API 把窗口设置为 “可点击但不激活”(不会夺走当前输入焦点)。大致思路:
✅ 可行方案(Windows 专属)
1) 设置窗口为 WS_EX_NOACTIVATE
这个扩展样式会让窗口在点击时不激活,焦点仍留在原窗口,光标继续闪烁。
2) 仍然能接收鼠标事件
只要不设置 WS_EX_TRANSPARENT,它仍能响应按钮点击。
✅ 在 Tauri 里怎么做
Tauri 没有内建跨平台 API,但可以用:
- tauri plugin / Rust side 调用 WinAPI
- 通过
winit/tao句柄拿到 HWND
示例思路(伪代码):
use windows::Win32::UI::WindowsAndMessaging::*;
let hwnd = get_hwnd_from_tauri_window(); // 需从 wry/tao 获取
let ex_style = GetWindowLongW(hwnd, GWL_EXSTYLE);
SetWindowLongW(hwnd, GWL_EXSTYLE, ex_style | WS_EX_NOACTIVATE | WS_EX_TOOLWINDOW);✅ 还能配合的特性
| 需求 | WinAPI |
|---|---|
| 不出现在任务栏 | WS_EX_TOOLWINDOW |
| 置顶浮层 | SetWindowPos(HWND_TOPMOST) |
| 不抢焦点 | WS_EX_NOACTIVATE |
✅ 总结
要做到“点击不抢焦点”,必须用 Windows 原生 API 设置
WS_EX_NOACTIVATE。
Tauri 目前没有跨平台开箱支持,只能通过 Rust side 处理。
方案二
## 需求
Tauri 如何出现不抢焦点的窗口:置顶但不抢聚焦。
如果可以的话,还要让该窗口可以被点击里面的按钮,但弹出时不能让原来的窗口聚焦被改变
例子: 像 "输入法" / `win+v` / `win+;` / "夸克的悬浮窗" 那样
前端部分是否能完成是否抢焦点模式的切换?
## 相关代码
/** 窗口切换是否显示 */
export async function toggleWindow(panel_list?: string[]) {
try {
const appWindow = getCurrentWindow()
// const isVisible = await appWindow.isVisible() // 检查窗口是否可见
const isFocused = await appWindow.isFocused() // 窗口是否聚焦
if (isFocused) {
await hideWindow()
} else {
await showWindow(panel_list)
}
} catch (error) {
console.error('Window show fail:', error)
}
}
...
async function showWindow(panel_list?: string[]) {
...
// step5. 应用坐标并显示窗口
await appWindow.setPosition(cursor) // 先移动再显示,await应该不用删
await appWindow.setIgnoreCursorEvents(false) // 关闭点击穿透 (点击透明部分可能会临时打开)
await appWindow.show(); global_state.isWindowVisible = true;
await appWindow.setFocus() // 聚焦窗口
// 这是必须的,否则不会显示/置顶窗口。注意作为菜单窗口而言,窗口消失时要恢复聚焦与光标
}下面给出实现思路与示例代码,分成“仅不抢焦点显示”与“可点击但不激活(Windows 方案)”。
关键点
- 创建窗口时就设为不聚焦:
focused(false)。 - 显示时不要再
setFocus()。 - 保持置顶:
always_on_top(true)。 - “可点击但不激活”属于 OS 级特性:Tauri 默认 API 没有跨平台开关。
- Windows 可以用原生 API 设
WS_EX_NOACTIVATE+SWP_NOACTIVATE,做到可点击但不改变前台窗口焦点。 - macOS 需用 AppKit 调用
orderFrontRegardless/setLevel等;Linux 依窗口管理器而异。下面示例只给 Windows 方案。
- Windows 可以用原生 API 设
创建一个默认不抢焦点的悬浮窗
在 Rust 端创建窗口,初始就不聚焦、置顶、可透明/无边框(按需)。
use tauri::{Manager, WindowBuilder, WindowUrl};
fn main() {
tauri::Builder::default()
.setup(|app| {
WindowBuilder::new(
app,
"overlay", // 窗口 label
WindowUrl::App("index.html".into()), // 你的前端入口
)
.focused(false) // 初始不聚焦
.visible(false) // 先不可见,调用 show 时再出现
.always_on_top(true) // 置顶
.skip_taskbar(true) // 不显示任务栏(可选)
.decorations(false) // 无边框(可选)
.transparent(true) // 透明(可选)
.resizable(false) // 不可调尺寸(可选)
.build()?;
Ok(())
})
.run(tauri::generate_context!())
.expect("error while running tauri application");
}前端的显示/隐藏逻辑,不要调用 setFocus():
import { WebviewWindow } from '@tauri-apps/api/window';
const overlay =
WebviewWindow.getByLabel('overlay') ??
new WebviewWindow('overlay', { url: 'index.html' });
export async function showOverlay() {
await overlay.setIgnoreCursorEvents(false); // 可点击
await overlay.setAlwaysOnTop(true);
await overlay.show(); // 不要 overlay.setFocus()
}
export async function hideOverlay() {
await overlay.hide();
}这样可以做到:出现时置顶但尽量不抢焦点。多数场景下,show() 不再显式聚焦即可满足“只显示不激活”。如果仍被系统强制激活,请看下方 Windows 原生方案。
Windows:可点击但不激活(不改变原窗口焦点)
需要用原生 API 设窗口扩展样式 WS_EX_NOACTIVATE 并用 SetWindowPos 带 SWP_NOACTIVATE。可写一个 Tauri command:
use tauri::{AppHandle, Manager};
#[cfg(target_os = "windows")]
use windows::{
Win32::Foundation::HWND,
Win32::UI::WindowsAndMessaging::{
GetWindowLongW, SetWindowLongW, SetWindowPos, GWL_EXSTYLE, SWP_NOMOVE, SWP_NOSIZE,
SWP_NOZORDER, SWP_SHOWWINDOW, SWP_NOACTIVATE, HWND_TOPMOST, WS_EX_NOACTIVATE,
WS_EX_TOOLWINDOW,
},
};
#[cfg(target_os = "windows")]
#[tauri::command]
pub fn make_no_activate(app: AppHandle, label: &str) -> Result<(), String> {
let window = app
.get_webview_window(label)
.ok_or_else(|| format!("window {label} not found"))?;
// 安全前提:Tauri 已保证同进程 HWND
let hwnd = window.hwnd().map_err(|e| e.to_string())? as isize;
let hwnd = HWND(hwnd as _);
unsafe {
// 加上 NOACTIVATE 和 TOOLWINDOW(避免任务栏图标)
let ex_style = GetWindowLongW(hwnd, GWL_EXSTYLE);
SetWindowLongW(hwnd, GWL_EXSTYLE, ex_style | WS_EX_NOACTIVATE.0 | WS_EX_TOOLWINDOW.0);
// 置顶且不激活地显示
SetWindowPos(
hwnd,
HWND_TOPMOST,
0,
0,
0,
0,
SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_SHOWWINDOW | SWP_NOACTIVATE,
);
}
Ok(())
}在 main.rs 里注册这个 command:
mod window_no_activate;
fn main() {
tauri::Builder::default()
.invoke_handler(tauri::generate_handler![window_no_activate::make_no_activate])
// 其他 setup...
.run(tauri::generate_context!())
.expect("error while running tauri application");
}前端调用以保持可点击但不激活:
import { invoke } from '@tauri-apps/api/tauri';
import { WebviewWindow } from '@tauri-apps/api/window';
const overlay = WebviewWindow.getByLabel('overlay') ?? new WebviewWindow('overlay');
export async function showOverlayNoActivate() {
await overlay.setIgnoreCursorEvents(false);
await overlay.show(); // 需要先 show,否则可能拿不到 HWND 或被隐藏
await invoke('make_no_activate', { label: 'overlay' });
}备注与局限
- 跨平台差异:
WS_EX_NOACTIVATE仅 Windows;macOS 需用 AppKit 级调用(如orderFrontRegardless、设置 window level),需自定义 Rust 代码;Linux 取决于窗口管理器。 - 前端无法单独切换“抢焦点模式”:是否激活是 OS 级行为,前端只能通过调用后端 command 间接实现。若需要“模式切换”,可以做两个窗口(一个可激活、一个不可激活)或在后端切换样式。
- 若仅需“显示但尽量不抢焦点”,通常删除
setFocus()并在创建时focused(false)就足够。真正要求“可点但不激活”则必须走原生扩展样式。
这样即可实现类似输入法/剪贴板候选窗的行为:置顶、可点、不改变原应用的前台焦点(需原生支持)。