298 lines
9.7 KiB
Rust
298 lines
9.7 KiB
Rust
//! Win32 剪贴板读取与写回
|
||
//!
|
||
//! 读取优先级:文本 > 图片 > 文件。写回支持三种格式。
|
||
//! 句柄类型说明(windows-sys 0.52):
|
||
//! - HWND / HANDLE = isize
|
||
//! - HGLOBAL / HLOCAL = *mut c_void
|
||
//! - HDROP = isize(与 HANDLE 同)
|
||
|
||
use windows_sys::Win32::System::DataExchange::{
|
||
CloseClipboard, EmptyClipboard, GetClipboardData, IsClipboardFormatAvailable, OpenClipboard,
|
||
SetClipboardData,
|
||
};
|
||
use windows_sys::Win32::System::Memory::{GlobalAlloc, GlobalLock, GlobalSize, GlobalUnlock, GMEM_MOVEABLE, GMEM_ZEROINIT};
|
||
use windows_sys::Win32::System::Ole::{CF_DIB, CF_HDROP, CF_UNICODETEXT};
|
||
use windows_sys::Win32::UI::Shell::DragQueryFileW;
|
||
|
||
/// 剪贴板当前内容
|
||
pub enum ClipData {
|
||
Text(String),
|
||
Image { dib: Vec<u8>, width: u32, height: u32 },
|
||
Files(Vec<String>),
|
||
}
|
||
|
||
/// 读取剪贴板(需在 monitor 线程或命令线程调用)。OpenClipboard 失败返回 None。
|
||
pub fn read_clipboard() -> Option<ClipData> {
|
||
unsafe {
|
||
if OpenClipboard(0) == 0 {
|
||
return None;
|
||
}
|
||
let result = if IsClipboardFormatAvailable(CF_UNICODETEXT as u32) != 0 {
|
||
read_text()
|
||
} else if IsClipboardFormatAvailable(CF_DIB as u32) != 0 {
|
||
read_image()
|
||
} else if IsClipboardFormatAvailable(CF_HDROP as u32) != 0 {
|
||
read_files()
|
||
} else {
|
||
None
|
||
};
|
||
CloseClipboard();
|
||
result
|
||
}
|
||
}
|
||
|
||
unsafe fn read_text() -> Option<ClipData> {
|
||
let h = GetClipboardData(CF_UNICODETEXT as u32); // HANDLE = isize
|
||
if h == 0 {
|
||
return None;
|
||
}
|
||
let hglob = h as *mut core::ffi::c_void; // 实际是 HGLOBAL
|
||
let ptr = GlobalLock(hglob) as *const u16;
|
||
if ptr.is_null() {
|
||
return None;
|
||
}
|
||
let mut len = 0usize;
|
||
while *ptr.add(len) != 0 {
|
||
len += 1;
|
||
}
|
||
let slice = core::slice::from_raw_parts(ptr, len);
|
||
let s = String::from_utf16_lossy(slice);
|
||
GlobalUnlock(hglob);
|
||
if s.is_empty() {
|
||
None
|
||
} else {
|
||
Some(ClipData::Text(s))
|
||
}
|
||
}
|
||
|
||
unsafe fn read_image() -> Option<ClipData> {
|
||
let h = GetClipboardData(CF_DIB as u32);
|
||
if h == 0 {
|
||
return None;
|
||
}
|
||
let hglob = h as *mut core::ffi::c_void;
|
||
let ptr = GlobalLock(hglob) as *const u8;
|
||
if ptr.is_null() {
|
||
return None;
|
||
}
|
||
let size = GlobalSize(hglob);
|
||
let slice = core::slice::from_raw_parts(ptr, size);
|
||
let dib = slice.to_vec();
|
||
GlobalUnlock(hglob);
|
||
let (w, hgt) = dib_info(&dib).unwrap_or((0, 0));
|
||
Some(ClipData::Image {
|
||
dib,
|
||
width: w,
|
||
height: hgt,
|
||
})
|
||
}
|
||
|
||
unsafe fn read_files() -> Option<ClipData> {
|
||
let h = GetClipboardData(CF_HDROP as u32); // HANDLE = isize
|
||
if h == 0 {
|
||
return None;
|
||
}
|
||
let hdrop = h; // HDROP = isize
|
||
let count = DragQueryFileW(hdrop, 0xFFFFFFFF, core::ptr::null_mut(), 0);
|
||
let mut files = Vec::with_capacity(count as usize);
|
||
for i in 0..count {
|
||
let len = DragQueryFileW(hdrop, i, core::ptr::null_mut(), 0);
|
||
if len == 0 {
|
||
continue;
|
||
}
|
||
let mut buf = vec![0u16; (len as usize) + 1];
|
||
let got = DragQueryFileW(hdrop, i, buf.as_mut_ptr(), buf.len() as u32);
|
||
let s = String::from_utf16_lossy(&buf[..got as usize]);
|
||
files.push(s);
|
||
}
|
||
// 剪贴板拥有句柄,不调用 DragFinish
|
||
if files.is_empty() {
|
||
None
|
||
} else {
|
||
Some(ClipData::Files(files))
|
||
}
|
||
}
|
||
|
||
// ===== 写回剪贴板(copy_back) =====
|
||
|
||
/// 写回文本。失败返回 false。写回会清空其他格式。
|
||
pub fn write_text(s: &str) -> bool {
|
||
unsafe {
|
||
if OpenClipboard(0) == 0 {
|
||
return false;
|
||
}
|
||
let r = write_text_inner(s);
|
||
CloseClipboard();
|
||
r
|
||
}
|
||
}
|
||
|
||
unsafe fn write_text_inner(s: &str) -> bool {
|
||
if EmptyClipboard() == 0 {
|
||
return false;
|
||
}
|
||
let mut utf16: Vec<u16> = s.encode_utf16().collect();
|
||
utf16.push(0); // null terminator
|
||
let byte_len = utf16.len() * 2;
|
||
let hglob = GlobalAlloc(GMEM_MOVEABLE, byte_len);
|
||
if hglob.is_null() {
|
||
return false;
|
||
}
|
||
let ptr = GlobalLock(hglob) as *mut u16;
|
||
if ptr.is_null() {
|
||
return false;
|
||
}
|
||
core::ptr::copy_nonoverlapping(utf16.as_ptr(), ptr, utf16.len());
|
||
GlobalUnlock(hglob);
|
||
SetClipboardData(CF_UNICODETEXT as u32, hglob as isize) != 0
|
||
}
|
||
|
||
/// 写回图片(CF_DIB 原始字节)。
|
||
pub fn write_dib(dib: &[u8]) -> bool {
|
||
unsafe {
|
||
if OpenClipboard(0) == 0 {
|
||
return false;
|
||
}
|
||
let r = (|| {
|
||
if EmptyClipboard() == 0 {
|
||
return false;
|
||
}
|
||
let hglob = GlobalAlloc(GMEM_MOVEABLE, dib.len());
|
||
if hglob.is_null() {
|
||
return false;
|
||
}
|
||
let ptr = GlobalLock(hglob) as *mut u8;
|
||
if ptr.is_null() {
|
||
return false;
|
||
}
|
||
core::ptr::copy_nonoverlapping(dib.as_ptr(), ptr, dib.len());
|
||
GlobalUnlock(hglob);
|
||
SetClipboardData(CF_DIB as u32, hglob as isize) != 0
|
||
})();
|
||
CloseClipboard();
|
||
r
|
||
}
|
||
}
|
||
|
||
/// 写回文件列表(CF_HDROP)。
|
||
pub fn write_files(paths: &[String]) -> bool {
|
||
unsafe {
|
||
if OpenClipboard(0) == 0 {
|
||
return false;
|
||
}
|
||
let r = (|| {
|
||
if EmptyClipboard() == 0 {
|
||
return false;
|
||
}
|
||
// DROPFILES 头部 20 字节:pFiles(u32) + pt.x(i32) + pt.y(i32) + fNC(i32) + fWide(i32)
|
||
const DF_SIZE: usize = 20;
|
||
let mut wide: Vec<u16> = Vec::new();
|
||
for p in paths {
|
||
wide.extend(p.encode_utf16());
|
||
wide.push(0); // 每条字符串 null 结尾
|
||
}
|
||
wide.push(0); // 末尾额外 null 表示列表结束
|
||
let total = DF_SIZE + wide.len() * 2;
|
||
let hglob = GlobalAlloc(GMEM_MOVEABLE | GMEM_ZEROINIT, total);
|
||
if hglob.is_null() {
|
||
return false;
|
||
}
|
||
let ptr = GlobalLock(hglob) as *mut u8;
|
||
if ptr.is_null() {
|
||
return false;
|
||
}
|
||
let offset = DF_SIZE as u32;
|
||
let off_bytes = offset.to_ne_bytes();
|
||
core::ptr::copy_nonoverlapping(off_bytes.as_ptr(), ptr, 4); // pFiles
|
||
let fwide: u32 = 1;
|
||
let fwide_bytes = fwide.to_ne_bytes();
|
||
core::ptr::copy_nonoverlapping(fwide_bytes.as_ptr(), ptr.add(16), 4); // fWide = 1
|
||
let str_ptr = ptr.add(DF_SIZE) as *mut u16;
|
||
core::ptr::copy_nonoverlapping(wide.as_ptr(), str_ptr, wide.len());
|
||
GlobalUnlock(hglob);
|
||
SetClipboardData(CF_HDROP as u32, hglob as isize) != 0
|
||
})();
|
||
CloseClipboard();
|
||
r
|
||
}
|
||
}
|
||
|
||
// ===== DIB → PNG(用于预览) =====
|
||
|
||
/// 从 DIB 字节解析宽高
|
||
pub fn dib_info(dib: &[u8]) -> Option<(u32, u32)> {
|
||
if dib.len() < 40 {
|
||
return None;
|
||
}
|
||
let width = i32::from_le_bytes([dib[4], dib[5], dib[6], dib[7]]);
|
||
let height = i32::from_le_bytes([dib[8], dib[9], dib[10], dib[11]]);
|
||
if width <= 0 {
|
||
return None;
|
||
}
|
||
Some((width as u32, height.abs() as u32))
|
||
}
|
||
|
||
/// 将 CF_DIB 字节转换为 PNG 字节(支持 24/32bpp BI_RGB / BI_BITFIELDS)。
|
||
pub fn dib_to_png(dib: &[u8]) -> Option<Vec<u8>> {
|
||
use image::codecs::png::PngEncoder;
|
||
use image::ImageEncoder;
|
||
|
||
// 兜底:数据本身就是 PNG / JPEG(极少数来源直接存放压缩数据)
|
||
if dib.len() >= 4 && &dib[0..4] == b"\x89PNG" {
|
||
return Some(dib.to_vec());
|
||
}
|
||
if dib.len() >= 3 && dib[0] == 0xFF && dib[1] == 0xD8 && dib[2] == 0xFF {
|
||
return Some(dib.to_vec());
|
||
}
|
||
|
||
if dib.len() < 40 {
|
||
return None;
|
||
}
|
||
let header_size = u32::from_le_bytes([dib[0], dib[1], dib[2], dib[3]]);
|
||
let width = i32::from_le_bytes([dib[4], dib[5], dib[6], dib[7]]);
|
||
let height_raw = i32::from_le_bytes([dib[8], dib[9], dib[10], dib[11]]);
|
||
let bpp = u16::from_le_bytes([dib[14], dib[15]]);
|
||
let compression = u32::from_le_bytes([dib[16], dib[17], dib[18], dib[19]]);
|
||
if width <= 0 {
|
||
return None;
|
||
}
|
||
// 接受 BI_RGB(0) 和 BI_BITFIELDS(3)。
|
||
// Windows 截图工具(Win+Shift+S / 截图工具)常用 BI_BITFIELDS 标记 32bpp BGRA,
|
||
// 像素数据本身未压缩,与 BI_RGB 解码方式一致。
|
||
// 拒绝 BI_RLE4/8(1/2) 和 BI_JPEG/PNG(4/5) 等真正压缩格式。
|
||
if compression != 0 && compression != 3 {
|
||
return None;
|
||
}
|
||
if bpp != 24 && bpp != 32 {
|
||
return None;
|
||
}
|
||
let height = height_raw.abs();
|
||
let top_down = height_raw < 0;
|
||
let pixel_offset = header_size as usize; // 24/32bpp 无调色板
|
||
let row_size = ((bpp as u32 * width as u32 + 31) / 32) * 4;
|
||
let bytes_per_pixel = (bpp / 8) as usize;
|
||
let needed = pixel_offset + (row_size as usize) * (height as usize);
|
||
if dib.len() < needed {
|
||
return None;
|
||
}
|
||
let mut rgba = vec![0u8; (width as usize) * (height as usize) * 4];
|
||
for y in 0..height {
|
||
let src_row = if top_down { y } else { height - 1 - y };
|
||
let src_off = pixel_offset + (src_row as usize) * (row_size as usize);
|
||
for x in 0..width {
|
||
let sp = src_off + (x as usize) * bytes_per_pixel;
|
||
let dp = (y as usize) * (width as usize) * 4 + (x as usize) * 4;
|
||
rgba[dp] = dib[sp + 2]; // R
|
||
rgba[dp + 1] = dib[sp + 1]; // G
|
||
rgba[dp + 2] = dib[sp]; // B
|
||
// 32bpp 保留 alpha 通道(截图工具常用);24bpp 不透明
|
||
rgba[dp + 3] = if bpp == 32 { dib[sp + 3] } else { 255 };
|
||
}
|
||
}
|
||
let mut buf = Vec::new();
|
||
let enc = PngEncoder::new(&mut buf);
|
||
enc.write_image(&rgba, width as u32, height as u32, image::ExtendedColorType::Rgba8)
|
||
.ok()?;
|
||
Some(buf)
|
||
}
|