截图模块初始化
This commit is contained in:
@@ -0,0 +1,475 @@
|
||||
//! Windows 屏幕捕获核心(仅 Windows 编译)
|
||||
//!
|
||||
//! 实现:
|
||||
//! - 全屏(虚拟屏)捕获:BitBlt 从屏幕 DC 拷贝到兼容位图,GetDIBits 取像素
|
||||
//! - 窗口捕获:PrintWindow(PW_RENDERFULLCONTENT) 捕获 DWM 内容(覆盖硬件加速窗口)
|
||||
//! - 窗口拾取:WindowFromPoint + GetAncestor(GA_ROOT) 取顶层窗口
|
||||
//! - 顶层窗口枚举:EnumWindows
|
||||
//! - 像素 → PNG / CF_DIB 转换
|
||||
//!
|
||||
//! 设计说明:
|
||||
//! - 不使用 Windows Graphics Capture:避免引入 windows crate(WinRT)增大体积,
|
||||
//! 且 WGC 默认绘制黄色捕获边框,截图工具不可接受。
|
||||
//! - 一次性捕获使用 BitBlt/PrintWindow,延迟低、无用户提示。
|
||||
//! - 32bpp 捕获后强制 alpha=255:BitBlt 取出的 alpha 通道未定义(常为 0),
|
||||
//! 直接编码 PNG 会得到全透明图,故强制不透明。
|
||||
|
||||
use std::sync::Mutex;
|
||||
use windows_sys::Win32::Foundation::{HWND, POINT, RECT};
|
||||
use windows_sys::Win32::Graphics::Gdi::{
|
||||
BitBlt, CreateCompatibleBitmap, CreateCompatibleDC, DeleteDC, DeleteObject, GetDC, GetDIBits,
|
||||
PatBlt, ReleaseDC, SelectObject, BITMAPINFO, BITMAPINFOHEADER, BLACKNESS, DIB_RGB_COLORS,
|
||||
RGBQUAD, SRCCOPY,
|
||||
};
|
||||
use windows_sys::Win32::Storage::Xps::PrintWindow;
|
||||
use windows_sys::Win32::System::DataExchange::{
|
||||
CloseClipboard, EmptyClipboard, OpenClipboard, SetClipboardData,
|
||||
};
|
||||
use windows_sys::Win32::System::Memory::{GlobalAlloc, GlobalLock, GlobalUnlock, GMEM_MOVEABLE};
|
||||
use windows_sys::Win32::System::Ole::CF_DIB;
|
||||
use windows_sys::Win32::UI::WindowsAndMessaging::{
|
||||
EnumWindows, GetAncestor, GetSystemMetrics, GetWindowLongPtrW, GetWindowRect,
|
||||
GetWindowTextLengthW, GetWindowTextW, GWL_EXSTYLE, GA_ROOT, WS_EX_TOOLWINDOW, WindowFromPoint,
|
||||
};
|
||||
|
||||
use super::{CaptureData, ScreenRect, WindowInfo};
|
||||
|
||||
/// 捕获结果(PNG 字节 + 原始 BGRA 像素,像素用于剪贴板 DIB 构造,避免重复解码)
|
||||
pub struct CapturedImage {
|
||||
pub width: i32,
|
||||
pub height: i32,
|
||||
pub png: Vec<u8>,
|
||||
/// 32bpp BGRA,top-down(与 GetDIBits negative height 一致)
|
||||
pub bgra: Vec<u8>,
|
||||
}
|
||||
|
||||
impl From<RECT> for ScreenRect {
|
||||
fn from(r: RECT) -> Self {
|
||||
Self {
|
||||
x: r.left,
|
||||
y: r.top,
|
||||
width: r.right - r.left,
|
||||
height: r.bottom - r.top,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 全屏捕获静态存储(供覆盖层选区后裁剪取用)
|
||||
static FULLSCREEN_CAPTURE: Mutex<Option<CapturedImage>> = Mutex::new(None);
|
||||
|
||||
const SM_XVIRTUALSCREEN: i32 = 76;
|
||||
const SM_YVIRTUALSCREEN: i32 = 77;
|
||||
const SM_CXVIRTUALSCREEN: i32 = 78;
|
||||
const SM_CYVIRTUALSCREEN: i32 = 79;
|
||||
const PW_RENDERFULLCONTENT: u32 = 0x00000002;
|
||||
const BI_RGB: u32 = 0;
|
||||
|
||||
/// 将 BGRA top-down 像素编码为 PNG(强制 alpha=255)
|
||||
fn bgra_to_png(bgra: &[u8], width: i32, height: i32) -> Result<Vec<u8>, String> {
|
||||
use image::codecs::png::PngEncoder;
|
||||
use image::ImageEncoder;
|
||||
if width <= 0 || height <= 0 {
|
||||
return Err("无效尺寸".into());
|
||||
}
|
||||
let w = width as usize;
|
||||
let h = height as usize;
|
||||
let mut rgba = vec![0u8; w * h * 4];
|
||||
for i in 0..(w * h) {
|
||||
rgba[i * 4] = bgra[i * 4 + 2]; // R
|
||||
rgba[i * 4 + 1] = bgra[i * 4 + 1]; // G
|
||||
rgba[i * 4 + 2] = bgra[i * 4]; // B
|
||||
rgba[i * 4 + 3] = 255; // 强制不透明
|
||||
}
|
||||
let mut buf = Vec::with_capacity(w * h * 4 / 3);
|
||||
PngEncoder::new(&mut buf)
|
||||
.write_image(&rgba, width as u32, height as u32, image::ExtendedColorType::Rgba8)
|
||||
.map_err(|e| format!("PNG 编码失败: {}", e))?;
|
||||
Ok(buf)
|
||||
}
|
||||
|
||||
/// 从 HBITMAP 提取 32bpp BGRA top-down 像素
|
||||
unsafe fn extract_pixels(
|
||||
hdc_mem: isize,
|
||||
hbm: isize,
|
||||
width: i32,
|
||||
height: i32,
|
||||
) -> Result<Vec<u8>, String> {
|
||||
let bi = BITMAPINFO {
|
||||
bmiHeader: BITMAPINFOHEADER {
|
||||
biSize: std::mem::size_of::<BITMAPINFOHEADER>() as u32,
|
||||
biWidth: width,
|
||||
biHeight: -height, // 负值 = top-down
|
||||
biPlanes: 1,
|
||||
biBitCount: 32,
|
||||
biCompression: BI_RGB,
|
||||
biSizeImage: 0,
|
||||
biXPelsPerMeter: 0,
|
||||
biYPelsPerMeter: 0,
|
||||
biClrUsed: 0,
|
||||
biClrImportant: 0,
|
||||
},
|
||||
bmiColors: [RGBQUAD {
|
||||
rgbBlue: 0,
|
||||
rgbGreen: 0,
|
||||
rgbRed: 0,
|
||||
rgbReserved: 0,
|
||||
}],
|
||||
};
|
||||
let mut pixels = vec![0u8; (width as usize) * (height as usize) * 4];
|
||||
let got = GetDIBits(
|
||||
hdc_mem,
|
||||
hbm,
|
||||
0,
|
||||
height as u32,
|
||||
pixels.as_mut_ptr() as *mut _,
|
||||
&bi as *const _ as *mut _,
|
||||
DIB_RGB_COLORS,
|
||||
);
|
||||
if got == 0 {
|
||||
return Err("GetDIBits 失败".into());
|
||||
}
|
||||
Ok(pixels)
|
||||
}
|
||||
|
||||
/// 捕获整个虚拟屏(所有显示器拼接为一张图)
|
||||
pub fn capture_virtual_screen() -> Result<CapturedImage, String> {
|
||||
unsafe {
|
||||
let x = GetSystemMetrics(SM_XVIRTUALSCREEN);
|
||||
let y = GetSystemMetrics(SM_YVIRTUALSCREEN);
|
||||
let w = GetSystemMetrics(SM_CXVIRTUALSCREEN);
|
||||
let h = GetSystemMetrics(SM_CYVIRTUALSCREEN);
|
||||
if w <= 0 || h <= 0 {
|
||||
return Err("无法获取虚拟屏尺寸".into());
|
||||
}
|
||||
|
||||
let hdc_screen = GetDC(0);
|
||||
if hdc_screen == 0 {
|
||||
return Err("GetDC 失败".into());
|
||||
}
|
||||
let hdc_mem = CreateCompatibleDC(hdc_screen);
|
||||
if hdc_mem == 0 {
|
||||
ReleaseDC(0, hdc_screen);
|
||||
return Err("CreateCompatibleDC 失败".into());
|
||||
}
|
||||
let hbm = CreateCompatibleBitmap(hdc_screen, w, h);
|
||||
if hbm == 0 {
|
||||
DeleteDC(hdc_mem);
|
||||
ReleaseDC(0, hdc_screen);
|
||||
return Err("CreateCompatibleBitmap 失败".into());
|
||||
}
|
||||
let old = SelectObject(hdc_mem, hbm);
|
||||
|
||||
let ok = BitBlt(hdc_mem, 0, 0, w, h, hdc_screen, x, y, SRCCOPY);
|
||||
|
||||
let result = if ok == 0 {
|
||||
Err("BitBlt 失败".into())
|
||||
} else {
|
||||
extract_pixels(hdc_mem, hbm, w, h).and_then(|bgra| {
|
||||
bgra_to_png(&bgra, w, h).map(|png| CapturedImage {
|
||||
width: w,
|
||||
height: h,
|
||||
png,
|
||||
bgra,
|
||||
})
|
||||
})
|
||||
};
|
||||
|
||||
SelectObject(hdc_mem, old);
|
||||
DeleteObject(hbm);
|
||||
DeleteDC(hdc_mem);
|
||||
ReleaseDC(0, hdc_screen);
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
/// 捕获指定窗口(PrintWindow + PW_RENDERFULLCONTENT,覆盖硬件加速窗口)
|
||||
pub fn capture_window(hwnd: isize) -> Result<CapturedImage, String> {
|
||||
unsafe {
|
||||
let mut rect: RECT = std::mem::zeroed();
|
||||
if GetWindowRect(hwnd, &mut rect) == 0 {
|
||||
return Err("GetWindowRect 失败".into());
|
||||
}
|
||||
let w = rect.right - rect.left;
|
||||
let h = rect.bottom - rect.top;
|
||||
if w <= 0 || h <= 0 {
|
||||
return Err("窗口尺寸无效".into());
|
||||
}
|
||||
|
||||
let hdc_screen = GetDC(0);
|
||||
if hdc_screen == 0 {
|
||||
return Err("GetDC 失败".into());
|
||||
}
|
||||
let hdc_mem = CreateCompatibleDC(hdc_screen);
|
||||
let hbm = CreateCompatibleBitmap(hdc_screen, w, h);
|
||||
let old = SelectObject(hdc_mem, hbm);
|
||||
|
||||
// 先用黑色填充(PrintWindow 对部分窗口不绘制透明区域)
|
||||
PatBlt(hdc_mem, 0, 0, w, h, BLACKNESS);
|
||||
|
||||
let ok = PrintWindow(hwnd, hdc_mem, PW_RENDERFULLCONTENT);
|
||||
|
||||
let result = if ok == 0 {
|
||||
Err("PrintWindow 失败(可能窗口无响应或权限不足)".into())
|
||||
} else {
|
||||
extract_pixels(hdc_mem, hbm, w, h).and_then(|bgra| {
|
||||
bgra_to_png(&bgra, w, h).map(|png| CapturedImage {
|
||||
width: w,
|
||||
height: h,
|
||||
png,
|
||||
bgra,
|
||||
})
|
||||
})
|
||||
};
|
||||
|
||||
SelectObject(hdc_mem, old);
|
||||
DeleteObject(hbm);
|
||||
DeleteDC(hdc_mem);
|
||||
ReleaseDC(0, hdc_screen);
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取指定屏幕坐标下的顶层窗口(窗口拾取)
|
||||
///
|
||||
/// 入参 x/y 为物理屏幕坐标(前端需按显示器 scaleFactor 从逻辑坐标换算)。
|
||||
pub fn window_from_point(x: i32, y: i32) -> Option<WindowInfo> {
|
||||
unsafe {
|
||||
let pt = POINT { x, y };
|
||||
let mut hwnd = WindowFromPoint(pt);
|
||||
if hwnd == 0 {
|
||||
return None;
|
||||
}
|
||||
// 取顶层父窗口(WindowFromPoint 可能返回子窗口)
|
||||
let root = GetAncestor(hwnd, GA_ROOT);
|
||||
if root != 0 {
|
||||
hwnd = root;
|
||||
}
|
||||
if windows_sys::Win32::UI::WindowsAndMessaging::IsWindowVisible(hwnd) == 0 {
|
||||
return None;
|
||||
}
|
||||
let mut rect: RECT = std::mem::zeroed();
|
||||
if GetWindowRect(hwnd, &mut rect) == 0 {
|
||||
return None;
|
||||
}
|
||||
let srect = ScreenRect::from(rect);
|
||||
if srect.width <= 0 || srect.height <= 0 {
|
||||
return None;
|
||||
}
|
||||
Some(WindowInfo {
|
||||
hwnd,
|
||||
title: get_window_title(hwnd),
|
||||
rect: srect,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// 枚举所有可见、有标题的顶层窗口(供窗口列表选择)
|
||||
pub fn enum_visible_windows() -> Vec<WindowInfo> {
|
||||
extern "system" fn enum_proc(hwnd: HWND, lparam: isize) -> i32 {
|
||||
unsafe {
|
||||
let vec = &mut *(lparam as *mut Vec<WindowInfo>);
|
||||
if windows_sys::Win32::UI::WindowsAndMessaging::IsWindowVisible(hwnd) == 0 {
|
||||
return 1;
|
||||
}
|
||||
let mut rect: RECT = std::mem::zeroed();
|
||||
if GetWindowRect(hwnd, &mut rect) == 0 {
|
||||
return 1;
|
||||
}
|
||||
let srect = ScreenRect::from(rect);
|
||||
if srect.width < 20 || srect.height < 20 {
|
||||
return 1;
|
||||
}
|
||||
// 跳过工具窗口(本应用 OSD / 托盘菜单 / 截图覆盖层)
|
||||
let ex = GetWindowLongPtrW(hwnd, GWL_EXSTYLE);
|
||||
if ex & (WS_EX_TOOLWINDOW as isize) != 0 {
|
||||
return 1;
|
||||
}
|
||||
let title = get_window_title(hwnd);
|
||||
if title.is_empty() {
|
||||
return 1;
|
||||
}
|
||||
vec.push(WindowInfo {
|
||||
hwnd,
|
||||
title,
|
||||
rect: srect,
|
||||
});
|
||||
}
|
||||
1
|
||||
}
|
||||
|
||||
let mut out: Vec<WindowInfo> = Vec::new();
|
||||
unsafe {
|
||||
EnumWindows(Some(enum_proc), &mut out as *mut _ as isize);
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
unsafe fn get_window_title(hwnd: HWND) -> String {
|
||||
let len = GetWindowTextLengthW(hwnd);
|
||||
if len <= 0 {
|
||||
return String::new();
|
||||
}
|
||||
let mut buf = vec![0u16; (len as usize) + 1];
|
||||
let got = GetWindowTextW(hwnd, buf.as_mut_ptr(), buf.len() as i32);
|
||||
if got <= 0 {
|
||||
return String::new();
|
||||
}
|
||||
String::from_utf16_lossy(&buf[..got as usize])
|
||||
}
|
||||
|
||||
/// 裁剪已存储的全屏捕获(按物理像素坐标)
|
||||
pub fn crop_stored(x: i32, y: i32, w: i32, h: i32) -> Result<CaptureData, String> {
|
||||
let guard = FULLSCREEN_CAPTURE.lock().map_err(|e| e.to_string())?;
|
||||
let img = guard.as_ref().ok_or("无已存储的全屏捕获")?;
|
||||
if x < 0 || y < 0 || w <= 0 || h <= 0 || x + w > img.width || y + h > img.height {
|
||||
return Err("裁剪区域越界".into());
|
||||
}
|
||||
let sw = img.width as usize;
|
||||
let sub = crop_bgra(&img.bgra, sw, x, y, w, h)?;
|
||||
let png = bgra_to_png(&sub, w, h)?;
|
||||
Ok(CaptureData {
|
||||
png_base64: base64_encode(&png),
|
||||
width: w,
|
||||
height: h,
|
||||
})
|
||||
}
|
||||
|
||||
fn crop_bgra(
|
||||
src: &[u8],
|
||||
src_width: usize,
|
||||
x: i32,
|
||||
y: i32,
|
||||
w: i32,
|
||||
h: i32,
|
||||
) -> Result<Vec<u8>, String> {
|
||||
let mut out = vec![0u8; (w as usize) * (h as usize) * 4];
|
||||
for row in 0..(h as usize) {
|
||||
let src_off = ((y as usize) + row) * src_width * 4 + (x as usize) * 4;
|
||||
let dst_off = row * (w as usize) * 4;
|
||||
out[dst_off..dst_off + (w as usize) * 4]
|
||||
.copy_from_slice(&src[src_off..src_off + (w as usize) * 4]);
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// 存储全屏捕获(覆盖层选区裁剪时取用)
|
||||
pub fn store_fullscreen(img: CapturedImage) -> CaptureData {
|
||||
let data = CaptureData {
|
||||
png_base64: base64_encode(&img.png),
|
||||
width: img.width,
|
||||
height: img.height,
|
||||
};
|
||||
if let Ok(mut g) = FULLSCREEN_CAPTURE.lock() {
|
||||
*g = Some(img);
|
||||
}
|
||||
data
|
||||
}
|
||||
|
||||
/// 取出并清除全屏捕获
|
||||
pub fn take_fullscreen() -> Option<CaptureData> {
|
||||
let mut g = FULLSCREEN_CAPTURE.lock().ok()?;
|
||||
let img = g.take()?;
|
||||
Some(CaptureData {
|
||||
png_base64: base64_encode(&img.png),
|
||||
width: img.width,
|
||||
height: img.height,
|
||||
})
|
||||
}
|
||||
|
||||
// ===== 输出:剪贴板 / 文件 =====
|
||||
|
||||
/// 将 PNG base64 解码为像素,构造 CF_DIB 并写入剪贴板
|
||||
pub fn copy_png_to_clipboard(png_base64: &str) -> Result<(), String> {
|
||||
let png = base64_decode(png_base64).ok_or("base64 解码失败")?;
|
||||
let img = image::load_from_memory(&png)
|
||||
.map_err(|e| format!("PNG 解码失败: {}", e))?
|
||||
.to_rgba8();
|
||||
let (w, h) = (img.width() as i32, img.height() as i32);
|
||||
let dib = rgba_to_dib(&img, w, h);
|
||||
write_dib_to_clipboard(&dib)
|
||||
}
|
||||
|
||||
/// RGBA → CF_DIB(BITMAPINFOHEADER + BGRA bottom-up,alpha 强制 255)
|
||||
fn rgba_to_dib(rgba: &image::RgbaImage, width: i32, height: i32) -> Vec<u8> {
|
||||
let header_size = std::mem::size_of::<BITMAPINFOHEADER>() as u32;
|
||||
let row_size = (width as usize) * 4;
|
||||
let mut dib = Vec::with_capacity(header_size as usize + row_size * height as usize);
|
||||
let header = BITMAPINFOHEADER {
|
||||
biSize: header_size,
|
||||
biWidth: width,
|
||||
biHeight: height, // 正值 = bottom-up
|
||||
biPlanes: 1,
|
||||
biBitCount: 32,
|
||||
biCompression: BI_RGB,
|
||||
biSizeImage: (row_size * height as usize) as u32,
|
||||
biXPelsPerMeter: 0,
|
||||
biYPelsPerMeter: 0,
|
||||
biClrUsed: 0,
|
||||
biClrImportant: 0,
|
||||
};
|
||||
let raw = unsafe {
|
||||
std::slice::from_raw_parts(
|
||||
&header as *const _ as *const u8,
|
||||
std::mem::size_of::<BITMAPINFOHEADER>(),
|
||||
)
|
||||
};
|
||||
dib.extend_from_slice(raw);
|
||||
// bottom-up 行序,BGRA 像素,alpha 强制 255
|
||||
for y in (0..height as usize).rev() {
|
||||
for x in 0..width as usize {
|
||||
let p = rgba.get_pixel(x as u32, y as u32).0;
|
||||
dib.push(p[2]); // B
|
||||
dib.push(p[1]); // G
|
||||
dib.push(p[0]); // R
|
||||
dib.push(255); // A
|
||||
}
|
||||
}
|
||||
dib
|
||||
}
|
||||
|
||||
fn write_dib_to_clipboard(dib: &[u8]) -> Result<(), String> {
|
||||
unsafe {
|
||||
if OpenClipboard(0) == 0 {
|
||||
return Err("OpenClipboard 失败".into());
|
||||
}
|
||||
let r = (|| {
|
||||
if EmptyClipboard() == 0 {
|
||||
return Err("EmptyClipboard 失败".to_string());
|
||||
}
|
||||
let hglob = GlobalAlloc(GMEM_MOVEABLE, dib.len());
|
||||
if hglob.is_null() {
|
||||
return Err("GlobalAlloc 失败".to_string());
|
||||
}
|
||||
let ptr = GlobalLock(hglob) as *mut u8;
|
||||
if ptr.is_null() {
|
||||
return Err("GlobalLock 失败".to_string());
|
||||
}
|
||||
std::ptr::copy_nonoverlapping(dib.as_ptr(), ptr, dib.len());
|
||||
GlobalUnlock(hglob);
|
||||
if SetClipboardData(CF_DIB as u32, hglob as isize) == 0 {
|
||||
return Err("SetClipboardData 失败".to_string());
|
||||
}
|
||||
Ok(())
|
||||
})();
|
||||
CloseClipboard();
|
||||
r
|
||||
}
|
||||
}
|
||||
|
||||
/// 将 PNG base64 写入文件
|
||||
pub fn save_png_to_file(png_base64: &str, path: &str) -> Result<(), String> {
|
||||
let png = base64_decode(png_base64).ok_or("base64 解码失败")?;
|
||||
std::fs::write(path, &png).map_err(|e| format!("写入文件失败: {}", e))
|
||||
}
|
||||
|
||||
pub fn base64_encode(b: &[u8]) -> String {
|
||||
use base64::engine::general_purpose::STANDARD;
|
||||
use base64::Engine as _;
|
||||
STANDARD.encode(b)
|
||||
}
|
||||
|
||||
fn base64_decode(s: &str) -> Option<Vec<u8>> {
|
||||
use base64::engine::general_purpose::STANDARD;
|
||||
use base64::Engine as _;
|
||||
STANDARD.decode(s).ok()
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
//! Tauri 命令:截图模块
|
||||
//!
|
||||
//! 命令清单:
|
||||
//! - screenshot_capture_fullscreen:捕获虚拟屏并存入静态,返回元数据 + base64
|
||||
//! - screenshot_take_fullscreen:取出并清除静态存储的全屏捕获
|
||||
//! - screenshot_crop_stored:按物理像素裁剪已存储的全屏捕获
|
||||
//! - screenshot_window_from_point:拾取指定屏幕坐标下的顶层窗口
|
||||
//! - screenshot_enum_windows:枚举可见顶层窗口
|
||||
//! - screenshot_capture_window:按 hwnd 捕获指定窗口
|
||||
//! - screenshot_set_editor_image / screenshot_get_editor_image:编辑器图片传递
|
||||
//! - screenshot_copy_image:写入剪贴板(CF_DIB)
|
||||
//! - screenshot_save_png:写入文件
|
||||
|
||||
use super::{CaptureData, WindowInfo};
|
||||
|
||||
/// 捕获整个虚拟屏(多显示器拼接),存入静态供后续裁剪,并返回 base64
|
||||
#[tauri::command]
|
||||
pub async fn screenshot_capture_fullscreen() -> Result<CaptureData, String> {
|
||||
#[cfg(windows)]
|
||||
{
|
||||
// 屏幕捕获涉及 GDI 调用,放线程池避免阻塞 async 调度
|
||||
let img = tauri::async_runtime::spawn_blocking(|| {
|
||||
super::capture::capture_virtual_screen()
|
||||
})
|
||||
.await
|
||||
.map_err(|e| format!("捕获任务失败: {}", e))??;
|
||||
Ok(super::capture::store_fullscreen(img))
|
||||
}
|
||||
#[cfg(not(windows))]
|
||||
{
|
||||
Err("截图仅支持 Windows".into())
|
||||
}
|
||||
}
|
||||
|
||||
/// 取出并清除静态存储的全屏捕获(覆盖层取消时清理用)
|
||||
#[tauri::command]
|
||||
pub async fn screenshot_take_fullscreen() -> Result<Option<CaptureData>, String> {
|
||||
#[cfg(windows)]
|
||||
{
|
||||
Ok(super::capture::take_fullscreen())
|
||||
}
|
||||
#[cfg(not(windows))]
|
||||
{
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
/// 按物理像素坐标裁剪已存储的全屏捕获
|
||||
#[tauri::command]
|
||||
pub async fn screenshot_crop_stored(
|
||||
x: i32,
|
||||
y: i32,
|
||||
w: i32,
|
||||
h: i32,
|
||||
) -> Result<CaptureData, String> {
|
||||
#[cfg(windows)]
|
||||
{
|
||||
tauri::async_runtime::spawn_blocking(move || {
|
||||
super::capture::crop_stored(x, y, w, h)
|
||||
})
|
||||
.await
|
||||
.map_err(|e| format!("裁剪任务失败: {}", e))?
|
||||
}
|
||||
#[cfg(not(windows))]
|
||||
{
|
||||
let _ = (x, y, w, h);
|
||||
Err("截图仅支持 Windows".into())
|
||||
}
|
||||
}
|
||||
|
||||
/// 拾取指定物理屏幕坐标下的顶层窗口
|
||||
#[tauri::command]
|
||||
pub async fn screenshot_window_from_point(
|
||||
x: i32,
|
||||
y: i32,
|
||||
) -> Result<Option<WindowInfo>, String> {
|
||||
#[cfg(windows)]
|
||||
{
|
||||
tauri::async_runtime::spawn_blocking(move || {
|
||||
Ok(super::capture::window_from_point(x, y))
|
||||
})
|
||||
.await
|
||||
.map_err(|e| format!("查询失败: {}", e))?
|
||||
}
|
||||
#[cfg(not(windows))]
|
||||
{
|
||||
let _ = (x, y);
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
/// 枚举所有可见顶层窗口
|
||||
#[tauri::command]
|
||||
pub async fn screenshot_enum_windows() -> Result<Vec<WindowInfo>, String> {
|
||||
#[cfg(windows)]
|
||||
{
|
||||
tauri::async_runtime::spawn_blocking(|| super::capture::enum_visible_windows())
|
||||
.await
|
||||
.map_err(|e| format!("枚举失败: {}", e))
|
||||
}
|
||||
#[cfg(not(windows))]
|
||||
{
|
||||
Ok(vec![])
|
||||
}
|
||||
}
|
||||
|
||||
/// 按 hwnd 捕获指定窗口
|
||||
#[tauri::command]
|
||||
pub async fn screenshot_capture_window(hwnd: isize) -> Result<CaptureData, String> {
|
||||
#[cfg(windows)]
|
||||
{
|
||||
tauri::async_runtime::spawn_blocking(move || {
|
||||
let img = super::capture::capture_window(hwnd)?;
|
||||
Ok(super::CaptureData {
|
||||
png_base64: super::capture::base64_encode(&img.png),
|
||||
width: img.width,
|
||||
height: img.height,
|
||||
})
|
||||
})
|
||||
.await
|
||||
.map_err(|e| format!("捕获任务失败: {}", e))?
|
||||
}
|
||||
#[cfg(not(windows))]
|
||||
{
|
||||
let _ = hwnd;
|
||||
Err("截图仅支持 Windows".into())
|
||||
}
|
||||
}
|
||||
|
||||
/// 存入编辑器图片(base64 PNG)
|
||||
#[tauri::command]
|
||||
pub async fn screenshot_set_editor_image(png_base64: String) -> Result<(), String> {
|
||||
super::set_editor_image(png_base64);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 取出编辑器图片(编辑器窗口加载时调用,取出即清除)
|
||||
#[tauri::command]
|
||||
pub async fn screenshot_get_editor_image() -> Result<Option<String>, String> {
|
||||
Ok(super::take_editor_image())
|
||||
}
|
||||
|
||||
/// 将 PNG base64 写入系统剪贴板(转 CF_DIB)
|
||||
#[tauri::command]
|
||||
pub async fn screenshot_copy_image(png_base64: String) -> Result<(), String> {
|
||||
#[cfg(windows)]
|
||||
{
|
||||
tauri::async_runtime::spawn_blocking(move || {
|
||||
super::capture::copy_png_to_clipboard(&png_base64)
|
||||
})
|
||||
.await
|
||||
.map_err(|e| format!("剪贴板任务失败: {}", e))?
|
||||
}
|
||||
#[cfg(not(windows))]
|
||||
{
|
||||
let _ = png_base64;
|
||||
Err("剪贴板仅支持 Windows".into())
|
||||
}
|
||||
}
|
||||
|
||||
/// 将 PNG base64 写入文件
|
||||
#[tauri::command]
|
||||
pub async fn screenshot_save_png(png_base64: String, path: String) -> Result<(), String> {
|
||||
tauri::async_runtime::spawn_blocking(move || {
|
||||
#[cfg(windows)]
|
||||
{
|
||||
super::capture::save_png_to_file(&png_base64, &path)
|
||||
}
|
||||
#[cfg(not(windows))]
|
||||
{
|
||||
let _ = (png_base64, path);
|
||||
Err("文件写入仅支持 Windows".to_string())
|
||||
}
|
||||
})
|
||||
.await
|
||||
.map_err(|e| format!("保存任务失败: {}", e))?
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
//! 截图模块
|
||||
//!
|
||||
//! 跨平台数据结构定义 + Windows 捕获实现(capture.rs)。
|
||||
//! 捕获引擎选型见 capture.rs 顶部说明:BitBlt + PrintWindow,不使用 WGC。
|
||||
|
||||
use std::sync::Mutex;
|
||||
|
||||
#[cfg(windows)]
|
||||
pub mod capture;
|
||||
pub mod commands;
|
||||
|
||||
/// 前端可见的捕获数据
|
||||
#[derive(serde::Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CaptureData {
|
||||
pub png_base64: String,
|
||||
pub width: i32,
|
||||
pub height: i32,
|
||||
}
|
||||
|
||||
/// 窗口信息(窗口拾取 / 枚举)
|
||||
#[derive(serde::Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct WindowInfo {
|
||||
pub hwnd: isize,
|
||||
pub title: String,
|
||||
pub rect: ScreenRect,
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, Clone, Copy)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ScreenRect {
|
||||
pub x: i32,
|
||||
pub y: i32,
|
||||
pub width: i32,
|
||||
pub height: i32,
|
||||
}
|
||||
|
||||
/// 编辑器图片静态存储(覆盖层裁剪后存入 → 编辑器窗口加载取出)
|
||||
static EDITOR_IMAGE: Mutex<Option<String>> = Mutex::new(None);
|
||||
|
||||
/// 存储编辑器图片(base64 PNG)
|
||||
pub fn set_editor_image(png_base64: String) {
|
||||
if let Ok(mut g) = EDITOR_IMAGE.lock() {
|
||||
*g = Some(png_base64);
|
||||
}
|
||||
}
|
||||
|
||||
/// 取出并清除编辑器图片
|
||||
pub fn take_editor_image() -> Option<String> {
|
||||
EDITOR_IMAGE.lock().ok()?.take()
|
||||
}
|
||||
Reference in New Issue
Block a user