截图模块初始化

This commit is contained in:
zhongluofeng
2026-07-31 18:30:55 +08:00
parent 9d8f963cc6
commit 66575c6166
16 changed files with 1011 additions and 481 deletions
+60 -22
View File
@@ -83,30 +83,28 @@ pub fn show_popup(app: &AppHandle) {
let w = 380.0_f64;
let h = 460.0_f64;
// 获取屏幕工作区(物理像素,Per-Monitor DPI V2
let (screen_w, screen_h) = get_work_area().unwrap_or((1920.0, 1080.0));
// 获取光标所在显示器的工作区(物理像素,与 get_cursor_pos 同一坐标系
let (wa_left, wa_top, wa_right, wa_bottom) = get_work_area_at_point(mx, my)
.unwrap_or((0, 0, 1920, 1040));
// 获取 scale factor,将窗口逻辑尺寸换算为物理像素用于边界裁剪
let scale = app
.get_webview_window(POPUP_LABEL)
.as_ref()
.and_then(|w| w.scale_factor().ok())
.or_else(|| {
app.get_webview_window("main")
.and_then(|w| w.scale_factor().ok())
})
.unwrap_or(1.0);
// 获取光标所在显示器的 DPI,将物理坐标转为逻辑坐标(DIP)
let dpi = get_dpi_for_point(mx, my).unwrap_or(96);
let scale = dpi as f64 / 96.0;
let w_phys = (w * scale) as i32;
let h_phys = (h * scale) as i32;
let mx_l = mx as f64 / scale;
let my_l = my as f64 / scale;
let wa_left_l = wa_left as f64 / scale;
let wa_top_l = wa_top as f64 / scale;
let wa_right_l = wa_right as f64 / scale;
let wa_bottom_l = wa_bottom as f64 / scale;
// 物理坐标 clamping
let x = mx.max(0).min(screen_w as i32 - w_phys);
let y = my.max(0).min(screen_h as i32 - h_phys);
// 逻辑坐标 clamping(窗口尺寸 w/h 也是逻辑像素)
let x = mx_l.max(wa_left_l).min(wa_right_l - w);
let y = my_l.max(wa_top_l).min(wa_bottom_l - h);
// 窗口已存在:移动 + 显示 + 请求焦点
if let Some(win) = app.get_webview_window(POPUP_LABEL) {
let _ = win.set_position(tauri::Position::Physical(tauri::PhysicalPosition {
let _ = win.set_position(tauri::Position::Logical(tauri::LogicalPosition {
x,
y,
}));
@@ -126,7 +124,7 @@ pub fn show_popup(app: &AppHandle) {
)
.title("剪贴板")
.inner_size(w, h)
.position(x as f64 / scale, y as f64 / scale)
.position(x, y)
.decorations(false)
.transparent(true)
.shadow(true)
@@ -233,7 +231,7 @@ mod win_api {
use windows_sys::Win32::Foundation::POINT;
use windows_sys::Win32::UI::WindowsAndMessaging::{GetCursorPos, SystemParametersInfoW, SPI_GETWORKAREA};
/// 获取鼠标位置(屏幕坐标,逻辑像素)
/// 获取鼠标位置(屏幕坐标,物理像素)
pub fn get_cursor_pos() -> Option<(i32, i32)> {
let mut pt = POINT { x: 0, y: 0 };
unsafe {
@@ -245,7 +243,7 @@ mod win_api {
}
}
/// 获取屏工作区(排除任务栏,逻辑像素)
/// 获取屏工作区尺寸(排除任务栏,物理像素)
pub fn get_work_area() -> Option<(f64, f64)> {
use windows_sys::Win32::Foundation::RECT;
let mut rect = RECT { left: 0, top: 0, right: 0, bottom: 0 };
@@ -257,12 +255,52 @@ mod win_api {
}
}
}
/// 获取指定点所在显示器的工作区(排除任务栏),返回 (left, top, right, bottom) 物理像素。
/// 使用 MonitorFromPoint 支持多显示器环境。
pub fn get_work_area_at_point(x: i32, y: i32) -> Option<(i32, i32, i32, i32)> {
use windows_sys::Win32::Graphics::Gdi::{
GetMonitorInfoW, MonitorFromPoint, MONITORINFO, MONITOR_DEFAULTTONEAREST,
};
let pt = POINT { x, y };
let hmon = unsafe { MonitorFromPoint(pt, MONITOR_DEFAULTTONEAREST) };
let mut mi: MONITORINFO = unsafe { std::mem::zeroed() };
mi.cbSize = std::mem::size_of::<MONITORINFO>() as u32;
unsafe {
if GetMonitorInfoW(hmon, &mut mi) != 0 {
let rc = mi.rcWork;
Some((rc.left, rc.top, rc.right, rc.bottom))
} else {
None
}
}
}
/// 获取指定点所在显示器的有效 DPI。
/// scale factor = dpi / 96。
pub fn get_dpi_for_point(x: i32, y: i32) -> Option<u32> {
use windows_sys::Win32::Graphics::Gdi::{MonitorFromPoint, MONITOR_DEFAULTTONEAREST};
use windows_sys::Win32::UI::HiDpi::{GetDpiForMonitor, MDT_EFFECTIVE_DPI};
let pt = POINT { x, y };
let hmon = unsafe { MonitorFromPoint(pt, MONITOR_DEFAULTTONEAREST) };
let mut dpi_x: u32 = 0;
let mut dpi_y: u32 = 0;
unsafe {
if GetDpiForMonitor(hmon, MDT_EFFECTIVE_DPI, &mut dpi_x, &mut dpi_y) == 0 {
Some(dpi_x)
} else {
None
}
}
}
}
#[cfg(not(windows))]
mod win_api {
pub fn get_cursor_pos() -> Option<(i32, i32)> { None }
pub fn get_work_area() -> Option<(f64, f64)> { None }
pub fn get_work_area_at_point(_x: i32, _y: i32) -> Option<(i32, i32, i32, i32)> { None }
pub fn get_dpi_for_point(_x: i32, _y: i32) -> Option<u32> { None }
}
pub use win_api::{get_cursor_pos, get_work_area};
pub use win_api::{get_cursor_pos, get_work_area, get_work_area_at_point, get_dpi_for_point};
+20 -3
View File
@@ -232,10 +232,19 @@ pub fn dib_info(dib: &[u8]) -> Option<(u32, u32)> {
Some((width as u32, height.abs() as u32))
}
/// 将 CF_DIB 字节转换为 PNG 字节(支持 24/32bpp BI_RGB)。
/// 将 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;
}
@@ -244,7 +253,14 @@ pub fn dib_to_png(dib: &[u8]) -> Option<Vec<u8>> {
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 {
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 {
@@ -269,7 +285,8 @@ pub fn dib_to_png(dib: &[u8]) -> Option<Vec<u8>> {
rgba[dp] = dib[sp + 2]; // R
rgba[dp + 1] = dib[sp + 1]; // G
rgba[dp + 2] = dib[sp]; // B
rgba[dp + 3] = 255; // A
// 32bpp 保留 alpha 通道(截图工具常用);24bpp 不透明
rgba[dp + 3] = if bpp == 32 { dib[sp + 3] } else { 255 };
}
}
let mut buf = Vec::new();
+20 -3
View File
@@ -9,6 +9,7 @@ mod monitor_kernel;
mod network_monitor;
mod osd_window;
mod process_manager;
mod screenshot;
mod snap_fix;
mod tray_menu;
@@ -43,6 +44,12 @@ use process_manager::{
get_all_process_status, get_process_status, start_monitoring_thread, start_process,
stop_all_processes, stop_process, ProcessManager,
};
use screenshot::commands::{
screenshot_capture_fullscreen, screenshot_capture_window, screenshot_copy_image,
screenshot_crop_stored, screenshot_enum_windows, screenshot_get_editor_image,
screenshot_save_png, screenshot_set_editor_image, screenshot_take_fullscreen,
screenshot_window_from_point,
};
use clipboard::{
ClipboardManager,
clipboard_clear, clipboard_copy_back, clipboard_count, clipboard_delete, clipboard_get_history,
@@ -51,7 +58,7 @@ use clipboard::{
clipboard_set_pinned, clipboard_show_popup, clipboard_show_window, clipboard_start,
clipboard_status, clipboard_stop, clipboard_unregister_shortcut,
};
use tray_menu::{tray_menu_action, tray_menu_hide, tray_menu_show_window};
use tray_menu::{tray_menu_action, tray_menu_hide, tray_menu_ready};
#[tauri::command]
fn greet(name: &str) -> String {
@@ -185,8 +192,18 @@ pub fn run() {
clipboard_paste_to_target,
tray_menu_action,
tray_menu_hide,
tray_menu_show_window,
snap_fix::fix_snap_background
tray_menu_ready,
snap_fix::fix_snap_background,
screenshot_capture_fullscreen,
screenshot_take_fullscreen,
screenshot_crop_stored,
screenshot_window_from_point,
screenshot_enum_windows,
screenshot_capture_window,
screenshot_set_editor_image,
screenshot_get_editor_image,
screenshot_copy_image,
screenshot_save_png
])
.setup(|app| {
// 初始化日志系统,日志目录: {app_data_dir}/logs/
+2 -2
View File
@@ -1101,7 +1101,7 @@ fn clear_system_proxy_windows() -> Result<(), String> {
}
#[cfg(windows)]
fn get_system_proxy_windows() -> bool {
pub(crate) fn get_system_proxy_windows() -> bool {
use winreg::enums::*;
use winreg::RegKey;
let hkcu = RegKey::predef(HKEY_CURRENT_USER);
@@ -1130,7 +1130,7 @@ fn clear_system_proxy_windows() -> Result<(), String> {
Err("系统代理仅支持 Windows".into())
}
#[cfg(not(windows))]
fn get_system_proxy_windows() -> bool {
pub(crate) fn get_system_proxy_windows() -> bool {
false
}
+92 -58
View File
@@ -20,7 +20,11 @@ use tauri::{
/// 记录窗口最后显示时间,用于失焦防抖(避免显示瞬间因焦点未稳定而被立即隐藏)
static LAST_SHOW_TIME: Mutex<Option<Instant>> = Mutex::new(None);
use crate::clipboard::popup::{get_cursor_pos, get_work_area};
/// 保存最近一次右键时计算出的定位参数(逻辑坐标),供 `tray_menu_ready` 使用
/// (x, tray_top_l, wa_top_l, wa_bottom_l, scale)
static LAST_MENU_LAYOUT: Mutex<Option<(f64, f64, f64, f64, f64)>> = Mutex::new(None);
use crate::clipboard::popup::{get_work_area, get_work_area_at_point, get_dpi_for_point};
use crate::clipboard::ClipboardManager;
use crate::download_engine::DownloadEngine;
use crate::mihomo_manager::MihomoManager;
@@ -211,7 +215,8 @@ pub async fn get_tray_menu_state(app: &AppHandle) -> TrayMenuState {
// ===== 显示/隐藏托盘菜单窗口 =====
/// 托盘菜单窗口尺寸(逻辑像素)
const MENU_W: f64 = 260.0;
/// 宽度需与前端 TrayMenu.vue 的 MENU_WIDTH 保持一致,避免 resize 时右对齐错位
const MENU_W: f64 = 220.0;
const MENU_H: f64 = 420.0;
/// 预创建隐藏的托盘菜单窗口(在应用启动时调用)。
@@ -272,26 +277,37 @@ pub fn precreate_tray_menu_window(app: &AppHandle) {
eprintln!("[tray-menu] 菜单窗口已预创建(隐藏渲染)");
}
/// 在鼠标当前位置显示托盘菜单窗口。
/// 窗口不存在则创建并显示,已存在则移动到鼠标位置并显示
pub fn show_tray_menu(app: &AppHandle) {
let (mx, my) = match get_cursor_pos() {
Some(p) => p,
None => return,
};
/// 右键托盘时调用:计算定位参数、发送状态给前端,但不立即显示窗口。
/// 窗口等待前端测量内容高度后调用 `tray_menu_ready` 才显示,确保底部精确对齐托盘图标
///
/// `cursor_pos`: 事件报告的鼠标物理坐标;`tray_rect`: 托盘图标区域(物理像素)。
pub fn show_tray_menu(app: &AppHandle, cursor_pos: (f64, f64), tray_rect: (f64, f64, f64, f64)) {
let (mx, my) = (cursor_pos.0, cursor_pos.1);
let tray_top = tray_rect.1;
// GetCursorPos 在 Per-Monitor DPI V2 下返回物理像素,直接用作 PhysicalPosition
let win = app.get_webview_window(TRAY_MENU_LABEL);
let scale = win.as_ref()
.and_then(|w| w.scale_factor().ok())
.unwrap_or(1.0);
// 获取光标所在显示器的工作区(物理像素)
let (wa_left, wa_top, wa_right, wa_bottom) = get_work_area_at_point(mx as i32, my as i32)
.unwrap_or((0, 0, 1920, 1040));
let w_phys = (MENU_W * scale) as i32;
let h_phys = (MENU_H * scale) as i32;
// 获取光标所在显示器的 DPI,将物理坐标转为逻辑坐标(DIP)
let dpi = get_dpi_for_point(mx as i32, my as i32).unwrap_or(96);
let scale = dpi as f64 / 96.0;
// 托盘位于屏幕右下角,菜单始终出现在鼠标左上方
let x = (mx - w_phys).max(0);
let y = (my - h_phys - 8).max(0);
let mx_l = mx / scale;
let tray_top_l = tray_top / scale;
let wa_left_l = wa_left as f64 / scale;
let wa_right_l = wa_right as f64 / scale;
let wa_top_l = wa_top as f64 / scale;
let wa_bottom_l = wa_bottom as f64 / scale;
// 水平:菜单左边缘对齐鼠标 X(向右延伸),超出右边界则左移
let x = mx_l.max(wa_left_l).min(wa_right_l - MENU_W);
// 保存布局参数,供 tray_menu_ready 使用
{
let mut layout = LAST_MENU_LAYOUT.lock().unwrap();
*layout = Some((x, tray_top_l, wa_top_l, wa_bottom_l, scale));
}
// 记录显示时间,用于失焦防抖
{
@@ -299,42 +315,17 @@ pub fn show_tray_menu(app: &AppHandle) {
*t = Some(Instant::now());
}
// 窗口存在(预创建或之前显示过):移动 + 显示 + 发送状态
if let Some(win) = win {
let _ = win.set_position(tauri::Position::Physical(tauri::PhysicalPosition { x, y }));
let _ = win.show();
let _ = win.set_focus();
let app_clone = app.clone();
tauri::async_runtime::spawn(async move {
let state = get_tray_menu_state(&app_clone).await;
let _ = app_clone.emit("tray-menu-show", state);
});
return;
// 确保窗口存在(兖底)
if app.get_webview_window(TRAY_MENU_LABEL).is_none() {
precreate_tray_menu_window(app);
}
// 兜底:窗口未预创建(理论上不会走到,因为 create_tray_menu 已预创建
precreate_tray_menu_window(app);
if let Some(win) = app.get_webview_window(TRAY_MENU_LABEL) {
let _ = win.set_position(tauri::Position::Physical(tauri::PhysicalPosition { x, y }));
let _ = win.show();
let _ = win.set_focus();
let app_clone = app.clone();
tauri::async_runtime::spawn(async move {
let state = get_tray_menu_state(&app_clone).await;
let _ = app_clone.emit("tray-menu-show", state);
});
}
}
/// 预创建窗口完成后由前端调用(仅触发状态推送,不显示窗口——预创建模式下窗口保持隐藏)。
pub fn show_window(app: &AppHandle) {
if app.get_webview_window(TRAY_MENU_LABEL).is_some() {
let app_clone = app.clone();
tauri::async_runtime::spawn(async move {
let state = get_tray_menu_state(&app_clone).await;
let _ = app_clone.emit("tray-menu-show", state);
});
}
// 发送状态给前端(前端测量内容高度后调用 tray_menu_ready 显示窗口
let app_clone = app.clone();
tauri::async_runtime::spawn(async move {
let state = get_tray_menu_state(&app_clone).await;
let _ = app_clone.emit("tray-menu-show", state);
});
}
/// 隐藏托盘菜单窗口(不销毁,保留复用)
@@ -403,6 +394,11 @@ pub async fn tray_menu_action(
let _ = app.emit("tray:new-download", ());
hide_tray_menu(&app);
}
"screenshot_region" => {
hide_tray_menu(&app);
// 主窗口(隐藏运行中)接收事件后调用 screenshotStore.startCapture('region')
let _ = app.emit("tray:start-screenshot", "region");
}
"settings" => {
if let Some(window) = app.get_webview_window("main") {
window.show().ok();
@@ -433,10 +429,35 @@ pub async fn tray_menu_hide(app: AppHandle) -> Result<(), String> {
Ok(())
}
/// 显示已创建的菜单窗口(前端 onMounted 后调用)
/// 前端测量内容高度后调用:调整窗口大小、精确定位、然后显示。
/// 这样窗口从一开始就是正确尺寸和位置,不会出现间隙。
#[tauri::command]
pub async fn tray_menu_show_window(app: AppHandle) -> Result<(), String> {
show_window(&app);
pub async fn tray_menu_ready(content_height: f64, app: AppHandle) -> Result<(), String> {
let win = app.get_webview_window(TRAY_MENU_LABEL)
.ok_or("tray-menu window not found")?;
let (x, tray_top_l, wa_top_l, wa_bottom_l, _scale) = {
let layout = LAST_MENU_LAYOUT.lock().unwrap();
layout.unwrap_or((0.0, 1040.0, 0.0, 1040.0, 1.0))
};
// 将内容高度限制在合理范围内
let h = content_height.max(100.0).min(520.0);
// 调整窗口尺寸
let _ = win.set_size(tauri::Size::Logical(tauri::LogicalSize {
width: MENU_W,
height: h,
}));
// 垂直:菜单下边缘紧贴托盘图标顶部(向上弹出)
let y = (tray_top_l - h).max(wa_top_l).min(wa_bottom_l - h);
let pos = tauri::Position::Logical(tauri::LogicalPosition { x, y });
let _ = win.set_position(pos);
let _ = win.show();
let _ = win.set_focus();
Ok(())
}
@@ -695,10 +716,23 @@ pub fn create_tray_menu(app: &AppHandle) -> Result<(), tauri::Error> {
}
TrayIconEvent::Click {
button: MouseButton::Right,
position,
rect,
..
} => {
// 右键:显示自定义菜单窗口
show_tray_menu(&app);
// 右键:显示自定义菜单窗口(使用事件中的精确坐标)
let cursor = (position.x, position.y);
// 从 Rect 的 Position/Size 枚举中提取物理像素值
let (rx, ry) = match rect.position {
tauri::Position::Physical(p) => (p.x as f64, p.y as f64),
tauri::Position::Logical(p) => (p.x, p.y),
};
let (_rw, rh) = match rect.size {
tauri::Size::Physical(s) => (s.width as f64, s.height as f64),
tauri::Size::Logical(s) => (s.width, s.height),
};
let tray_r = (rx, ry, _rw, rh);
show_tray_menu(&app, cursor, tray_r);
}
_ => {}
}