剪切板操作的优化手段
剪切板操作的优化手段
原代码,改进前
/// 通过模拟复制来获取当前选中的文本
///
/// 模拟Ctrl+C一般是用 winuser::keybd_event / enigo,不用 simulate (组合键存在问题)
///
/// 需要注意的是: ctrl+c模拟函数到按键按出来,以及按出来后到剪切板刷新。通常需要等待一小会儿,再获取时结果才是正确的
/// 这个时间不确定 (根据系统不同可能不同,但通常不能太短)
///
/// 优化: 不过好在这里的复制时机是展开面板时,该函数可以线程/闭包执行。
/// 而不像我之前搞 autohotkey 或 kanata 那样用热键触发,慢得多
fn get_selected_by_clipboard() -> Option<String> {
match text::clipboard::simulate_copy() {
Ok(_) => {}
Err(_) => { log::error!("Failed to simulate copy"); return None; }
};
std::thread::sleep(std::time::Duration::from_millis(100));
let Ok(selected_text) = text::clipboard::clipboard_get_text() else {
log::error!("Failed to get clipboard text");
return None;
};
Some(selected_text)
}
pub mod clipboard {
/// 模拟复制按键
pub fn simulate_copy() -> Result<(), String> {
log::info!("simulate_copy called start");
#[cfg(not(target_os = "windows"))]
Err("Copy simulation not implemented for this platform".to_string());
#[cfg(target_os = "windows")] {
use winapi::um::winuser::{keybd_event, KEYEVENTF_KEYUP, VK_CONTROL, VK_MENU};
const VK_C: u8 = b'C'; // 大写C的ascii码是 67 = 0x43
const VK_A: u8 = b'A'; // 大写A的ascii码是 65 = 0x41
unsafe {
// 可能的冲突: 释放 Alt和A, 防止与召唤菜单时的按键冲突
// 焦点转移策略时,需要在菜单召唤前 (焦点转移前) 完成
keybd_event(VK_A, 0, KEYEVENTF_KEYUP, 0);
keybd_event(VK_MENU as u8, 0, KEYEVENTF_KEYUP, 0);
keybd_event(VK_CONTROL as u8, 0, 0, 0); // 按下 Ctrl
keybd_event(VK_C, 0, 0, 0); // 按下 C
keybd_event(VK_C, 0, KEYEVENTF_KEYUP, 0); // 释放 C
keybd_event(VK_CONTROL as u8, 0, KEYEVENTF_KEYUP, 0); // 释放 Ctrl
}
}
log::info!("simulate_copy called end");
Ok(())
}
}存在问题、改进思路
尽可能将get_selected_by_clipboard的返回时间缩短且保证能成功获取到当前的选择对象
旧方案中加固定时间延时的方式并不可靠,一来可能会使用过长的时间导致阻塞,二来可能使用过短的时间导致获取了上次的复制内容
是否有得知是否成功按完按键和成功复制和剪切板成功更新的方法
改进后
gpt 思路
(by gemini 2.5 pro)
你好!你提出了一个非常好的问题,这确实是在 Windows 上通过模拟按键与剪切板交互时常见的痛点。固定的 sleep 既不可靠,也效率低下。
我们可以通过使用更高级的 Windows API 来解决这个问题,实现一个带超时的、事件驱动的等待机制,而不是固定时间的休眠。
下面是改进后的代码和详细解释。
核心改进思路
- 不再使用固定
sleep:我们将轮询(poll)检查剪切板是否真的发生了变化。 - 使用
GetClipboardSequenceNumber:这是一个 Windows API 函数,它返回一个随剪切板内容改变而递增的唯一编号。我们可以在模拟复制前获取一次编号,然后循环等待这个编号发生变化。 - 添加超时机制:为了防止在没有选中任何内容(导致复制失败,剪切板无变化)的情况下无限等待,我们会加入一个总的等待超时时间。
- 优化轮询:在轮询等待时,使用非常短的休眠(如 1 毫秒)来防止 CPU 空转,同时能快速响应变化。
对你问题的直接回答
- 如何缩短返回时间且保证成功?
- 通过上述代码中的
GetClipboardSequenceNumber和带超时的轮询机制。函数不再等待一个固定的、可能过长的时间,而是在检测到剪切板确实发生变化的时刻立即继续执行。这使得在成功复制的情况下,等待时间可以被缩短到只有几毫秒。同时,超时机制处理了复制失败(如没有选中任何内容)的场景,避免了无限等待。
- 通过上述代码中的
- 是否有得知是否成功按完按键的方法?
keybd_event函数本身是“即发即忘”的。当你调用它时,它只是将一个键盘输入事件投递到系统事件队列中。它不保证另一个应用程序会接收并处理这个事件,也不会返回任何关于按键是否“成功”被处理的信息。在 Windows 中,键盘输入是一个低级事件流,系统不跟踪单个按键的“完成”状态。因此,我们无法直接知道按键是否成功,最好的间接方法就是观察其副作用——在这个场景下,就是剪切板内容的变化。
- 是否有得知剪切板成功更新的方法?
- 有,这正是我们改进方案的核心!
GetClipboardSequenceNumber()函数就是为此设计的。每次剪切板的内容被新内容替换时,系统都会增加这个全局序列号。通过比较调用simulate_copy前后的序列号,我们就能确切地知道剪切板是否被更新了。 - 更高级的方法:还有一个更复杂的方案,即创建一个隐藏的窗口并使用
AddClipboardFormatListener函数。这会让你的窗口在剪切板更新时收到一个WM_CLIPBOARDUPDATE消息。这种方法是纯事件驱动的,完全消除了轮询,但实现起来要复杂得多(需要设置窗口、消息循环等)。对于你这个“一次性获取”的需求,轮询GetClipboardSequenceNumber是性能和实现复杂度之间最好的平衡。
- 有,这正是我们改进方案的核心!
希望这个改进方案能帮到你!
代码
核心: 轮询 + GetClipboardSequenceNumber
(详见 github LincZero 的 anymenu 项目,的 Tauri 子项目的 text.rs / text_c.rs )
链接到当前文件 0
没有文件链接到当前文件