This commit is contained in:
zhongluofeng
2026-07-30 09:09:29 +08:00
parent 74902c4cec
commit f452322aad
32 changed files with 4586 additions and 115 deletions
+280
View File
@@ -0,0 +1,280 @@
//! 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)。
pub fn dib_to_png(dib: &[u8]) -> Option<Vec<u8>> {
use image::codecs::png::PngEncoder;
use image::ImageEncoder;
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 || compression != 0 {
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
rgba[dp + 3] = 255; // A
}
}
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)
}