郭靈紋

Rust 实现 Windows 剪贴板监听

前言

本文主要涉及如何使用 Windows API 实现剪贴板监听器,以及如何在 Rust 中使用指针和内存管理来操作Windows API。

监听 Windows 剪贴板

Windows 剪贴板监听基本原理

Windows 有三种方法可以监视剪贴板的更改:最早的方法是创建剪贴板查看器窗口,Windows 2000 添加了查询剪贴板序列号的功能,Windows Vista 添加了剪贴板格式侦听器。对于新程序,建议使用剪贴板格式侦听器或剪贴板序列号,尤其是侦听器。

剪贴板格式侦听器是一个注册的窗口,它会在剪贴板内容发生更改时收到通知。窗口通过调用 AddClipboardFormatListener 函数注册为剪贴板格式侦听器,当剪贴板的内容发生更改时,会收到一条 WM_CLIPBOARDUPDATE 通知。

通过 CreateWindowEx 方法可以创建一个不可见的消息窗口,它只能收发消息,可以用该窗口注册一个剪贴板侦听器。

代码实现

Rust 最小代码

use std::{
ffi::OsStr,
os::windows::prelude::OsStrExt,
ptr::{null, null_mut},
};

use winapi::um::winuser::{
AddClipboardFormatListener, CreateWindowExW, GetMessageW, HWND_MESSAGE, WM_CLIPBOARDUPDATE,
};

fn main() {
// 创建消息窗口
let hwnd = unsafe {
CreateWindowExW(
0,
str_to_lpcwstr("STATIC").as_ptr(),
null(),
0,
0,
0,
0,
0,
HWND_MESSAGE,
null_mut(),
// wnd_class.hInstance,
null_mut(),
null_mut(),
)
};
if hwnd == null_mut() {
panic!("CreateWindowEx failed");
}

// 添加剪贴板监听器
unsafe { AddClipboardFormatListener(hwnd) };

// 监听窗口消息
let mut msg = unsafe { std::mem::zeroed() };
loop {
let ret = unsafe { GetMessageW(&mut msg, hwnd, 0, 0) };
if ret != 1 {
break;
}
match msg.message {
WM_CLIPBOARDUPDATE => {
println!("WM_CLIPBOARDUPDATE ");
}
_ => (),
}
}
}

fn str_to_lpcwstr(s: &str) -> Vec<u16> {
OsStr::new(s)
.encode_wide()
.chain(std::iter::once(0))
.collect()
}

Rust 封装代码

use std::{
ffi::OsStr,
os::windows::prelude::OsStrExt,
ptr::{null, null_mut},
};

use winapi::{
shared::windef::HWND,
um::winuser::{
AddClipboardFormatListener, CreateWindowExW, GetMessageW, HWND_MESSAGE, MSG,
WM_CLIPBOARDUPDATE,
},
};

fn main() {
ClipboardListen::run(move || {
println!("clipboard updated.");
//TODO::剪贴板内容获取
});
}

pub struct ClipboardListen {}

impl ClipboardListen {
pub fn run<F: Fn() + Send + 'static>(callback: F) {
std::thread::spawn(move || {
for msg in Message::new() {
match msg.message {
WM_CLIPBOARDUPDATE => callback(),
_ => (),
}
}
});
}
}

pub struct Message {
hwnd: HWND,
}

impl Message {
pub fn new() -> Self {
// 创建消息窗口
let hwnd = unsafe {
CreateWindowExW(
0,
str_to_lpcwstr("STATIC").as_ptr(),
null(),
0,
0,
0,
0,
0,
HWND_MESSAGE,
null_mut(),
// wnd_class.hInstance,
null_mut(),
null_mut(),
)
};
if hwnd == null_mut() {
panic!("CreateWindowEx failed");
}

unsafe { AddClipboardFormatListener(hwnd) };

Self { hwnd }
}

fn get(&self) -> Option<MSG> {
let mut msg = unsafe { std::mem::zeroed() };
let ret = unsafe { GetMessageW(&mut msg, self.hwnd, 0, 0) };
if ret == 1 {
Some(msg)
} else {
None
}
}
}

impl Iterator for Message {
type Item = MSG;

fn next(&mut self) -> Option<Self::Item> {
self.get()
}
}

fn str_to_lpcwstr(s: &str) -> Vec<u16> {
OsStr::new(s)
.encode_wide()
.chain(std::iter::once(0))
.collect()
}

总结

尽管最终代码只有短短不到百行,但实现过程异常曲折。

首先,相关资料相对匮乏,而且各种实现方式五花八门,甚至有使用定时器定期检查剪贴板内容差异的方法😅。

通过查看各种开源库的代码,最终选择了添加剪贴板监听器的方案。但由于个人不熟悉Windows API开发,对于为什么需要创建一个窗口来监听剪贴板,进行了大量的资料查阅和理解,但结果仍是一知半解(现在的理解是大概 Windows 万物皆窗口吧x)。

写下这篇文章用于记录,不让努力白费。

相关参考

Windows 剪贴板相关 API

Rust 相关

剪贴板监听讨论

Windows API 库

剪贴板库

剪贴板应用程序