截图模块调整
This commit is contained in:
@@ -3,7 +3,7 @@
|
||||
//! 实现:
|
||||
//! - 全屏(虚拟屏)捕获:BitBlt 从屏幕 DC 拷贝到兼容位图,GetDIBits 取像素
|
||||
//! - 窗口捕获:PrintWindow(PW_RENDERFULLCONTENT) 捕获 DWM 内容(覆盖硬件加速窗口)
|
||||
//! - 窗口拾取:WindowFromPoint + GetAncestor(GA_ROOT) 取顶层窗口
|
||||
//! - 窗口拾取:EnumWindows 按 Z 序命中测试(排除本进程窗口,避免命中覆盖层自身)
|
||||
//! - 顶层窗口枚举:EnumWindows
|
||||
//! - 像素 → PNG / CF_DIB 转换
|
||||
//!
|
||||
@@ -15,7 +15,7 @@
|
||||
//! 直接编码 PNG 会得到全透明图,故强制不透明。
|
||||
|
||||
use std::sync::Mutex;
|
||||
use windows_sys::Win32::Foundation::{HWND, POINT, RECT};
|
||||
use windows_sys::Win32::Foundation::{BOOL, 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,
|
||||
@@ -28,8 +28,9 @@ use windows_sys::Win32::System::DataExchange::{
|
||||
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,
|
||||
EnumWindows, GetCursorPos, GetSystemMetrics, GetWindowLongPtrW, GetWindowRect,
|
||||
GetWindowTextLengthW, GetWindowTextW, GetWindowThreadProcessId, GWL_EXSTYLE,
|
||||
WS_EX_TOOLWINDOW,
|
||||
};
|
||||
|
||||
use super::{CaptureData, ScreenRect, WindowInfo};
|
||||
@@ -64,9 +65,54 @@ const SM_CYVIRTUALSCREEN: i32 = 79;
|
||||
const PW_RENDERFULLCONTENT: u32 = 0x00000002;
|
||||
const BI_RGB: u32 = 0;
|
||||
|
||||
/// 将 BGRA top-down 像素编码为 BMP(强制 alpha=255)
|
||||
///
|
||||
/// 用于覆盖层/编辑器快速显示:相比 PNG 编码几乎零 CPU 开销,
|
||||
/// Chromium(WebView2)原生支持 top-down 32bpp BMP。
|
||||
fn bgra_to_bmp(bgra: &[u8], width: i32, height: i32) -> Result<Vec<u8>, String> {
|
||||
if width <= 0 || height <= 0 {
|
||||
return Err("无效尺寸".into());
|
||||
}
|
||||
let w = width as usize;
|
||||
let h = height as usize;
|
||||
let data_size = w * h * 4;
|
||||
let file_size = 14 + 40 + data_size;
|
||||
|
||||
// BitBlt 取出的 alpha 通道未定义(常为 0),强制不透明
|
||||
let mut pixels = bgra.to_vec();
|
||||
for a in pixels.iter_mut().skip(3).step_by(4) {
|
||||
*a = 255;
|
||||
}
|
||||
|
||||
let mut bmp = Vec::with_capacity(file_size);
|
||||
// BITMAPFILEHEADER
|
||||
bmp.extend_from_slice(b"BM");
|
||||
bmp.extend_from_slice(&(file_size as u32).to_le_bytes());
|
||||
bmp.extend_from_slice(&0u16.to_le_bytes()); // reserved
|
||||
bmp.extend_from_slice(&0u16.to_le_bytes()); // reserved
|
||||
bmp.extend_from_slice(&54u32.to_le_bytes()); // 像素数据偏移
|
||||
// BITMAPINFOHEADER
|
||||
bmp.extend_from_slice(&40u32.to_le_bytes()); // header size
|
||||
bmp.extend_from_slice(&(width as i32).to_le_bytes());
|
||||
bmp.extend_from_slice(&(-(height as i32)).to_le_bytes()); // 负值 = top-down
|
||||
bmp.extend_from_slice(&1u16.to_le_bytes()); // planes
|
||||
bmp.extend_from_slice(&32u16.to_le_bytes()); // bpp
|
||||
bmp.extend_from_slice(&0u32.to_le_bytes()); // BI_RGB
|
||||
bmp.extend_from_slice(&(data_size as u32).to_le_bytes());
|
||||
bmp.extend_from_slice(&0u32.to_le_bytes()); // x ppm
|
||||
bmp.extend_from_slice(&0u32.to_le_bytes()); // y ppm
|
||||
bmp.extend_from_slice(&0u32.to_le_bytes()); // colors used
|
||||
bmp.extend_from_slice(&0u32.to_le_bytes()); // important colors
|
||||
bmp.extend_from_slice(&pixels);
|
||||
Ok(bmp)
|
||||
}
|
||||
|
||||
/// 将 BGRA top-down 像素编码为 PNG(强制 alpha=255)
|
||||
///
|
||||
/// 使用 Fast 压缩 + 无过滤:历史缩略图/自动保存不需要最优压缩比,
|
||||
/// 大幅降低"点击完成 → 关闭窗口"的编码延迟。
|
||||
fn bgra_to_png(bgra: &[u8], width: i32, height: i32) -> Result<Vec<u8>, String> {
|
||||
use image::codecs::png::PngEncoder;
|
||||
use image::codecs::png::{CompressionType, FilterType, PngEncoder};
|
||||
use image::ImageEncoder;
|
||||
if width <= 0 || height <= 0 {
|
||||
return Err("无效尺寸".into());
|
||||
@@ -81,9 +127,13 @@ fn bgra_to_png(bgra: &[u8], width: i32, height: i32) -> Result<Vec<u8>, String>
|
||||
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))?;
|
||||
PngEncoder::new_with_quality(
|
||||
&mut buf,
|
||||
CompressionType::Fast,
|
||||
FilterType::NoFilter,
|
||||
)
|
||||
.write_image(&rgba, width as u32, height as u32, image::ExtendedColorType::Rgba8)
|
||||
.map_err(|e| format!("PNG 编码失败: {}", e))?;
|
||||
Ok(buf)
|
||||
}
|
||||
|
||||
@@ -164,13 +214,11 @@ pub fn capture_virtual_screen() -> Result<CapturedImage, String> {
|
||||
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,
|
||||
})
|
||||
extract_pixels(hdc_mem, hbm, w, h).map(|bgra| CapturedImage {
|
||||
width: w,
|
||||
height: h,
|
||||
png: Vec::new(), // 全屏捕获不做 PNG 编码,显示/裁剪走 raw BMP
|
||||
bgra,
|
||||
})
|
||||
};
|
||||
|
||||
@@ -232,34 +280,117 @@ pub fn capture_window(hwnd: isize) -> Result<CapturedImage, String> {
|
||||
/// 获取指定屏幕坐标下的顶层窗口(窗口拾取)
|
||||
///
|
||||
/// 入参 x/y 为物理屏幕坐标(前端需按显示器 scaleFactor 从逻辑坐标换算)。
|
||||
///
|
||||
/// 不能直接用 WindowFromPoint:覆盖层是 alwaysOnTop 全屏窗口,会命中覆盖层自身。
|
||||
/// 改为 EnumWindows 按 Z 序(顶→底)枚举顶层窗口做命中测试,并排除本进程
|
||||
/// (覆盖层/主窗口/编辑器)的窗口,从而取到覆盖层下面的目标窗口。
|
||||
pub fn window_from_point(x: i32, y: i32) -> Option<WindowInfo> {
|
||||
struct PickContext {
|
||||
my_pid: u32,
|
||||
pt: POINT,
|
||||
found: Option<WindowInfo>,
|
||||
}
|
||||
|
||||
extern "system" fn enum_proc(hwnd: HWND, lparam: isize) -> i32 {
|
||||
unsafe {
|
||||
let ctx = &mut *(lparam as *mut PickContext);
|
||||
// 跳过本进程窗口(覆盖层 / 主窗口 / 编辑器等)
|
||||
let mut pid: u32 = 0;
|
||||
GetWindowThreadProcessId(hwnd, &mut pid);
|
||||
if pid == ctx.my_pid {
|
||||
return 1;
|
||||
}
|
||||
if windows_sys::Win32::UI::WindowsAndMessaging::IsWindowVisible(hwnd) == 0 {
|
||||
return 1;
|
||||
}
|
||||
// 跳过工具窗口(如本应用 OSD / 托盘菜单)
|
||||
let ex = GetWindowLongPtrW(hwnd, GWL_EXSTYLE);
|
||||
if ex & (WS_EX_TOOLWINDOW as isize) != 0 {
|
||||
return 1;
|
||||
}
|
||||
let mut rect: RECT = std::mem::zeroed();
|
||||
if GetWindowRect(hwnd, &mut rect) == 0 {
|
||||
return 1;
|
||||
}
|
||||
// 命中测试(物理坐标),Z 序最顶层的第一个命中即为目标
|
||||
let pt = ctx.pt;
|
||||
if pt.x >= rect.left && pt.x < rect.right && pt.y >= rect.top && pt.y < rect.bottom {
|
||||
ctx.found = Some(WindowInfo {
|
||||
hwnd,
|
||||
title: get_window_title(hwnd),
|
||||
rect: ScreenRect::from(rect),
|
||||
visual_rect: extended_frame_bounds(hwnd),
|
||||
});
|
||||
return 0; // 停止枚举
|
||||
}
|
||||
}
|
||||
1
|
||||
}
|
||||
|
||||
let mut ctx = PickContext {
|
||||
my_pid: std::process::id(),
|
||||
pt: POINT { x, y },
|
||||
found: None,
|
||||
};
|
||||
unsafe {
|
||||
let pt = POINT { x, y };
|
||||
let mut hwnd = WindowFromPoint(pt);
|
||||
if hwnd == 0 {
|
||||
return None;
|
||||
EnumWindows(Some(enum_proc), &mut ctx as *mut _ as isize);
|
||||
}
|
||||
ctx.found
|
||||
}
|
||||
|
||||
/// 获取当前鼠标物理屏幕坐标(覆盖层打开时定位初始悬停窗口)
|
||||
pub fn cursor_pos() -> Result<(i32, i32), String> {
|
||||
unsafe {
|
||||
let mut pt: POINT = std::mem::zeroed();
|
||||
if GetCursorPos(&mut pt) == 0 {
|
||||
return Err("GetCursorPos 失败".into());
|
||||
}
|
||||
// 取顶层父窗口(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 {
|
||||
Ok((pt.x, pt.y))
|
||||
}
|
||||
}
|
||||
|
||||
/// DWM 扩展边框矩形(视觉边界)
|
||||
///
|
||||
/// GetWindowRect 对最大化窗口包含屏幕外的隐形缩放边框(各向 7~8px),
|
||||
/// 导致窗口高亮框比实际窗口大一圈;DWMWA_EXTENDED_FRAME_BOUNDS 返回可视边界。
|
||||
fn extended_frame_bounds(hwnd: HWND) -> Option<ScreenRect> {
|
||||
use windows_sys::Win32::Graphics::Dwm::{DwmGetWindowAttribute, DWMWA_EXTENDED_FRAME_BOUNDS};
|
||||
let mut r: RECT = unsafe { std::mem::zeroed() };
|
||||
let hr = unsafe {
|
||||
DwmGetWindowAttribute(
|
||||
hwnd,
|
||||
title: get_window_title(hwnd),
|
||||
rect: srect,
|
||||
})
|
||||
DWMWA_EXTENDED_FRAME_BOUNDS as u32,
|
||||
&mut r as *mut _ as *mut core::ffi::c_void,
|
||||
std::mem::size_of::<RECT>() as u32,
|
||||
)
|
||||
};
|
||||
if hr == 0 {
|
||||
Some(ScreenRect::from(r))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// 禁用指定窗口的显示/隐藏过渡动画
|
||||
///
|
||||
/// Windows 默认对 show/hide 播放"缩放+淡入淡出"动画,截图覆盖层会出现
|
||||
/// "从小变大/从大变小"的过渡;DWMWA_TRANSITIONS_FORCEDISABLED 对该窗口永久关闭过渡。
|
||||
pub fn disable_window_transitions(hwnd: isize) -> Result<(), String> {
|
||||
use windows_sys::Win32::Graphics::Dwm::DwmSetWindowAttribute;
|
||||
const DWMWA_TRANSITIONS_FORCEDISABLED: u32 = 3;
|
||||
let mut disabled: BOOL = 1;
|
||||
let hr = unsafe {
|
||||
DwmSetWindowAttribute(
|
||||
hwnd as HWND,
|
||||
DWMWA_TRANSITIONS_FORCEDISABLED,
|
||||
&mut disabled as *mut _ as *mut core::ffi::c_void,
|
||||
std::mem::size_of::<BOOL>() as u32,
|
||||
)
|
||||
};
|
||||
if hr == 0 {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(format!("DwmSetWindowAttribute 失败: {}", hr))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -292,6 +423,7 @@ pub fn enum_visible_windows() -> Vec<WindowInfo> {
|
||||
hwnd,
|
||||
title,
|
||||
rect: srect,
|
||||
visual_rect: extended_frame_bounds(hwnd),
|
||||
});
|
||||
}
|
||||
1
|
||||
@@ -334,6 +466,67 @@ pub fn crop_stored(x: i32, y: i32, w: i32, h: i32) -> Result<CaptureData, String
|
||||
})
|
||||
}
|
||||
|
||||
/// 裁剪已存储的全屏捕获并直接写入剪贴板(一次 IPC 完成"裁剪+复制")
|
||||
///
|
||||
/// 相比前端 crop_stored → copy_image 两次大 base64 往返:
|
||||
/// 直接从原始 BGRA 构造成 bottom-up DIB,省去 PNG 解码,显著降低"点击完成"延迟。
|
||||
pub fn crop_copy_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 dib = bgra_to_dib(&sub, w, h);
|
||||
write_dib_to_clipboard(&dib)?;
|
||||
let png = bgra_to_png(&sub, w, h)?;
|
||||
Ok(CaptureData {
|
||||
png_base64: base64_encode(&png),
|
||||
width: w,
|
||||
height: h,
|
||||
})
|
||||
}
|
||||
|
||||
/// 将 BGRA top-down 像素构造为 CF_DIB(bottom-up,alpha 强制 255)
|
||||
fn bgra_to_dib(bgra: &[u8], width: i32, height: i32) -> Vec<u8> {
|
||||
let header_size = std::mem::size_of::<BITMAPINFOHEADER>() as u32;
|
||||
let w = width as usize;
|
||||
let h = height as usize;
|
||||
let mut dib = Vec::with_capacity(header_size as usize + w * h * 4);
|
||||
let header = BITMAPINFOHEADER {
|
||||
biSize: header_size,
|
||||
biWidth: width,
|
||||
biHeight: height, // 正值 = bottom-up
|
||||
biPlanes: 1,
|
||||
biBitCount: 32,
|
||||
biCompression: BI_RGB,
|
||||
biSizeImage: (w * h * 4) 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);
|
||||
// 从 top-down 行序倒序拷贝为 bottom-up,alpha 强制 255
|
||||
for y in (0..h).rev() {
|
||||
let row = &bgra[y * w * 4..(y + 1) * w * 4];
|
||||
for x in 0..w {
|
||||
dib.push(row[x * 4]); // B
|
||||
dib.push(row[x * 4 + 1]); // G
|
||||
dib.push(row[x * 4 + 2]); // R
|
||||
dib.push(255); // A
|
||||
}
|
||||
}
|
||||
dib
|
||||
}
|
||||
|
||||
fn crop_bgra(
|
||||
src: &[u8],
|
||||
src_width: usize,
|
||||
@@ -352,30 +545,40 @@ fn crop_bgra(
|
||||
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 store_fullscreen(img: CapturedImage) -> Result<(), String> {
|
||||
FULLSCREEN_CAPTURE
|
||||
.lock()
|
||||
.map(|mut g| *g = Some(img))
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
/// 取出并清除全屏捕获
|
||||
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),
|
||||
/// 取出全屏捕获的 BMP 字节用于显示(不移除,裁剪仍依赖原始像素)
|
||||
pub fn fullscreen_bmp() -> Result<Vec<u8>, String> {
|
||||
let guard = FULLSCREEN_CAPTURE.lock().map_err(|e| e.to_string())?;
|
||||
let img = guard.as_ref().ok_or("无已存储的全屏捕获")?;
|
||||
bgra_to_bmp(&img.bgra, img.width, img.height)
|
||||
}
|
||||
|
||||
/// 全屏捕获编码为 PNG base64 并移除(全屏截图直接进编辑器时用)
|
||||
pub fn fullscreen_png() -> Result<CaptureData, String> {
|
||||
let mut g = FULLSCREEN_CAPTURE.lock().map_err(|e| e.to_string())?;
|
||||
let img = g.take().ok_or("无已存储的全屏捕获")?;
|
||||
let png = bgra_to_png(&img.bgra, img.width, img.height)?;
|
||||
Ok(CaptureData {
|
||||
png_base64: base64_encode(&png),
|
||||
width: img.width,
|
||||
height: img.height,
|
||||
})
|
||||
}
|
||||
|
||||
/// 清除全屏捕获(覆盖层关闭/取消时释放内存)
|
||||
pub fn clear_fullscreen() {
|
||||
if let Ok(mut g) = FULLSCREEN_CAPTURE.lock() {
|
||||
*g = None;
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 输出:剪贴板 / 文件 =====
|
||||
|
||||
/// 将 PNG base64 解码为像素,构造 CF_DIB 并写入剪贴板
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
//! Tauri 命令:截图模块
|
||||
//!
|
||||
//! 命令清单:
|
||||
//! - screenshot_capture_fullscreen:捕获虚拟屏并存入静态,返回元数据 + base64
|
||||
//! - screenshot_take_fullscreen:取出并清除静态存储的全屏捕获
|
||||
//! - screenshot_capture_fullscreen:捕获虚拟屏并存入静态(不做 PNG 编码)
|
||||
//! - screenshot_get_fullscreen_bmp:取出全屏捕获的 BMP 原始字节(raw IPC,覆盖层显示用,不移除)
|
||||
//! - screenshot_fullscreen_png:全屏捕获编码 PNG base64 并清除(全屏截图进编辑器用)
|
||||
//! - screenshot_clear_fullscreen:清除静态全屏捕获(覆盖层关闭时)
|
||||
//! - screenshot_crop_stored:按物理像素裁剪已存储的全屏捕获
|
||||
//! - screenshot_window_from_point:拾取指定屏幕坐标下的顶层窗口
|
||||
//! - screenshot_enum_windows:枚举可见顶层窗口
|
||||
@@ -10,21 +12,68 @@
|
||||
//! - screenshot_set_editor_image / screenshot_get_editor_image:编辑器图片传递
|
||||
//! - screenshot_copy_image:写入剪贴板(CF_DIB)
|
||||
//! - screenshot_save_png:写入文件
|
||||
//! - screenshot_disable_transitions:禁用窗口显示/隐藏过渡动画(消除覆盖层缩放动画)
|
||||
|
||||
use super::{CaptureData, WindowInfo};
|
||||
use tauri::Manager;
|
||||
|
||||
/// 捕获整个虚拟屏(多显示器拼接),存入静态供后续裁剪,并返回 base64
|
||||
/// 禁用指定窗口(按 label 查找)的显示/隐藏过渡动画,消除覆盖层出现/消失时的缩放动画
|
||||
#[tauri::command]
|
||||
pub async fn screenshot_capture_fullscreen() -> Result<CaptureData, String> {
|
||||
pub async fn screenshot_disable_transitions(
|
||||
app: tauri::AppHandle,
|
||||
label: String,
|
||||
) -> Result<(), String> {
|
||||
#[cfg(windows)]
|
||||
{
|
||||
use raw_window_handle::HasWindowHandle;
|
||||
let win = app
|
||||
.get_webview_window(&label)
|
||||
.ok_or_else(|| format!("窗口不存在: {}", label))?;
|
||||
let handle = win
|
||||
.window_handle()
|
||||
.map_err(|e| format!("获取窗口句柄失败: {}", e))?;
|
||||
match handle.as_raw() {
|
||||
raw_window_handle::RawWindowHandle::Win32(h) => {
|
||||
super::capture::disable_window_transitions(h.hwnd.get() as isize)
|
||||
}
|
||||
_ => Ok(()),
|
||||
}
|
||||
}
|
||||
#[cfg(not(windows))]
|
||||
{
|
||||
let _ = (app, label);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// 注册(或切换)截图全局快捷键。传入空字符串则禁用快捷键。
|
||||
#[tauri::command]
|
||||
pub async fn screenshot_register_shortcut(
|
||||
app: tauri::AppHandle,
|
||||
shortcut: String,
|
||||
) -> Result<(), String> {
|
||||
super::shortcut::register_shortcut(&app, &shortcut)
|
||||
}
|
||||
|
||||
/// 注销截图全局快捷键
|
||||
#[tauri::command]
|
||||
pub async fn screenshot_unregister_shortcut(app: tauri::AppHandle) -> Result<(), String> {
|
||||
super::shortcut::unregister_shortcut(&app);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 捕获整个虚拟屏(多显示器拼接)存入静态,不做 PNG 编码
|
||||
#[tauri::command]
|
||||
pub async fn screenshot_capture_fullscreen() -> Result<(), String> {
|
||||
#[cfg(windows)]
|
||||
{
|
||||
// 屏幕捕获涉及 GDI 调用,放线程池避免阻塞 async 调度
|
||||
let img = tauri::async_runtime::spawn_blocking(|| {
|
||||
super::capture::capture_virtual_screen()
|
||||
tauri::async_runtime::spawn_blocking(|| {
|
||||
let img = super::capture::capture_virtual_screen()?;
|
||||
super::capture::store_fullscreen(img)
|
||||
})
|
||||
.await
|
||||
.map_err(|e| format!("捕获任务失败: {}", e))??;
|
||||
Ok(super::capture::store_fullscreen(img))
|
||||
.map_err(|e| format!("捕获任务失败: {}", e))?
|
||||
}
|
||||
#[cfg(not(windows))]
|
||||
{
|
||||
@@ -32,16 +81,50 @@ pub async fn screenshot_capture_fullscreen() -> Result<CaptureData, String> {
|
||||
}
|
||||
}
|
||||
|
||||
/// 取出并清除静态存储的全屏捕获(覆盖层取消时清理用)
|
||||
/// 取出全屏捕获的 BMP 原始字节(raw IPC → 前端 ArrayBuffer),不移除
|
||||
#[tauri::command]
|
||||
pub async fn screenshot_take_fullscreen() -> Result<Option<CaptureData>, String> {
|
||||
pub async fn screenshot_get_fullscreen_bmp() -> Result<tauri::ipc::Response, String> {
|
||||
#[cfg(windows)]
|
||||
{
|
||||
Ok(super::capture::take_fullscreen())
|
||||
tauri::async_runtime::spawn_blocking(|| {
|
||||
let bmp = super::capture::fullscreen_bmp()?;
|
||||
Ok(tauri::ipc::Response::new(bmp))
|
||||
})
|
||||
.await
|
||||
.map_err(|e| format!("读取捕获失败: {}", e))?
|
||||
}
|
||||
#[cfg(not(windows))]
|
||||
{
|
||||
Ok(None)
|
||||
Err("截图仅支持 Windows".into())
|
||||
}
|
||||
}
|
||||
|
||||
/// 全屏捕获编码为 PNG base64 并清除(全屏截图直接进编辑器)
|
||||
#[tauri::command]
|
||||
pub async fn screenshot_fullscreen_png() -> Result<CaptureData, String> {
|
||||
#[cfg(windows)]
|
||||
{
|
||||
tauri::async_runtime::spawn_blocking(|| super::capture::fullscreen_png())
|
||||
.await
|
||||
.map_err(|e| format!("编码任务失败: {}", e))?
|
||||
}
|
||||
#[cfg(not(windows))]
|
||||
{
|
||||
Err("截图仅支持 Windows".into())
|
||||
}
|
||||
}
|
||||
|
||||
/// 清除静态全屏捕获(覆盖层关闭/取消时释放内存)
|
||||
#[tauri::command]
|
||||
pub async fn screenshot_clear_fullscreen() -> Result<(), String> {
|
||||
#[cfg(windows)]
|
||||
{
|
||||
super::capture::clear_fullscreen();
|
||||
Ok(())
|
||||
}
|
||||
#[cfg(not(windows))]
|
||||
{
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,6 +151,29 @@ pub async fn screenshot_crop_stored(
|
||||
}
|
||||
}
|
||||
|
||||
/// 裁剪已存储的全屏捕获并直接写入剪贴板(一次 IPC 完成"裁剪+复制")
|
||||
#[tauri::command]
|
||||
pub async fn screenshot_crop_copy_stored(
|
||||
x: i32,
|
||||
y: i32,
|
||||
w: i32,
|
||||
h: i32,
|
||||
) -> Result<CaptureData, String> {
|
||||
#[cfg(windows)]
|
||||
{
|
||||
tauri::async_runtime::spawn_blocking(move || {
|
||||
super::capture::crop_copy_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(
|
||||
@@ -89,6 +195,21 @@ pub async fn screenshot_window_from_point(
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取当前鼠标物理屏幕坐标(覆盖层打开时定位初始悬停窗口)
|
||||
#[tauri::command]
|
||||
pub async fn screenshot_cursor_pos() -> Result<(i32, i32), String> {
|
||||
#[cfg(windows)]
|
||||
{
|
||||
tauri::async_runtime::spawn_blocking(super::capture::cursor_pos)
|
||||
.await
|
||||
.map_err(|e| format!("查询任务失败: {}", e))?
|
||||
}
|
||||
#[cfg(not(windows))]
|
||||
{
|
||||
Err("截图仅支持 Windows".into())
|
||||
}
|
||||
}
|
||||
|
||||
/// 枚举所有可见顶层窗口
|
||||
#[tauri::command]
|
||||
pub async fn screenshot_enum_windows() -> Result<Vec<WindowInfo>, String> {
|
||||
|
||||
@@ -8,6 +8,7 @@ use std::sync::Mutex;
|
||||
#[cfg(windows)]
|
||||
pub mod capture;
|
||||
pub mod commands;
|
||||
pub mod shortcut;
|
||||
|
||||
/// 前端可见的捕获数据
|
||||
#[derive(serde::Serialize)]
|
||||
@@ -25,6 +26,8 @@ pub struct WindowInfo {
|
||||
pub hwnd: isize,
|
||||
pub title: String,
|
||||
pub rect: ScreenRect,
|
||||
/// DWM 扩展边框矩形(视觉边界,去掉最大化窗口的隐形缩放边框),命中测试用 rect,高亮用 visual_rect
|
||||
pub visual_rect: Option<ScreenRect>,
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, Clone, Copy)]
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
//! 截图全局快捷键:可自定义注册/注销(默认 Ctrl+Alt+A)。
|
||||
//!
|
||||
//! 与剪贴板快捷弹窗(clipboard::popup)的实现一致:
|
||||
//! 用 `on_shortcut` 为每个快捷键绑定独立处理器,切换时先注销旧的再注册新的。
|
||||
//! 按下时 emit `screenshot-shortcut` 事件,前端 store 监听后触发 startCapture。
|
||||
|
||||
use std::sync::Mutex;
|
||||
use tauri::{AppHandle, Emitter};
|
||||
use tauri_plugin_global_shortcut::{GlobalShortcutExt, Shortcut, ShortcutState};
|
||||
|
||||
/// 当前已注册的快捷键字符串(用于切换时注销旧快捷键)
|
||||
static CURRENT_SHORTCUT: Mutex<Option<String>> = Mutex::new(None);
|
||||
|
||||
/// 解析快捷键字符串为 Shortcut(格式如 "Ctrl+Alt+A"、"Shift+PrintScreen")
|
||||
pub fn parse_shortcut(s: &str) -> Option<Shortcut> {
|
||||
s.trim().parse::<Shortcut>().ok()
|
||||
}
|
||||
|
||||
/// 注册全局快捷键。重复调用会先注销旧快捷键。
|
||||
/// 传入空字符串则仅注销不注册(禁用快捷键)。
|
||||
pub fn register_shortcut(app: &AppHandle, shortcut_str: &str) -> Result<(), String> {
|
||||
unregister_shortcut(app);
|
||||
|
||||
if shortcut_str.trim().is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let shortcut = parse_shortcut(shortcut_str)
|
||||
.ok_or_else(|| format!("无效的快捷键: {}", shortcut_str))?;
|
||||
|
||||
let app_handle = app.clone();
|
||||
app.global_shortcut()
|
||||
.on_shortcut(shortcut, move |_app, _shortcut, event| {
|
||||
// 仅在按下时触发(松开不触发)
|
||||
if event.state == ShortcutState::Pressed {
|
||||
let _ = app_handle.emit("screenshot-shortcut", ());
|
||||
}
|
||||
})
|
||||
.map_err(|e| format!("注册快捷键失败: {}", e))?;
|
||||
|
||||
if let Ok(mut cur) = CURRENT_SHORTCUT.lock() {
|
||||
*cur = Some(shortcut_str.to_string());
|
||||
}
|
||||
eprintln!("[screenshot] 已注册快捷键: {}", shortcut_str);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 注销当前快捷键
|
||||
pub fn unregister_shortcut(app: &AppHandle) {
|
||||
if let Ok(cur) = CURRENT_SHORTCUT.lock() {
|
||||
if let Some(ref s) = *cur {
|
||||
if let Some(shortcut) = parse_shortcut(s) {
|
||||
let _ = app.global_shortcut().unregister(shortcut);
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Ok(mut cur) = CURRENT_SHORTCUT.lock() {
|
||||
*cur = None;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user