diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index a9149d4..ceb8e0b 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -23,6 +23,9 @@ // } // ] + // Features to add to the dev container. More info: https://containers.dev/features. + // "features": {}, + // Use 'forwardPorts' to make a list of ports inside the container available locally. "forwardPorts": [6080, 5901], @@ -33,9 +36,9 @@ "customizations": { "vscode": { "extensions": [ - "wengerk.highlight-bad-chars", "streetsidesoftware.code-spell-checker", - "EditorConfig.EditorConfig", + "wengerk.highlight-bad-chars", + "editorconfig.editorconfig", "tamasfe.even-better-toml", "rust-lang.rust-analyzer" ] diff --git a/Cargo.toml b/Cargo.toml index f0514d1..63912bd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "xcap" -version = "0.1.0" +version = "0.3.1" edition = "2021" description = "XCap is a cross-platform screen capture library written in Rust. It supports Linux (X11, Wayland), MacOS, and Windows. XCap supports screenshot and video recording (WIP)." license = "Apache-2.0" @@ -13,26 +13,34 @@ keywords = ["screen", "monitor", "window", "capture", "image"] [features] vendored = ["dbus/vendored"] +image = ["image/default"] [dependencies] -image = "0.25" +image = { version = "0.25", default-features = false, features = ["png"] } log = "0.4" -sysinfo = "0.33" +scopeguard = "1.2" thiserror = "2.0" [target.'cfg(target_os = "macos")'.dependencies] -core-foundation = "0.10" -core-graphics = "0.24" +objc2 = "0.6" +objc2-app-kit = "0.3" +objc2-core-foundation = "0.3" +objc2-core-graphics = "0.3" +objc2-foundation = "0.3" [target.'cfg(target_os = "windows")'.dependencies] -windows = { version = "0.58", features = [ +widestring = "1.1" +windows = { version = "0.59", features = [ "Win32_Foundation", "Win32_Graphics_Gdi", "Win32_Graphics_Dwm", + "Win32_Devices_Display", + "Win32_System_LibraryLoader", "Win32_UI_WindowsAndMessaging", "Win32_Storage_Xps", "Win32_System_Threading", "Win32_System_ProcessStatus", + "Win32_System_Registry", "Win32_Storage_FileSystem", "Win32_Graphics_Dxgi", "Win32_Graphics_Direct3D", @@ -41,9 +49,9 @@ windows = { version = "0.58", features = [ ] } [target.'cfg(target_os="linux")'.dependencies] +dbus = "0.9" percent-encoding = "2.3" xcb = { version = "1.5", features = ["randr"] } -dbus = { version = "0.9" } [dev-dependencies] fs_extra = "1.3" diff --git a/examples/monitor_capture.rs b/examples/monitor_capture.rs index c7c28f5..bb77d13 100644 --- a/examples/monitor_capture.rs +++ b/examples/monitor_capture.rs @@ -3,11 +3,7 @@ use std::time::Instant; use xcap::Monitor; fn normalized(filename: &str) -> String { - filename - .replace('|', "") - .replace('\\', "") - .replace(':', "") - .replace('/', "") + filename.replace(['|', '\\', ':', '/'], "") } fn main() { diff --git a/examples/monitor_record.rs b/examples/monitor_record.rs index 46b0b78..c5a24b5 100644 --- a/examples/monitor_record.rs +++ b/examples/monitor_record.rs @@ -1,47 +1,30 @@ -use fs_extra::dir; -use std::{ - thread, - time::{Duration, Instant}, -}; +use std::{sync::Arc, thread, time::Duration}; use xcap::Monitor; fn main() { - let monitors = Monitor::all().unwrap(); + let monitor = Monitor::from_point(100, 100).unwrap(); - dir::create_all("target/monitors", true).unwrap(); + let video_recorder = Arc::new(monitor.video_recorder().unwrap()); - let monitor = monitors.get(0).unwrap().clone(); - - let mut i = 0; - let frame = 20; - let start = Instant::now(); - let fps = 1000 / frame; - - loop { - i += 1; - let time = Instant::now(); - let image = monitor.capture_image().unwrap(); - image - .save(format!("target/monitors/monitor-{}.png", i,)) + let video_recorder_clone = video_recorder.clone(); + thread::spawn(move || { + video_recorder_clone + .on_frame(|frame| { + println!("frame: {:?}", frame.width); + Ok(()) + }) .unwrap(); - let sleep_time = fps * i - start.elapsed().as_millis() as i128; - println!( - "sleep_time: {:?} current_step_time: {:?}", - sleep_time, - time.elapsed() - ); - if sleep_time > 0 { - thread::sleep(Duration::from_millis(sleep_time as u64)); - } - - if i >= 900 { - break; - } - } - - println!("time {:?}", start.elapsed()); - let actual_fps = 900 / start.elapsed().as_secs(); - println!("actual fps: {}", actual_fps); - - // ffmpeg -framerate {actual_fps} -i monitor-%d.png -c:v libx264 -pix_fmt yuv420p output.mp4 + }); + + println!("start"); + video_recorder.start().unwrap(); + thread::sleep(Duration::from_secs(2)); + println!("stop"); + video_recorder.stop().unwrap(); + thread::sleep(Duration::from_secs(2)); + println!("start"); + video_recorder.start().unwrap(); + thread::sleep(Duration::from_secs(2)); + println!("stop"); + video_recorder.stop().unwrap(); } diff --git a/examples/window.rs b/examples/window.rs index 2264b57..1e06884 100644 --- a/examples/window.rs +++ b/examples/window.rs @@ -1,25 +1,22 @@ -use std::time::Instant; +use std::thread; use xcap::Window; fn main() { - let start = Instant::now(); + thread::sleep(std::time::Duration::from_secs(3)); + let windows = Window::all().unwrap(); - println!("Window::all() 运行耗时: {:?}", start.elapsed()); - loop { - for window in windows.clone() { - println!( - "Window:\n id: {}\n title: {}\n app_name: {}\n monitor: {:?}\n position: {:?}\n size {:?}\n state {:?}\n", - window.id(), - window.title(), - window.app_name(), + for window in windows.clone() { + println!( + "Window:\n id: {}\n title: {}\n app_name: {}\n pid: {}\n monitor: {:?}\n position: {:?}\n size {:?}\n state {:?}\n", + window.id(), + window.title(), + window.app_name(), + window.pid(), window.current_monitor().name(), - (window.x(), window.y()), + (window.x(), window.y(), window.z()), (window.width(), window.height()), (window.is_minimized(), window.is_maximized(), window.is_focused()) ); - } - - std::thread::sleep(std::time::Duration::from_secs(1)); } } diff --git a/examples/window_capture.rs b/examples/window_capture.rs index fabaa8e..891173d 100644 --- a/examples/window_capture.rs +++ b/examples/window_capture.rs @@ -3,11 +3,7 @@ use std::time::Instant; use xcap::Window; fn normalized(filename: &str) -> String { - filename - .replace('|', "") - .replace('\\', "") - .replace(':', "") - .replace('/', "") + filename.replace(['|', '\\', ':', '/'], "") } fn main() { diff --git a/examples/window_record.rs b/examples/window_record.rs deleted file mode 100644 index 9326fe5..0000000 --- a/examples/window_record.rs +++ /dev/null @@ -1,70 +0,0 @@ -use fs_extra::dir; -use std::{ - thread, - time::{Duration, Instant}, -}; -use xcap::Window; - -fn main() { - let windows = Window::all().unwrap(); - - dir::create_all("target/windows", true).unwrap(); - - let mut i = 0; - for window in &windows { - // 最小化的窗口不能截屏 - if window.is_minimized() { - continue; - } - - if window.title().contains("Chrome") { - break; - } - - println!( - "Window: {:?} {:?} {:?}", - window.title(), - (window.x(), window.y(), window.width(), window.height()), - (window.is_minimized(), window.is_maximized()) - ); - - i += 1; - } - - let mut win = windows.get(i).unwrap().clone(); - println!("{:?}", win); - - let mut i = 0; - let frame = 20; - let start = Instant::now(); - let fps = 1000 / frame; - - loop { - i += 1; - let time = Instant::now(); - win.refresh().unwrap(); - let image = win.capture_image().unwrap(); - image - .save(format!("target/windows/window-{}.png", i,)) - .unwrap(); - let sleep_time = fps * i - start.elapsed().as_millis() as i128; - println!( - "sleep_time: {:?} current_step_time: {:?}", - sleep_time, - time.elapsed() - ); - if sleep_time > 0 { - thread::sleep(Duration::from_millis(sleep_time as u64)); - } - - if i >= 900 { - break; - } - } - - println!("time {:?}", start.elapsed()); - let actual_fps = 900 / start.elapsed().as_secs(); - println!("actual fps: {}", actual_fps); - - // ffmpeg -framerate {actual_fps} -i window-%d.png -c:v libx264 -pix_fmt yuv420p output.mp4 -} diff --git a/examples/windows_monitor_record.rs b/examples/windows_monitor_record.rs deleted file mode 100644 index c5a24b5..0000000 --- a/examples/windows_monitor_record.rs +++ /dev/null @@ -1,30 +0,0 @@ -use std::{sync::Arc, thread, time::Duration}; -use xcap::Monitor; - -fn main() { - let monitor = Monitor::from_point(100, 100).unwrap(); - - let video_recorder = Arc::new(monitor.video_recorder().unwrap()); - - let video_recorder_clone = video_recorder.clone(); - thread::spawn(move || { - video_recorder_clone - .on_frame(|frame| { - println!("frame: {:?}", frame.width); - Ok(()) - }) - .unwrap(); - }); - - println!("start"); - video_recorder.start().unwrap(); - thread::sleep(Duration::from_secs(2)); - println!("stop"); - video_recorder.stop().unwrap(); - thread::sleep(Duration::from_secs(2)); - println!("start"); - video_recorder.start().unwrap(); - thread::sleep(Duration::from_secs(2)); - println!("stop"); - video_recorder.stop().unwrap(); -} diff --git a/src/error.rs b/src/error.rs index cc565f1..49ea772 100644 --- a/src/error.rs +++ b/src/error.rs @@ -32,15 +32,15 @@ pub enum XCapError { StdTimeSystemTimeError(#[from] std::time::SystemTimeError), #[cfg(target_os = "macos")] - #[error("CoreGraphicsDisplayCGError {0}")] - CoreGraphicsDisplayCGError(core_graphics::display::CGError), + #[error("Objc2CoreGraphicsCGError {:?}", 0)] + Objc2CoreGraphicsCGError(objc2_core_graphics::CGError), #[cfg(target_os = "windows")] #[error(transparent)] WindowsCoreError(#[from] windows::core::Error), #[cfg(target_os = "windows")] #[error(transparent)] - StdStringFromUtf16Error(#[from] std::string::FromUtf16Error), + Utf16Error(#[from] widestring::error::Utf16Error), } impl XCapError { @@ -49,12 +49,12 @@ impl XCapError { } } -#[cfg(target_os = "macos")] -impl From for XCapError { - fn from(value: core_graphics::display::CGError) -> Self { - XCapError::CoreGraphicsDisplayCGError(value) - } -} +// #[cfg(target_os = "macos")] +// impl From for XCapError { +// fn from(value: core_graphics::display::CGError) -> Self { +// XCapError::CoreGraphicsDisplayCGError(value) +// } +// } pub type XCapResult = Result; diff --git a/src/linux/impl_window.rs b/src/linux/impl_window.rs index c8fe3f9..1811424 100644 --- a/src/linux/impl_window.rs +++ b/src/linux/impl_window.rs @@ -3,8 +3,8 @@ use std::str; use xcb::{ x::{ Atom, Drawable, GetGeometry, GetProperty, GetPropertyReply, InternAtom, QueryPointer, - TranslateCoordinates, Window, ATOM_ATOM, ATOM_NONE, ATOM_STRING, ATOM_WM_CLASS, - ATOM_WM_NAME, ATOM_WINDOW, + TranslateCoordinates, Window, ATOM_ATOM, ATOM_CARDINAL, ATOM_NONE, ATOM_STRING, + ATOM_WM_CLASS, ATOM_WM_NAME, }, Connection, Xid, }; @@ -19,9 +19,11 @@ pub(crate) struct ImplWindow { pub id: u32, pub title: String, pub app_name: String, + pub pid: u32, pub current_monitor: ImplMonitor, pub x: i32, pub y: i32, + pub z: i32, pub width: u32, pub height: u32, pub is_minimized: bool, @@ -37,7 +39,7 @@ fn get_atom(conn: &Connection, name: &str) -> XCapResult { let atom_reply = conn.wait_for_reply(atom_cookie)?; let atom = atom_reply.atom(); - if atom == ATOM_NONE { + if atom.is_none() { return Err(XCapError::new(format!("{} not supported", name))); } @@ -68,28 +70,52 @@ fn get_window_property( fn get_focused_window(conn: &Connection, root_window: Window) -> XCapResult { let active_window_atom = get_atom(conn, "_NET_ACTIVE_WINDOW")?; - - let active_window_reply = get_window_property( - conn, - root_window, - active_window_atom, - ATOM_WINDOW, - 0, - 1, - )?; - - let active_window = active_window_reply.value::() + + let active_window_reply = + get_window_property(conn, root_window, active_window_atom, ATOM_WINDOW, 0, 1)?; + let active_window = active_window_reply + .value::() .first() .copied() .unwrap_or(Window::none()); - Ok(active_window) } +pub fn get_window_pid(conn: &Connection, window: &Window) -> XCapResult { + let wm_pid_atom = get_atom(conn, "_NET_WM_PID")?; + + let reply = get_window_property(conn, *window, wm_pid_atom, ATOM_CARDINAL, 0, 4)?; + let value = reply.value::(); + + value + .first() + .ok_or(XCapError::new("Get window pid failed")) + .copied() +} + +fn get_active_window_id(conn: &Connection) -> Option { + let active_window_atom = get_atom(conn, "_NET_ACTIVE_WINDOW").ok()?; + let setup = conn.get_setup(); + + for screen in setup.roots() { + let root_window = screen.root(); + let active_window_id = + get_window_property(conn, root_window, active_window_atom, ATOM_NONE, 0, 4).ok()?; + if let Some(&active_window_id) = active_window_id.value::().first() { + return Some(active_window_id); + } + } + + None +} + impl ImplWindow { fn new( conn: &Connection, window: &Window, + pid: u32, + z: i32, + is_focused: bool, impl_monitors: &Vec, ) -> XCapResult { let title = { @@ -190,7 +216,10 @@ impl ImplWindow { let is_focused = { let setup = conn.get_setup(); - let screen = setup.roots().next().ok_or(XCapError::new("No screen found"))?; + let screen = setup + .roots() + .next() + .ok_or(XCapError::new("No screen found"))?; let focused_window = get_focused_window(conn, screen.root())?; focused_window == *window }; @@ -200,9 +229,11 @@ impl ImplWindow { id: window.resource_id(), title, app_name, + pid, current_monitor, x, y, + z, width, height, is_minimized, @@ -216,11 +247,15 @@ impl ImplWindow { let setup = conn.get_setup(); // https://github.com/rust-x-bindings/rust-xcb/blob/main/examples/get_all_windows.rs - let client_list_atom = get_atom(&conn, "_NET_CLIENT_LIST")?; + // https://specifications.freedesktop.org/wm-spec/1.5/ar01s03.html#id-1.4.4 + // list all windows by stacking order + let client_list_atom = get_atom(&conn, "_NET_CLIENT_LIST_STACKING")?; + let active_window_id = get_active_window_id(&conn); let mut impl_windows = Vec::new(); let impl_monitors = ImplMonitor::all()?; + let mut z = -1; for screen in setup.roots() { let root_window = screen.root(); @@ -239,14 +274,35 @@ impl ImplWindow { client_list_atom, ATOM_NONE, 0, - 100, + 1024, ) { Ok(list_window_reply) => list_window_reply, _ => continue, }; for client in list_window_reply.value::() { - if let Ok(impl_window) = ImplWindow::new(&conn, client, &impl_monitors) { + z += 1; + let pid = match get_window_pid(&conn, client) { + Ok(pid) => pid, + err => { + log::error!("{:?}", err); + continue; + } + }; + + let is_focused = { + let setup = conn.get_setup(); + let screen = setup + .roots() + .next() + .ok_or(XCapError::new("No screen found"))?; + let focused_window = get_focused_window(conn, screen.root())?; + focused_window == *window + }; + + if let Ok(impl_window) = + ImplWindow::new(&conn, client, pid, z, is_focused, &impl_monitors) + { impl_windows.push(impl_window); } else { log::error!( @@ -259,31 +315,13 @@ impl ImplWindow { } } + impl_windows.reverse(); + Ok(impl_windows) } } impl ImplWindow { - pub fn refresh(&mut self) -> XCapResult<()> { - let (conn, _) = Connection::connect(None)?; - let impl_monitors = ImplMonitor::all()?; - let impl_window = ImplWindow::new(&conn, &self.window, &impl_monitors)?; - - self.window = impl_window.window; - self.id = impl_window.id; - self.title = impl_window.title; - self.app_name = impl_window.app_name; - self.current_monitor = impl_window.current_monitor; - self.x = impl_window.x; - self.y = impl_window.y; - self.width = impl_window.width; - self.height = impl_window.height; - self.is_minimized = impl_window.is_minimized; - self.is_maximized = impl_window.is_maximized; - self.is_focused = impl_window.is_focused; - - Ok(()) - } pub fn capture_image(&self) -> XCapResult { capture_window(self) } diff --git a/src/macos/boxed.rs b/src/macos/boxed.rs deleted file mode 100644 index aae49e0..0000000 --- a/src/macos/boxed.rs +++ /dev/null @@ -1,31 +0,0 @@ -use core_foundation::{ - array::CFArrayRef, - base::{CFRelease, ToVoid}, -}; -use std::ops::Deref; - -#[derive(Debug)] -pub(super) struct BoxCFArrayRef { - cf_array_ref: CFArrayRef, -} - -impl Deref for BoxCFArrayRef { - type Target = CFArrayRef; - fn deref(&self) -> &Self::Target { - &self.cf_array_ref - } -} - -impl Drop for BoxCFArrayRef { - fn drop(&mut self) { - unsafe { - CFRelease(self.cf_array_ref.to_void()); - } - } -} - -impl BoxCFArrayRef { - pub fn new(cf_array_ref: CFArrayRef) -> Self { - BoxCFArrayRef { cf_array_ref } - } -} diff --git a/src/macos/capture.rs b/src/macos/capture.rs index abe5bbc..ef46a8a 100644 --- a/src/macos/capture.rs +++ b/src/macos/capture.rs @@ -1,9 +1,9 @@ -use core_graphics::{ - display::{kCGWindowImageDefault, CGWindowID, CGWindowListOption}, - geometry::CGRect, - window::create_image, -}; use image::RgbaImage; +use objc2_core_foundation::CGRect; +use objc2_core_graphics::{ + CGDataProviderCopyData, CGImageGetBytesPerRow, CGImageGetDataProvider, CGImageGetHeight, + CGImageGetWidth, CGWindowID, CGWindowImageOption, CGWindowListCreateImage, CGWindowListOption, +}; use crate::error::{XCapError, XCapResult}; @@ -12,26 +12,36 @@ pub fn capture( list_option: CGWindowListOption, window_id: CGWindowID, ) -> XCapResult { - let cg_image = create_image(cg_rect, list_option, window_id, kCGWindowImageDefault) - .ok_or_else(|| XCapError::new(format!("Capture failed {} {:?}", window_id, cg_rect)))?; + unsafe { + let cg_image = CGWindowListCreateImage( + cg_rect, + list_option, + window_id, + CGWindowImageOption::Default, + ); - let width = cg_image.width(); - let height = cg_image.height(); - let bytes = Vec::from(cg_image.data().bytes()); + let width = CGImageGetWidth(cg_image.as_deref()); + let height = CGImageGetHeight(cg_image.as_deref()); + let data_provider = CGImageGetDataProvider(cg_image.as_deref()); + let data = CGDataProviderCopyData(data_provider.as_deref()) + .ok_or_else(|| XCapError::new("Failed to copy data"))? + .to_vec(); + let bytes_per_row = CGImageGetBytesPerRow(cg_image.as_deref()); - // Some platforms e.g. MacOS can have extra bytes at the end of each row. - // See - // https://github.com/nashaofu/xcap/issues/29 - // https://github.com/nashaofu/xcap/issues/38 - let mut buffer = Vec::with_capacity(width * height * 4); - for row in bytes.chunks_exact(cg_image.bytes_per_row()) { - buffer.extend_from_slice(&row[..width * 4]); - } + // Some platforms e.g. MacOS can have extra bytes at the end of each row. + // See + // https://github.com/nashaofu/xcap/issues/29 + // https://github.com/nashaofu/xcap/issues/38 + let mut buffer = Vec::with_capacity(width * height * 4); + for row in data.chunks_exact(bytes_per_row) { + buffer.extend_from_slice(&row[..width * 4]); + } - for bgra in buffer.chunks_exact_mut(4) { - bgra.swap(0, 2); - } + for bgra in buffer.chunks_exact_mut(4) { + bgra.swap(0, 2); + } - RgbaImage::from_raw(width as u32, height as u32, buffer) - .ok_or_else(|| XCapError::new("RgbaImage::from_raw failed")) + RgbaImage::from_raw(width as u32, height as u32, buffer) + .ok_or_else(|| XCapError::new("RgbaImage::from_raw failed")) + } } diff --git a/src/macos/impl_monitor.rs b/src/macos/impl_monitor.rs index 595bf94..323a6f8 100644 --- a/src/macos/impl_monitor.rs +++ b/src/macos/impl_monitor.rs @@ -1,8 +1,13 @@ -use core_graphics::display::{ - kCGNullWindowID, kCGWindowListOptionAll, CGDirectDisplayID, CGDisplay, CGDisplayMode, CGError, - CGPoint, -}; use image::RgbaImage; +use objc2::MainThreadMarker; +use objc2_app_kit::NSScreen; +use objc2_core_foundation::{CGPoint, CGRect}; +use objc2_core_graphics::{ + CGDirectDisplayID, CGDisplayBounds, CGDisplayCopyDisplayMode, CGDisplayIsActive, + CGDisplayIsMain, CGDisplayModeGetPixelWidth, CGDisplayModeGetRefreshRate, CGDisplayRotation, + CGError, CGGetActiveDisplayList, CGGetDisplaysWithPoint, CGWindowListOption, +}; +use objc2_foundation::{NSNumber, NSString}; use crate::error::{XCapError, XCapResult}; @@ -10,7 +15,7 @@ use super::{capture::capture, impl_video_recorder::ImplVideoRecorder}; #[derive(Debug, Clone)] pub(crate) struct ImplMonitor { - pub cg_display: CGDisplay, + pub cg_direct_display_id: CGDirectDisplayID, pub id: u32, pub name: String, pub x: i32, @@ -23,53 +28,90 @@ pub(crate) struct ImplMonitor { pub is_primary: bool, } -#[link(name = "CoreGraphics", kind = "framework")] -extern "C" { - fn CGGetDisplaysWithPoint( - point: CGPoint, - max_displays: u32, - displays: *mut CGDirectDisplayID, - display_count: *mut u32, - ) -> CGError; +fn get_display_friendly_name(display_id: CGDirectDisplayID) -> XCapResult { + let screens = NSScreen::screens(unsafe { MainThreadMarker::new_unchecked() }); + for screen in screens { + let device_description = screen.deviceDescription(); + let screen_number = device_description + .objectForKey(&NSString::from_str("NSScreenNumber")) + .ok_or(XCapError::new("Get NSScreenNumber failed"))?; + + let screen_id = screen_number + .downcast::() + .map_err(|err| XCapError::new(format!("{:?}", err)))? + .unsignedIntValue(); + + if screen_id == display_id { + unsafe { return Ok(screen.localizedName().to_string()) }; + } + } + + Err(XCapError::new(format!( + "Get display {} friendly name failed", + display_id + ))) } impl ImplMonitor { pub(super) fn new(id: CGDirectDisplayID) -> XCapResult { - let cg_display = CGDisplay::new(id); - let screen_num = cg_display.model_number(); - let cg_rect = cg_display.bounds(); - let cg_display_mode = get_cg_display_mode(cg_display)?; - let pixel_width = cg_display_mode.pixel_width(); - let scale_factor = pixel_width as f32 / cg_rect.size.width as f32; - - Ok(ImplMonitor { - cg_display, - id: cg_display.id, - name: format!("Monitor #{screen_num}"), - x: cg_rect.origin.x as i32, - y: cg_rect.origin.y as i32, - width: cg_rect.size.width as u32, - height: cg_rect.size.height as u32, - rotation: cg_display.rotation() as f32, - scale_factor, - frequency: cg_display_mode.refresh_rate() as f32, - is_primary: cg_display.is_main(), - }) + unsafe { + let CGRect { origin, size } = CGDisplayBounds(id); + + let rotation = CGDisplayRotation(id) as f32; + + let display_mode = CGDisplayCopyDisplayMode(id); + let pixel_width = CGDisplayModeGetPixelWidth(display_mode.as_deref()); + let scale_factor = pixel_width as f32 / size.width as f32; + let frequency = CGDisplayModeGetRefreshRate(display_mode.as_deref()) as f32; + let is_primary = CGDisplayIsMain(id); + + Ok(ImplMonitor { + cg_direct_display_id: id, + id, + name: get_display_friendly_name(id).unwrap_or(format!("Unknown Monitor {}", id)), + x: origin.x as i32, + y: origin.y as i32, + width: size.width as u32, + height: size.height as u32, + rotation, + scale_factor, + frequency, + is_primary, + }) + } } pub fn all() -> XCapResult> { - // active vs online https://developer.apple.com/documentation/coregraphics/1454964-cggetonlinedisplaylist?language=objc - let display_ids = CGDisplay::active_displays()?; + let max_displays: u32 = 16; + let mut active_displays: Vec = vec![0; max_displays as usize]; + let mut display_count: u32 = 0; - let mut impl_monitors: Vec = Vec::with_capacity(display_ids.len()); + let cg_error = unsafe { + CGGetActiveDisplayList( + max_displays, + active_displays.as_mut_ptr(), + &mut display_count, + ) + }; - for display_id in display_ids { + if cg_error != CGError::Success { + return Err(XCapError::new(format!( + "CGGetActiveDisplayList failed: {:?}", + cg_error + ))); + } + + active_displays.truncate(display_count as usize); + + let mut impl_monitors = Vec::with_capacity(active_displays.len()); + + for display in active_displays { // 运行过程中,如果遇到显示器插拔,可能会导致调用报错 // 对于报错的情况,就把报错的情况给排除掉 // https://github.com/nashaofu/xcap/issues/118 - if let Ok(impl_monitor) = ImplMonitor::new(display_id) { + if let Ok(impl_monitor) = ImplMonitor::new(display) { impl_monitors.push(impl_monitor); } else { - log::error!("ImplMonitor::new({}) failed", display_id); + log::error!("ImplMonitor::new({}) failed", display); } } @@ -81,6 +123,7 @@ impl ImplMonitor { x: x as f64, y: y as f64, }; + let max_displays: u32 = 16; let mut display_ids: Vec = vec![0; max_displays as usize]; let mut display_count: u32 = 0; @@ -94,43 +137,34 @@ impl ImplMonitor { ) }; - if cg_error != 0 { - return Err(XCapError::CoreGraphicsDisplayCGError(cg_error)); + if cg_error != CGError::Success { + return Err(XCapError::new(format!( + "CGGetDisplaysWithPoint failed: {:?}", + cg_error + ))); } if display_count == 0 { - return Err(XCapError::new("Get displays from point failed")); + return Err(XCapError::new("Monitor not found")); } - let display_id = display_ids - .first() - .ok_or(XCapError::new("Monitor not found"))?; - - let impl_monitor = ImplMonitor::new(*display_id)?; - - if !impl_monitor.cg_display.is_active() { - Err(XCapError::new("Monitor is not active")) + if let Some(&display_id) = display_ids.first() { + if unsafe { !CGDisplayIsActive(display_id) } { + return Err(XCapError::new("Monitor is not active")); + } + println!("display_id: {}", display_id); + ImplMonitor::new(display_id) } else { - Ok(impl_monitor) + Err(XCapError::new("Monitor not found")) } } } -fn get_cg_display_mode(cg_display: CGDisplay) -> XCapResult { - let cg_display_mode = cg_display - .display_mode() - .ok_or_else(|| XCapError::new("Get display mode failed"))?; - - Ok(cg_display_mode) -} - impl ImplMonitor { pub fn capture_image(&self) -> XCapResult { - capture( - self.cg_display.bounds(), - kCGWindowListOptionAll, - kCGNullWindowID, - ) + let cg_rect = unsafe { CGDisplayBounds(self.cg_direct_display_id) }; + + capture(cg_rect, CGWindowListOption::OptionAll, 0) } pub fn video_recorder(&self) -> XCapResult { diff --git a/src/macos/impl_window.rs b/src/macos/impl_window.rs index 1f3f1c8..03a961d 100644 --- a/src/macos/impl_window.rs +++ b/src/macos/impl_window.rs @@ -1,33 +1,31 @@ -use core_foundation::{ - array::{CFArrayGetCount, CFArrayGetValueAtIndex}, - base::{FromVoid, TCFType}, - dictionary::{CFDictionaryGetValue, CFDictionaryRef}, - number::{kCFNumberIntType, CFBooleanGetValue, CFBooleanRef, CFNumberGetValue, CFNumberRef}, - string::CFString, +use std::ffi::c_void; + +use image::RgbaImage; +use objc2_core_foundation::{ + CFArrayGetCount, CFArrayGetValueAtIndex, CFBoolean, CFBooleanGetValue, CFDictionary, + CFDictionaryGetValue, CFNumber, CFNumberGetValue, CFNumberType, CFString, CGPoint, CGRect, + CGSize, }; -use core_graphics::{ - display::{ - kCGWindowListExcludeDesktopElements, kCGWindowListOptionIncludingWindow, - kCGWindowListOptionOnScreenOnly, CGDisplay, CGPoint, CGSize, CGWindowListCopyWindowInfo, - }, - geometry::CGRect, - window::{kCGNullWindowID, kCGWindowSharingNone}, +use objc2_core_graphics::{ + CGDisplayBounds, CGEventCreate, CGEventGetUnflippedLocation, CGEventSourceCreate, + CGEventSourceStateID, CGMainDisplayID, CGRectContainsPoint, CGRectIntersectsRect, + CGRectMakeWithDictionaryRepresentation, CGWindowListCopyWindowInfo, CGWindowListOption, }; -use image::RgbaImage; -use std::ffi::c_void; use crate::{error::XCapResult, XCapError}; -use super::{boxed::BoxCFArrayRef, capture::capture, impl_monitor::ImplMonitor}; +use super::{capture::capture, impl_monitor::ImplMonitor}; #[derive(Debug, Clone)] pub(crate) struct ImplWindow { pub id: u32, pub title: String, pub app_name: String, + pub pid: u32, pub current_monitor: ImplMonitor, pub x: i32, pub y: i32, + pub z: i32, pub width: u32, pub height: u32, pub is_minimized: bool, @@ -37,26 +35,15 @@ pub(crate) struct ImplWindow { unsafe impl Send for ImplWindow {} -#[link(name = "CoreGraphics", kind = "framework")] -extern "C" { - fn CGRectMakeWithDictionaryRepresentation( - dict: CFDictionaryRef, - rect: &mut CGRect, - ) -> CFBooleanRef; - fn CGEventSourceCreate(stateID: i32) -> *mut c_void; - fn CGEventGetUnflippedLocation(event: *mut c_void) -> CGPoint; - fn CGEventCreate(source: *mut c_void) -> *mut c_void; - fn CFRelease(cf: *mut c_void); -} - fn get_cf_dictionary_get_value( - cf_dictionary_ref: CFDictionaryRef, + cf_dictionary: &CFDictionary, key: &str, ) -> XCapResult<*const c_void> { unsafe { - let cf_dictionary_key = CFString::new(key); + let cf_dictionary_key = CFString::from_str(key); + let cf_dictionary_key_ref = cf_dictionary_key.as_ref() as *const CFString; - let value = CFDictionaryGetValue(cf_dictionary_ref, cf_dictionary_key.as_CFTypeRef()); + let value = CFDictionaryGetValue(cf_dictionary, cf_dictionary_key_ref.cast()); if value.is_null() { return Err(XCapError::new(format!( @@ -69,14 +56,14 @@ fn get_cf_dictionary_get_value( } } -fn get_cf_number_u32_value(cf_dictionary_ref: CFDictionaryRef, key: &str) -> XCapResult { +fn get_cf_number_i32_value(cf_dictionary: &CFDictionary, key: &str) -> XCapResult { unsafe { - let cf_number_ref = get_cf_dictionary_get_value(cf_dictionary_ref, key)?; + let cf_number = get_cf_dictionary_get_value(cf_dictionary, key)? as *const CFNumber; - let mut value: u32 = 0; + let mut value: i32 = 0; let is_success = CFNumberGetValue( - cf_number_ref as CFNumberRef, - kCFNumberIntType, + &*cf_number, + CFNumberType::IntType, &mut value as *mut _ as *mut c_void, ); @@ -91,52 +78,40 @@ fn get_cf_number_u32_value(cf_dictionary_ref: CFDictionaryRef, key: &str) -> XCa } } -fn get_cf_string_value(cf_dictionary_ref: CFDictionaryRef, key: &str) -> XCapResult { - let value_ref = get_cf_dictionary_get_value(cf_dictionary_ref, key)?; +fn get_mouse_position() -> CGPoint { + unsafe { + // HIDSystemState = 1 + let source = CGEventSourceCreate(CGEventSourceStateID::HIDSystemState); + let event = CGEventCreate(source.as_deref()); + let position = CGEventGetUnflippedLocation(event.as_deref()); - Ok(unsafe { CFString::from_void(value_ref).to_string() }) + position + } } -fn get_cf_bool_value(cf_dictionary_ref: CFDictionaryRef, key: &str) -> XCapResult { - let value_ref = get_cf_dictionary_get_value(cf_dictionary_ref, key)?; - - Ok(unsafe { CFBooleanGetValue(value_ref as CFBooleanRef) }) +fn get_cf_string_value(cf_dictionary: &CFDictionary, key: &str) -> XCapResult { + let value_ref = get_cf_dictionary_get_value(cf_dictionary, key)? as *const CFString; + let value = unsafe { (*value_ref).to_string() }; + Ok(value) } -fn get_cf_number_i32_value(cf_dictionary_ref: CFDictionaryRef, key: &str) -> XCapResult { - unsafe { - let cf_number_ref = get_cf_dictionary_get_value(cf_dictionary_ref, key)?; - - let mut value: i32 = 0; - let is_success = CFNumberGetValue( - cf_number_ref as CFNumberRef, - kCFNumberIntType, - &mut value as *mut _ as *mut c_void, - ); +fn get_cf_bool_value(cf_dictionary: &CFDictionary, key: &str) -> XCapResult { + let value_ref = get_cf_dictionary_get_value(cf_dictionary, key)? as *const CFBoolean; - if !is_success { - return Err(XCapError::new(format!( - "Get {} CFNumberGetValue failed", - key - ))); - } - - Ok(value) - } + Ok(unsafe { CFBooleanGetValue(&*value_ref) }) } -fn get_window_cg_rect(window_cf_dictionary_ref: CFDictionaryRef) -> XCapResult { +fn get_window_cg_rect(window_cf_dictionary: &CFDictionary) -> XCapResult { unsafe { - let window_bounds_ref = - get_cf_dictionary_get_value(window_cf_dictionary_ref, "kCGWindowBounds")? - as CFDictionaryRef; + let window_bounds = get_cf_dictionary_get_value(window_cf_dictionary, "kCGWindowBounds")? + as *const CFDictionary; let mut cg_rect = CGRect::default(); - let is_success_ref = - CGRectMakeWithDictionaryRepresentation(window_bounds_ref, &mut cg_rect); + let is_success = + CGRectMakeWithDictionaryRepresentation(Some(&*window_bounds), &mut cg_rect); - if is_success_ref.is_null() { + if !is_success { return Err(XCapError::new( "CGRectMakeWithDictionaryRepresentation failed", )); @@ -146,29 +121,20 @@ fn get_window_cg_rect(window_cf_dictionary_ref: CFDictionaryRef) -> XCapResult CGPoint { - unsafe { - // HIDSystemState = 1 - let source = CGEventSourceCreate(1); - let event = CGEventCreate(source); - let position = CGEventGetUnflippedLocation(event); - CFRelease(event); - CFRelease(source); - position - } -} - impl ImplWindow { pub fn new( - window_cf_dictionary_ref: CFDictionaryRef, + window_cf_dictionary: &CFDictionary, impl_monitors: &[ImplMonitor], window_name: String, window_owner_name: String, + z: i32, ) -> XCapResult { - let id = get_cf_number_u32_value(window_cf_dictionary_ref, "kCGWindowNumber")?; - let cg_rect = get_window_cg_rect(window_cf_dictionary_ref)?; + let id = get_cf_number_i32_value(window_cf_dictionary, "kCGWindowNumber")? as u32; + let pid = get_cf_number_i32_value(window_cf_dictionary, "kCGWindowOwnerPID")?; - let primary_monitor = ImplMonitor::new(CGDisplay::main().id)?; + let cg_rect = get_window_cg_rect(window_cf_dictionary)?; + + let primary_monitor = ImplMonitor::new(unsafe { CGMainDisplayID() })?; let (is_maximized, current_monitor) = { // 获取窗口中心点的坐标 @@ -181,9 +147,10 @@ impl ImplWindow { let impl_monitor = impl_monitors .iter() - .find(|impl_monitor| { - let display_bounds = impl_monitor.cg_display.bounds(); - display_bounds.contains(&cg_point) || display_bounds.is_intersects(&cg_rect) + .find(|impl_monitor| unsafe { + let display_bounds = CGDisplayBounds(impl_monitor.cg_direct_display_id); + CGRectContainsPoint(display_bounds, cg_point) + || CGRectIntersectsRect(display_bounds, cg_rect) }) .unwrap_or(&primary_monitor); @@ -195,20 +162,19 @@ impl ImplWindow { }; let is_minimized = - !get_cf_bool_value(window_cf_dictionary_ref, "kCGWindowIsOnscreen")? && !is_maximized; + !get_cf_bool_value(window_cf_dictionary, "kCGWindowIsOnscreen")? && !is_maximized; - let is_focused = { - let mouse_pos = get_mouse_position(); - cg_rect.contains(&mouse_pos) - }; + let is_focused = unsafe { CGRectContainsPoint(cg_rect, get_mouse_position()) }; Ok(ImplWindow { id, title: window_name, app_name: window_owner_name, + pid: pid as u32, current_monitor: current_monitor.clone(), x: cg_rect.origin.x as i32, y: cg_rect.origin.y as i32, + z, width: cg_rect.size.width as u32, height: cg_rect.size.height as u32, is_minimized, @@ -220,35 +186,38 @@ impl ImplWindow { pub fn all() -> XCapResult> { unsafe { let impl_monitors = ImplMonitor::all()?; - let mut impl_windows = Vec::new(); - let box_cf_array_ref = BoxCFArrayRef::new(CGWindowListCopyWindowInfo( - kCGWindowListOptionOnScreenOnly | kCGWindowListExcludeDesktopElements, - kCGNullWindowID, - )); + let mut impl_windows = Vec::new(); - if box_cf_array_ref.is_null() { - return Ok(impl_windows); - } + // CGWindowListCopyWindowInfo 返回窗口顺序为从顶层到最底层 + // 即在前面的窗口在数组前面 + let cf_array = match CGWindowListCopyWindowInfo( + CGWindowListOption::OptionOnScreenOnly | CGWindowListOption::ExcludeDesktopElements, + 0, + ) { + Some(cf_array) => cf_array, + None => return Ok(impl_windows), + }; - let num_windows = CFArrayGetCount(*box_cf_array_ref); + let num_windows = CFArrayGetCount(&cf_array); for i in 0..num_windows { let window_cf_dictionary_ref = - CFArrayGetValueAtIndex(*box_cf_array_ref, i) as CFDictionaryRef; + CFArrayGetValueAtIndex(&cf_array, i) as *const CFDictionary; if window_cf_dictionary_ref.is_null() { continue; } - let window_name = - match get_cf_string_value(window_cf_dictionary_ref, "kCGWindowName") { - Ok(window_name) => window_name, - _ => continue, - }; + let window_cf_dictionary = &*window_cf_dictionary_ref; + + let window_name = match get_cf_string_value(window_cf_dictionary, "kCGWindowName") { + Ok(window_name) => window_name, + _ => continue, + }; let window_owner_name = - match get_cf_string_value(window_cf_dictionary_ref, "kCGWindowOwnerName") { + match get_cf_string_value(window_cf_dictionary, "kCGWindowOwnerName") { Ok(window_owner_name) => window_owner_name, _ => continue, }; @@ -257,29 +226,28 @@ impl ImplWindow { continue; } - let window_sharing_state = match get_cf_number_u32_value( - window_cf_dictionary_ref, - "kCGWindowSharingState", - ) { - Ok(window_sharing_state) => window_sharing_state, - _ => continue, - }; + let window_sharing_state = + match get_cf_number_i32_value(window_cf_dictionary, "kCGWindowSharingState") { + Ok(window_sharing_state) => window_sharing_state as u32, + _ => continue, + }; - if window_sharing_state == kCGWindowSharingNone { + if window_sharing_state == 0 { continue; } if let Ok(impl_window) = ImplWindow::new( - window_cf_dictionary_ref, + window_cf_dictionary, &impl_monitors, window_name.clone(), window_owner_name.clone(), + num_windows as i32 - i as i32 - 1, ) { impl_windows.push(impl_window); } else { log::error!( "ImplWindow::new({:?}, {:?}, {:?}, {:?}) failed", - window_cf_dictionary_ref, + window_cf_dictionary, &impl_monitors, &window_name, &window_owner_name @@ -293,78 +261,13 @@ impl ImplWindow { } impl ImplWindow { - pub fn refresh(&mut self) -> XCapResult<()> { - unsafe { - let impl_monitors = ImplMonitor::all()?; - - let box_cf_array_ref = BoxCFArrayRef::new(CGWindowListCopyWindowInfo( - kCGWindowListOptionOnScreenOnly | kCGWindowListExcludeDesktopElements, - kCGNullWindowID, - )); - - if box_cf_array_ref.is_null() { - return Err(XCapError::new("Run CGWindowListCopyWindowInfo error")); - } - - let num_windows = CFArrayGetCount(*box_cf_array_ref); - - for i in 0..num_windows { - let window_cf_dictionary_ref = - CFArrayGetValueAtIndex(*box_cf_array_ref, i) as CFDictionaryRef; - - if window_cf_dictionary_ref.is_null() { - continue; - } - - let k_cg_window_number = - get_cf_number_u32_value(window_cf_dictionary_ref, "kCGWindowNumber")?; - - if k_cg_window_number == self.id { - let window_name = - match get_cf_string_value(window_cf_dictionary_ref, "kCGWindowName") { - Ok(window_name) => window_name, - _ => return Err(XCapError::new("Get window name failed")), - }; - - let window_owner_name = - match get_cf_string_value(window_cf_dictionary_ref, "kCGWindowOwnerName") { - Ok(window_owner_name) => window_owner_name, - _ => return Err(XCapError::new("Get window owner name failed")), - }; - - let impl_window = ImplWindow::new( - window_cf_dictionary_ref, - &impl_monitors, - window_name, - window_owner_name, - )?; - - self.id = impl_window.id; - self.title = impl_window.title; - self.app_name = impl_window.app_name; - self.current_monitor = impl_window.current_monitor; - self.x = impl_window.x; - self.y = impl_window.y; - self.width = impl_window.width; - self.height = impl_window.height; - self.is_minimized = impl_window.is_minimized; - self.is_maximized = impl_window.is_maximized; - self.is_focused = impl_window.is_focused; - - return Ok(()); - } - } - - Err(XCapError::new("Not Found window")) - } - } pub fn capture_image(&self) -> XCapResult { capture( CGRect::new( - &CGPoint::new(self.x as f64, self.y as f64), - &CGSize::new(self.width as f64, self.height as f64), + CGPoint::new(self.x as f64, self.y as f64), + CGSize::new(self.width as f64, self.height as f64), ), - kCGWindowListOptionIncludingWindow, + CGWindowListOption::OptionIncludingWindow, self.id, ) } diff --git a/src/macos/mod.rs b/src/macos/mod.rs index ff876af..6e5b2d1 100644 --- a/src/macos/mod.rs +++ b/src/macos/mod.rs @@ -1,4 +1,3 @@ -mod boxed; mod capture; pub mod impl_monitor; diff --git a/src/window.rs b/src/window.rs index 29f1817..7e66d55 100644 --- a/src/window.rs +++ b/src/window.rs @@ -14,6 +14,7 @@ impl Window { } impl Window { + /// List all windows, sorted by z coordinate. pub fn all() -> XCapResult> { let windows = ImplWindow::all()? .iter() @@ -37,11 +38,9 @@ impl Window { pub fn title(&self) -> &str { &self.impl_window.title } - - #[cfg(target_os = "windows")] /// The window process id - pub fn process_id(&self) -> u32 { - self.impl_window.process_id + pub fn pid(&self) -> u32 { + self.impl_window.pid } /// The window current monitor pub fn current_monitor(&self) -> Monitor { @@ -55,6 +54,10 @@ impl Window { pub fn y(&self) -> i32 { self.impl_window.y } + /// The window z coordinate. + pub fn z(&self) -> i32 { + self.impl_window.z + } /// The window pixel width. pub fn width(&self) -> u32 { self.impl_window.width @@ -78,9 +81,6 @@ impl Window { } impl Window { - pub fn refresh(&mut self) -> XCapResult<()> { - self.impl_window.refresh() - } pub fn capture_image(&self) -> XCapResult { self.impl_window.capture_image() } diff --git a/src/windows/boxed.rs b/src/windows/boxed.rs deleted file mode 100644 index e579bda..0000000 --- a/src/windows/boxed.rs +++ /dev/null @@ -1,139 +0,0 @@ -use std::{ops::Deref, ptr}; -use windows::{ - core::PCWSTR, - Win32::{ - Foundation::{CloseHandle, GetLastError, HANDLE, HWND}, - Graphics::Gdi::{CreateDCW, DeleteDC, DeleteObject, GetWindowDC, ReleaseDC, HBITMAP, HDC}, - System::Threading::{OpenProcess, PROCESS_ACCESS_RIGHTS}, - }, -}; - -use crate::{XCapError, XCapResult}; - -#[derive(Debug)] -pub(super) struct BoxHDC { - hdc: HDC, - hwnd: Option, -} - -impl Deref for BoxHDC { - type Target = HDC; - fn deref(&self) -> &Self::Target { - &self.hdc - } -} - -impl Drop for BoxHDC { - fn drop(&mut self) { - // ReleaseDC 与 DeleteDC 的区别 - // https://learn.microsoft.com/zh-cn/windows/win32/api/winuser/nf-winuser-releasedc - unsafe { - if let Some(hwnd) = self.hwnd { - if ReleaseDC(hwnd, self.hdc) != 1 { - log::error!("ReleaseDC {:?} failed", self) - } - } else if !DeleteDC(self.hdc).as_bool() { - log::error!("DeleteDC {:?} failed", self) - } - }; - } -} - -impl BoxHDC { - pub fn new(hdc: HDC, hwnd: Option) -> Self { - BoxHDC { hdc, hwnd } - } -} - -impl From<&[u16; 32]> for BoxHDC { - fn from(sz_device: &[u16; 32]) -> Self { - let sz_device_ptr = sz_device.as_ptr(); - - let hdc = unsafe { - CreateDCW( - PCWSTR(sz_device_ptr), - PCWSTR(sz_device_ptr), - PCWSTR(ptr::null()), - None, - ) - }; - - BoxHDC::new(hdc, None) - } -} - -impl From for BoxHDC { - fn from(hwnd: HWND) -> Self { - // GetWindowDC vs GetDC, GetDC 不会绘制窗口边框 - // https://learn.microsoft.com/zh-cn/windows/win32/api/winuser/nf-winuser-getwindowdc - let hdc = unsafe { GetWindowDC(hwnd) }; - - BoxHDC::new(hdc, Some(hwnd)) - } -} - -#[derive(Debug)] -pub(super) struct BoxHBITMAP(HBITMAP); - -impl Deref for BoxHBITMAP { - type Target = HBITMAP; - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl Drop for BoxHBITMAP { - fn drop(&mut self) { - // https://learn.microsoft.com/zh-cn/windows/win32/api/wingdi/nf-wingdi-createcompatiblebitmap - unsafe { - if !DeleteObject(self.0).as_bool() { - log::error!("DeleteObject {:?} failed", self) - } - }; - } -} - -impl BoxHBITMAP { - pub fn new(h_bitmap: HBITMAP) -> Self { - BoxHBITMAP(h_bitmap) - } -} - -#[derive(Debug)] -pub(super) struct BoxProcessHandle(HANDLE); - -impl Deref for BoxProcessHandle { - type Target = HANDLE; - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl Drop for BoxProcessHandle { - fn drop(&mut self) { - unsafe { - CloseHandle(self.0).unwrap_or_else(|_| log::error!("CloseHandle {:?} failed", self)); - }; - } -} - -impl BoxProcessHandle { - pub fn open( - dw_desired_access: PROCESS_ACCESS_RIGHTS, - b_inherit_handle: bool, - dw_process_id: u32, - ) -> XCapResult { - unsafe { - let h_process = OpenProcess(dw_desired_access, b_inherit_handle, dw_process_id)?; - - if h_process.is_invalid() { - return Err(XCapError::new(format!( - "OpenProcess error {:?}", - GetLastError() - ))); - } - - Ok(BoxProcessHandle(h_process)) - } - } -} diff --git a/src/windows/capture.rs b/src/windows/capture.rs index 9740556..e010901 100644 --- a/src/windows/capture.rs +++ b/src/windows/capture.rs @@ -1,13 +1,15 @@ -use image::{DynamicImage, RgbaImage}; use std::{ffi::c_void, mem}; + +use image::{DynamicImage, RgbaImage}; +use scopeguard::guard; use windows::Win32::{ Foundation::HWND, Graphics::{ Dwm::DwmIsCompositionEnabled, Gdi::{ - BitBlt, CreateCompatibleBitmap, CreateCompatibleDC, GetCurrentObject, GetDIBits, - GetObjectW, SelectObject, BITMAP, BITMAPINFO, BITMAPINFOHEADER, DIB_RGB_COLORS, - OBJ_BITMAP, SRCCOPY, + BitBlt, CreateCompatibleBitmap, CreateCompatibleDC, DeleteDC, DeleteObject, + GetCurrentObject, GetDIBits, GetObjectW, GetWindowDC, ReleaseDC, SelectObject, BITMAP, + BITMAPINFO, BITMAPINFOHEADER, DIB_RGB_COLORS, HBITMAP, HDC, OBJ_BITMAP, SRCCOPY, }, }, Storage::Xps::{PrintWindow, PRINT_WINDOW_FLAGS}, @@ -16,14 +18,11 @@ use windows::Win32::{ use crate::error::{XCapError, XCapResult}; -use super::{ - boxed::{BoxHBITMAP, BoxHDC}, - utils::{bgra_to_rgba_image, get_os_major_version}, -}; +use super::utils::{bgra_to_rgba_image, get_os_major_version}; fn to_rgba_image( - box_hdc_mem: BoxHDC, - box_h_bitmap: BoxHBITMAP, + hdc_mem: HDC, + h_bitmap: HBITMAP, width: i32, height: i32, ) -> XCapResult { @@ -46,9 +45,9 @@ fn to_rgba_image( unsafe { // 读取数据到 buffer 中 - let is_success = GetDIBits( - *box_hdc_mem, - *box_h_bitmap, + let is_failed = GetDIBits( + hdc_mem, + h_bitmap, 0, height as u32, Some(buffer.as_mut_ptr().cast()), @@ -56,7 +55,7 @@ fn to_rgba_image( DIB_RGB_COLORS, ) == 0; - if is_success { + if is_failed { return Err(XCapError::new("Get RGBA data failed")); } }; @@ -68,36 +67,51 @@ fn to_rgba_image( pub fn capture_monitor(x: i32, y: i32, width: i32, height: i32) -> XCapResult { unsafe { let hwnd = GetDesktopWindow(); - let box_hdc_desktop_window = BoxHDC::from(hwnd); + let scope_guard_hdc_desktop_window = guard(GetWindowDC(Some(hwnd)), |val| { + if ReleaseDC(Some(hwnd), val) != 1 { + log::error!("ReleaseDC {:?} failed", val) + } + }); // 内存中的HDC,使用 DeleteDC 函数释放 // https://learn.microsoft.com/zh-cn/windows/win32/api/wingdi/nf-wingdi-createcompatibledc - let box_hdc_mem = BoxHDC::new(CreateCompatibleDC(*box_hdc_desktop_window), None); - let box_h_bitmap = BoxHBITMAP::new(CreateCompatibleBitmap( - *box_hdc_desktop_window, - width, - height, - )); + let scope_guard_mem = guard( + CreateCompatibleDC(Some(*scope_guard_hdc_desktop_window)), + |val| { + if !DeleteDC(val).as_bool() { + log::error!("DeleteDC {:?} failed", val) + } + }, + ); + + let scope_guard_h_bitmap = guard( + CreateCompatibleBitmap(*scope_guard_hdc_desktop_window, width, height), + |val| { + if DeleteObject(val.into()).as_bool() { + log::error!("DeleteObject {:?} failed", val); + } + }, + ); // 使用SelectObject函数将这个位图选择到DC中 - SelectObject(*box_hdc_mem, *box_h_bitmap); + SelectObject(*scope_guard_mem, (*scope_guard_h_bitmap).into()); // 拷贝原始图像到内存 // 这里不需要缩放图片,所以直接使用BitBlt // 如需要缩放,则使用 StretchBlt BitBlt( - *box_hdc_mem, + *scope_guard_mem, 0, 0, width, height, - *box_hdc_desktop_window, + Some(*scope_guard_hdc_desktop_window), x, y, SRCCOPY, )?; - to_rgba_image(box_hdc_mem, box_h_bitmap, width, height) + to_rgba_image(*scope_guard_mem, *scope_guard_h_bitmap, width, height) } } @@ -108,13 +122,18 @@ pub fn capture_window( window_info: &WINDOWINFO, ) -> XCapResult { unsafe { - let box_hdc_window: BoxHDC = BoxHDC::from(hwnd); let rc_window = window_info.rcWindow; let mut width = rc_window.right - rc_window.left; let mut height = rc_window.bottom - rc_window.top; - let hgdi_obj = GetCurrentObject(*box_hdc_window, OBJ_BITMAP); + let scope_guard_hdc_window = guard(GetWindowDC(Some(hwnd)), |val| { + if ReleaseDC(Some(hwnd), val) != 1 { + log::error!("ReleaseDC {:?} failed", val) + } + }); + + let hgdi_obj = GetCurrentObject(*scope_guard_hdc_window, OBJ_BITMAP); let mut bitmap = BITMAP::default(); let mut horizontal_scale = 1.0; @@ -135,34 +154,45 @@ pub fn capture_window( // 内存中的HDC,使用 DeleteDC 函数释放 // https://learn.microsoft.com/zh-cn/windows/win32/api/wingdi/nf-wingdi-createcompatibledc - let box_hdc_mem = BoxHDC::new(CreateCompatibleDC(*box_hdc_window), None); - let box_h_bitmap = BoxHBITMAP::new(CreateCompatibleBitmap(*box_hdc_window, width, height)); - - let previous_object = SelectObject(*box_hdc_mem, *box_h_bitmap); + let scope_guard_hdc_mem = guard(CreateCompatibleDC(Some(*scope_guard_hdc_window)), |val| { + if !DeleteDC(val).as_bool() { + log::error!("DeleteDC {:?} failed", val) + } + }); + let scope_guard_h_bitmap = guard( + CreateCompatibleBitmap(*scope_guard_hdc_window, width, height), + |val| { + if DeleteObject(val.into()).as_bool() { + log::error!("DeleteObject {:?} failed", val); + } + }, + ); + + let previous_object = SelectObject(*scope_guard_hdc_mem, (*scope_guard_h_bitmap).into()); let mut is_success = false; // https://webrtc.googlesource.com/src.git/+/refs/heads/main/modules/desktop_capture/win/window_capturer_win_gdi.cc#301 if get_os_major_version() >= 8 { - is_success = PrintWindow(hwnd, *box_hdc_mem, PRINT_WINDOW_FLAGS(2)).as_bool(); + is_success = PrintWindow(hwnd, *scope_guard_hdc_mem, PRINT_WINDOW_FLAGS(2)).as_bool(); } if !is_success && DwmIsCompositionEnabled()?.as_bool() { - is_success = PrintWindow(hwnd, *box_hdc_mem, PRINT_WINDOW_FLAGS(0)).as_bool(); + is_success = PrintWindow(hwnd, *scope_guard_hdc_mem, PRINT_WINDOW_FLAGS(0)).as_bool(); } if !is_success { - is_success = PrintWindow(hwnd, *box_hdc_mem, PRINT_WINDOW_FLAGS(3)).as_bool(); + is_success = PrintWindow(hwnd, *scope_guard_hdc_mem, PRINT_WINDOW_FLAGS(4)).as_bool(); } if !is_success { is_success = BitBlt( - *box_hdc_mem, + *scope_guard_hdc_mem, 0, 0, width, height, - *box_hdc_window, + Some(*scope_guard_hdc_window), 0, 0, SRCCOPY, @@ -170,9 +200,9 @@ pub fn capture_window( .is_ok(); } - SelectObject(*box_hdc_mem, previous_object); + SelectObject(*scope_guard_hdc_mem, previous_object); - let image = to_rgba_image(box_hdc_mem, box_h_bitmap, width, height)?; + let image = to_rgba_image(*scope_guard_hdc_mem, *scope_guard_h_bitmap, width, height)?; let mut rc_client = window_info.rcClient; diff --git a/src/windows/impl_monitor.rs b/src/windows/impl_monitor.rs index cd7ae70..eab78cf 100644 --- a/src/windows/impl_monitor.rs +++ b/src/windows/impl_monitor.rs @@ -1,15 +1,18 @@ +use std::{mem, ptr}; + use image::RgbaImage; -use std::mem; +use scopeguard::guard; use windows::{ - core::PCWSTR, + core::{s, w, HRESULT, PCWSTR}, Win32::{ Foundation::{BOOL, LPARAM, POINT, RECT, TRUE}, Graphics::Gdi::{ - EnumDisplayMonitors, EnumDisplaySettingsW, GetDeviceCaps, GetMonitorInfoW, - MonitorFromPoint, DESKTOPHORZRES, DEVMODEW, DMDO_180, DMDO_270, DMDO_90, DMDO_DEFAULT, - ENUM_CURRENT_SETTINGS, HDC, HMONITOR, HORZRES, MONITORINFO, MONITORINFOEXW, - MONITOR_DEFAULTTONULL, + CreateDCW, DeleteDC, EnumDisplayMonitors, EnumDisplaySettingsW, GetDeviceCaps, + GetMonitorInfoW, MonitorFromPoint, DESKTOPHORZRES, DEVMODEW, DMDO_180, DMDO_270, + DMDO_90, DMDO_DEFAULT, ENUM_CURRENT_SETTINGS, HDC, HMONITOR, HORZRES, MONITORINFO, + MONITORINFOEXW, MONITOR_DEFAULTTONULL, }, + System::{LibraryLoader::GetProcAddress, Threading::GetCurrentProcess}, UI::WindowsAndMessaging::MONITORINFOF_PRIMARY, }, }; @@ -17,8 +20,9 @@ use windows::{ use crate::error::{XCapError, XCapResult}; use super::{ - boxed::BoxHDC, capture::capture_monitor, impl_video_recorder::ImplVideoRecorder, - utils::wide_string_to_string, + capture::capture_monitor, + impl_video_recorder::ImplVideoRecorder, + utils::{get_monitor_name, get_process_is_dpi_awareness, load_library}, }; // A 函数与 W 函数区别 @@ -27,7 +31,7 @@ use super::{ #[derive(Debug, Clone)] pub(crate) struct ImplMonitor { #[allow(unused)] - pub hmonitor: HMONITOR, + pub h_monitor: HMONITOR, #[allow(unused)] pub monitor_info_ex_w: MONITORINFOEXW, pub id: u32, @@ -43,14 +47,14 @@ pub(crate) struct ImplMonitor { } extern "system" fn monitor_enum_proc( - hmonitor: HMONITOR, + h_monitor: HMONITOR, _: HDC, _: *mut RECT, state: LPARAM, ) -> BOOL { unsafe { let state = Box::leak(Box::from_raw(state.0 as *mut Vec)); - state.push(hmonitor); + state.push(h_monitor); TRUE } @@ -70,15 +74,84 @@ fn get_dev_mode_w(monitor_info_exw: &MONITORINFOEXW) -> XCapResult { Ok(dev_mode_w) } +// 定义 GetDpiForMonitor 函数的类型 +type GetDpiForMonitor = unsafe extern "system" fn( + h_monitor: HMONITOR, + dpi_type: u32, + dpi_x: *mut u32, + dpi_y: *mut u32, +) -> HRESULT; + +fn get_hi_dpi_scale_factor(h_monitor: HMONITOR) -> XCapResult { + unsafe { + let current_process_is_dpi_awareness: bool = + get_process_is_dpi_awareness(GetCurrentProcess())?; + + // 当前进程不感知 DPI,则回退到 GetDeviceCaps 获取 DPI + if !current_process_is_dpi_awareness { + return Err(XCapError::new("Process not DPI aware")); + } + + let scope_guard_hmodule = load_library(w!("Shcore.dll"))?; + + let get_dpi_for_monitor_proc_address = + GetProcAddress(*scope_guard_hmodule, s!("GetDpiForMonitor")) + .ok_or(XCapError::new("GetProcAddress GetDpiForMonitor failed"))?; + + let get_dpi_for_monitor: GetDpiForMonitor = + mem::transmute(get_dpi_for_monitor_proc_address); + + let mut dpi_x = 0; + let mut dpi_y = 0; + + // https://learn.microsoft.com/zh-cn/windows/win32/api/shellscalingapi/ne-shellscalingapi-monitor_dpi_type + get_dpi_for_monitor(h_monitor, 0, &mut dpi_x, &mut dpi_y).ok()?; + + Ok(dpi_x as f32 / 96.0) + } +} + +fn get_scale_factor(h_monitor: HMONITOR, monitor_info_ex_w: MONITORINFOEXW) -> XCapResult { + let scale_factor = get_hi_dpi_scale_factor(h_monitor).unwrap_or_else(|err| { + log::info!("{}", err); + // https://learn.microsoft.com/zh-cn/windows/win32/api/wingdi/nf-wingdi-getdevicecaps + unsafe { + let scope_guard_hdc = guard( + CreateDCW( + PCWSTR(monitor_info_ex_w.szDevice.as_ptr()), + PCWSTR(monitor_info_ex_w.szDevice.as_ptr()), + PCWSTR(ptr::null()), + None, + ), + |val| { + if !DeleteDC(val).as_bool() { + log::error!("DeleteDC {:?} failed", val) + } + }, + ); + + let physical_width = GetDeviceCaps(Some(*scope_guard_hdc), DESKTOPHORZRES); + let logical_width = GetDeviceCaps(Some(*scope_guard_hdc), HORZRES); + + physical_width as f32 / logical_width as f32 + } + }); + + Ok(scale_factor) +} + impl ImplMonitor { - pub fn new(hmonitor: HMONITOR) -> XCapResult { + pub fn new(h_monitor: HMONITOR) -> XCapResult { let mut monitor_info_ex_w = MONITORINFOEXW::default(); monitor_info_ex_w.monitorInfo.cbSize = mem::size_of::() as u32; let monitor_info_ex_w_ptr = &mut monitor_info_ex_w as *mut MONITORINFOEXW as *mut MONITORINFO; // https://learn.microsoft.com/zh-cn/windows/win32/api/winuser/nf-winuser-getmonitorinfoa - unsafe { GetMonitorInfoW(hmonitor, monitor_info_ex_w_ptr).ok()? }; + unsafe { GetMonitorInfoW(h_monitor, monitor_info_ex_w_ptr).ok()? }; + + let name = get_monitor_name(monitor_info_ex_w) + .unwrap_or(format!("Unknown Monitor {}", h_monitor.0 as u32)); let dev_mode_w = get_dev_mode_w(&monitor_info_ex_w)?; @@ -96,20 +169,13 @@ impl ImplMonitor { _ => 0.0, }; - let box_hdc_monitor = BoxHDC::from(&monitor_info_ex_w.szDevice); - - let scale_factor = unsafe { - let physical_width = GetDeviceCaps(*box_hdc_monitor, DESKTOPHORZRES); - let logical_width = GetDeviceCaps(*box_hdc_monitor, HORZRES); - - physical_width as f32 / logical_width as f32 - }; + let scale_factor = get_scale_factor(h_monitor, monitor_info_ex_w)?; Ok(ImplMonitor { - hmonitor, + h_monitor, monitor_info_ex_w, - id: hmonitor.0 as u32, - name: wide_string_to_string(&monitor_info_ex_w.szDevice)?, + id: h_monitor.0 as u32, + name, x: dm_position.x, y: dm_position.y, width: dm_pels_width, @@ -124,9 +190,9 @@ impl ImplMonitor { pub fn all() -> XCapResult> { let hmonitors_mut_ptr: *mut Vec = Box::into_raw(Box::default()); - let hmonitors = unsafe { + let h_monitors = unsafe { EnumDisplayMonitors( - HDC::default(), + None, None, Some(monitor_enum_proc), LPARAM(hmonitors_mut_ptr as isize), @@ -135,13 +201,13 @@ impl ImplMonitor { Box::from_raw(hmonitors_mut_ptr) }; - let mut impl_monitors = Vec::with_capacity(hmonitors.len()); + let mut impl_monitors = Vec::with_capacity(h_monitors.len()); - for &hmonitor in hmonitors.iter() { - if let Ok(impl_monitor) = ImplMonitor::new(hmonitor) { + for &h_monitor in h_monitors.iter() { + if let Ok(impl_monitor) = ImplMonitor::new(h_monitor) { impl_monitors.push(impl_monitor); } else { - log::error!("ImplMonitor::new({:?}) failed", hmonitor); + log::error!("ImplMonitor::new({:?}) failed", h_monitor); } } @@ -150,13 +216,13 @@ impl ImplMonitor { pub fn from_point(x: i32, y: i32) -> XCapResult { let point = POINT { x, y }; - let hmonitor = unsafe { MonitorFromPoint(point, MONITOR_DEFAULTTONULL) }; + let h_monitor = unsafe { MonitorFromPoint(point, MONITOR_DEFAULTTONULL) }; - if hmonitor.is_invalid() { + if h_monitor.is_invalid() { return Err(XCapError::new("Not found monitor")); } - ImplMonitor::new(hmonitor) + ImplMonitor::new(h_monitor) } } @@ -166,6 +232,6 @@ impl ImplMonitor { } pub fn video_recorder(&self) -> XCapResult { - ImplVideoRecorder::new(self.hmonitor) + ImplVideoRecorder::new(self.h_monitor) } } diff --git a/src/windows/impl_video_recorder.rs b/src/windows/impl_video_recorder.rs index 727abb3..13973ad 100644 --- a/src/windows/impl_video_recorder.rs +++ b/src/windows/impl_video_recorder.rs @@ -2,19 +2,22 @@ use std::{slice, sync::Arc}; use windows::{ core::Interface, - Win32::Graphics::{ - Direct3D::D3D_DRIVER_TYPE_HARDWARE, - Direct3D11::{ - D3D11CreateDevice, ID3D11Device, ID3D11DeviceContext, ID3D11Resource, ID3D11Texture2D, - D3D11_CPU_ACCESS_READ, D3D11_CREATE_DEVICE_BGRA_SUPPORT, - D3D11_CREATE_DEVICE_SINGLETHREADED, D3D11_MAPPED_SUBRESOURCE, D3D11_MAP_READ, - D3D11_SDK_VERSION, D3D11_TEXTURE2D_DESC, D3D11_USAGE_STAGING, + Win32::{ + Foundation::HMODULE, + Graphics::{ + Direct3D::D3D_DRIVER_TYPE_HARDWARE, + Direct3D11::{ + D3D11CreateDevice, ID3D11Device, ID3D11DeviceContext, ID3D11Resource, + ID3D11Texture2D, D3D11_CPU_ACCESS_READ, D3D11_CREATE_DEVICE_BGRA_SUPPORT, + D3D11_CREATE_DEVICE_SINGLETHREADED, D3D11_MAPPED_SUBRESOURCE, D3D11_MAP_READ, + D3D11_SDK_VERSION, D3D11_TEXTURE2D_DESC, D3D11_USAGE_STAGING, + }, + Dxgi::{ + IDXGIDevice, IDXGIOutput1, IDXGIOutputDuplication, IDXGIResource, + DXGI_ERROR_WAIT_TIMEOUT, DXGI_OUTDUPL_FRAME_INFO, + }, + Gdi::HMONITOR, }, - Dxgi::{ - IDXGIDevice, IDXGIOutput1, IDXGIOutputDuplication, IDXGIResource, - DXGI_ERROR_WAIT_TIMEOUT, DXGI_OUTDUPL_FRAME_INFO, - }, - Gdi::HMONITOR, }, }; @@ -81,13 +84,13 @@ pub struct ImplVideoRecorder { } impl ImplVideoRecorder { - pub fn new(hmonitor: HMONITOR) -> XCapResult { + pub fn new(h_monitor: HMONITOR) -> XCapResult { unsafe { let mut d3d_device = None; D3D11CreateDevice( None, D3D_DRIVER_TYPE_HARDWARE, - None, + HMODULE::default(), D3D11_CREATE_DEVICE_BGRA_SUPPORT | D3D11_CREATE_DEVICE_SINGLETHREADED, None, D3D11_SDK_VERSION, @@ -111,7 +114,7 @@ impl ImplVideoRecorder { let output1 = output.cast::()?; let duplication = output1.DuplicateOutput(&dxgi_device)?; - if output_desc.Monitor == hmonitor { + if output_desc.Monitor == h_monitor { return Ok(Self { d3d_device, d3d_context, diff --git a/src/windows/impl_window.rs b/src/windows/impl_window.rs index c76d73c..7008ffb 100644 --- a/src/windows/impl_window.rs +++ b/src/windows/impl_window.rs @@ -1,10 +1,12 @@ use core::slice; -use image::RgbaImage; use std::{cmp::Ordering, ffi::c_void, mem, ptr}; + +use image::RgbaImage; +use widestring::U16CString; use windows::{ core::{HSTRING, PCWSTR}, Win32::{ - Foundation::{BOOL, HWND, LPARAM, MAX_PATH, RECT, TRUE}, + Foundation::{BOOL, HANDLE, HWND, LPARAM, MAX_PATH, RECT, TRUE}, Graphics::{ Dwm::{DwmGetWindowAttribute, DWMWA_CLOAKED, DWMWA_EXTENDED_FRAME_BOUNDS}, Gdi::{IsRectEmpty, MonitorFromWindow, MONITOR_DEFAULTTONEAREST}, @@ -12,22 +14,25 @@ use windows::{ Storage::FileSystem::{GetFileVersionInfoSizeW, GetFileVersionInfoW, VerQueryValueW}, System::{ ProcessStatus::{GetModuleBaseNameW, GetModuleFileNameExW}, - Threading::{GetCurrentProcessId, PROCESS_ALL_ACCESS}, + Threading::{ + GetCurrentProcess, GetCurrentProcessId, PROCESS_QUERY_LIMITED_INFORMATION, + }, }, UI::WindowsAndMessaging::{ - EnumWindows, GetClassNameW, GetWindowInfo, GetWindowLongPtrW, GetWindowTextLengthW, - GetWindowTextW, GetWindowThreadProcessId, IsIconic, IsWindow, IsWindowVisible, - IsZoomed, GWL_EXSTYLE, WINDOWINFO, WINDOW_EX_STYLE, WS_EX_TOOLWINDOW, + EnumWindows, GetClassNameW, GetForegroundWindow, GetWindowInfo, GetWindowLongPtrW, + GetWindowTextLengthW, GetWindowTextW, GetWindowThreadProcessId, IsIconic, IsWindow, + IsWindowVisible, IsZoomed, GWL_EXSTYLE, WINDOWINFO, WINDOW_EX_STYLE, WS_EX_TOOLWINDOW, }, }, }; -use crate::{ - error::XCapResult, - platform::{boxed::BoxProcessHandle, utils::log_last_error}, -}; +use crate::{error::XCapResult, platform::utils::log_last_error}; -use super::{capture::capture_window, impl_monitor::ImplMonitor, utils::wide_string_to_string}; +use super::{ + capture::capture_window, + impl_monitor::ImplMonitor, + utils::{get_process_is_dpi_awareness, open_process}, +}; #[derive(Debug, Clone)] pub(crate) struct ImplWindow { @@ -37,10 +42,11 @@ pub(crate) struct ImplWindow { pub id: u32, pub title: String, pub app_name: String, - pub process_id: u32, + pub pid: u32, pub current_monitor: ImplMonitor, pub x: i32, pub y: i32, + pub z: i32, pub width: u32, pub height: u32, pub is_minimized: bool, @@ -72,7 +78,7 @@ fn is_window_cloaked(hwnd: HWND) -> bool { fn is_valid_window(hwnd: HWND) -> bool { unsafe { // ignore invisible windows - if !IsWindow(hwnd).as_bool() || !IsWindowVisible(hwnd).as_bool() { + if !IsWindow(Some(hwnd)).as_bool() || !IsWindowVisible(hwnd).as_bool() { return false; } @@ -89,8 +95,9 @@ fn is_valid_window(hwnd: HWND) -> bool { return false; } - let class_name = - wide_string_to_string(&lp_class_name[0..lp_class_name_length]).unwrap_or_default(); + let class_name = U16CString::from_vec_truncate(&lp_class_name[0..lp_class_name_length]) + .to_string() + .unwrap_or_default(); if class_name.is_empty() { return false; } @@ -117,7 +124,7 @@ fn is_valid_window(hwnd: HWND) -> bool { // windows owned by the current process. Consumers should either ensure that // the thread running their message loop never waits on this operation, or use // the option to exclude these windows from the source list. - let lp_dw_process_id = get_process_id(hwnd); + let lp_dw_process_id = get_window_pid(hwnd); if lp_dw_process_id == GetCurrentProcessId() { return false; } @@ -160,12 +167,13 @@ fn is_valid_window(hwnd: HWND) -> bool { } unsafe extern "system" fn enum_windows_proc(hwnd: HWND, state: LPARAM) -> BOOL { - if !is_valid_window(hwnd) { - return TRUE; + let state = Box::leak(Box::from_raw(state.0 as *mut (Vec<(HWND, i32)>, i32))); + + if is_valid_window(hwnd) { + state.0.push((hwnd, state.1)); } - let state = Box::leak(Box::from_raw(state.0 as *mut Vec)); - state.push(hwnd); + state.1 += 1; TRUE } @@ -175,7 +183,9 @@ fn get_window_title(hwnd: HWND) -> XCapResult { let text_length = GetWindowTextLengthW(hwnd); let mut wide_buffer = vec![0u16; (text_length + 1) as usize]; GetWindowTextW(hwnd, &mut wide_buffer); - wide_string_to_string(&wide_buffer) + let window_title = U16CString::from_vec_truncate(wide_buffer).to_string()?; + + Ok(window_title) } } @@ -185,23 +195,25 @@ struct LangCodePage { pub w_code_page: u16, } -fn get_module_basename(box_process_handle: BoxProcessHandle) -> XCapResult { +fn get_module_basename(handle: HANDLE) -> XCapResult { unsafe { // 默认使用 module_basename let mut module_base_name_w = [0; MAX_PATH as usize]; - let result = GetModuleBaseNameW(*box_process_handle, None, &mut module_base_name_w); + let result = GetModuleBaseNameW(handle, None, &mut module_base_name_w); if result == 0 { log_last_error("GetModuleBaseNameW"); - GetModuleFileNameExW(*box_process_handle, None, &mut module_base_name_w); + GetModuleFileNameExW(Some(handle), None, &mut module_base_name_w); } - wide_string_to_string(&module_base_name_w) + let module_basename = U16CString::from_vec_truncate(module_base_name_w).to_string()?; + + Ok(module_basename) } } -fn get_process_id(hwnd: HWND) -> u32 { +fn get_window_pid(hwnd: HWND) -> u32 { unsafe { let mut lp_dw_process_id = 0; GetWindowThreadProcessId(hwnd, Some(&mut lp_dw_process_id)); @@ -209,21 +221,18 @@ fn get_process_id(hwnd: HWND) -> u32 { } } -fn get_app_name(hwnd: HWND) -> XCapResult { +fn get_app_name(pid: u32) -> XCapResult { unsafe { - let lp_dw_process_id = get_process_id(hwnd); - - let box_process_handle = - match BoxProcessHandle::open(PROCESS_ALL_ACCESS, false, lp_dw_process_id) { - Ok(box_handle) => box_handle, - Err(err) => { - log::error!("{}", err); - return Ok(String::new()); - } - }; + let scope_guard_handle = match open_process(PROCESS_QUERY_LIMITED_INFORMATION, false, pid) { + Ok(box_handle) => box_handle, + Err(err) => { + log::error!("{}", err); + return Ok(String::new()); + } + }; let mut filename = [0; MAX_PATH as usize]; - GetModuleFileNameExW(*box_process_handle, None, &mut filename); + GetModuleFileNameExW(Some(*scope_guard_handle), None, &mut filename); let pcw_filename = PCWSTR::from_raw(filename.as_ptr()); @@ -231,14 +240,14 @@ fn get_app_name(hwnd: HWND) -> XCapResult { if file_version_info_size_w == 0 { log_last_error("GetFileVersionInfoSizeW"); - return get_module_basename(box_process_handle); + return get_module_basename(*scope_guard_handle); } let mut file_version_info = vec![0u16; file_version_info_size_w as usize]; GetFileVersionInfoW( pcw_filename, - 0, + None, file_version_info_size_w, file_version_info.as_mut_ptr().cast(), )?; @@ -290,7 +299,7 @@ fn get_app_name(hwnd: HWND) -> XCapResult { } let value = slice::from_raw_parts(value_ptr.cast(), value_length as usize); - let attr = wide_string_to_string(value)?; + let attr = U16CString::from_vec_truncate(value).to_string()?; let attr = attr.trim(); if !attr.is_empty() { @@ -299,16 +308,12 @@ fn get_app_name(hwnd: HWND) -> XCapResult { } } - get_module_basename(box_process_handle) + get_module_basename(*scope_guard_handle) } } -fn is_window_focused(hwnd: HWND) -> bool { - unsafe { GetForegroundWindow() == hwnd } -} - impl ImplWindow { - fn new(hwnd: HWND) -> XCapResult { + fn new(hwnd: HWND, z: i32) -> XCapResult { unsafe { let mut window_info = WINDOWINFO { cbSize: mem::size_of::() as u32, @@ -318,13 +323,14 @@ impl ImplWindow { GetWindowInfo(hwnd, &mut window_info)?; let title = get_window_title(hwnd)?; - let app_name = get_app_name(hwnd)?; + let pid = get_window_pid(hwnd); + let app_name = get_app_name(pid)?; - let hmonitor = MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST); + let h_monitor = MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST); let rc_client = window_info.rcClient; let is_minimized = IsIconic(hwnd).as_bool(); let is_maximized = IsZoomed(hwnd).as_bool(); - let is_focused = is_window_focused(hwnd); + let is_focused = GetForegroundWindow() == hwnd; Ok(ImplWindow { hwnd, @@ -332,10 +338,11 @@ impl ImplWindow { id: hwnd.0 as u32, title, app_name, - process_id: get_process_id(hwnd), - current_monitor: ImplMonitor::new(hmonitor)?, + pid, + current_monitor: ImplMonitor::new(h_monitor)?, x: rc_client.left, y: rc_client.top, + z, width: (rc_client.right - rc_client.left) as u32, height: (rc_client.bottom - rc_client.top) as u32, is_minimized, @@ -346,17 +353,22 @@ impl ImplWindow { } pub fn all() -> XCapResult> { - let hwnds_mut_ptr: *mut Vec = Box::into_raw(Box::default()); + // (HWND, i32) 表示当前窗口以及层级,既(窗口,层级 z),i32 表示 max_z_order,既最大的窗口的 z 顺序 + // 窗口当前层级为 max_z_order - z + let hwnds_mut_ptr: *mut (Vec<(HWND, i32)>, i32) = Box::into_raw(Box::default()); let hwnds = unsafe { + // EnumWindows 函数按照 Z 顺序遍历顶层窗口,从最顶层的窗口开始,依次向下遍历。 EnumWindows(Some(enum_windows_proc), LPARAM(hwnds_mut_ptr as isize))?; Box::from_raw(hwnds_mut_ptr) }; let mut impl_windows = Vec::new(); - for &hwnd in hwnds.iter() { - if let Ok(impl_window) = ImplWindow::new(hwnd) { + let max_z_order = hwnds.1; + + for &(hwnd, z) in hwnds.0.iter() { + if let Ok(impl_window) = ImplWindow::new(hwnd, max_z_order - z) { impl_windows.push(impl_window); } else { log::error!("ImplWindow::new({:?}) failed", hwnd); @@ -368,33 +380,22 @@ impl ImplWindow { } impl ImplWindow { - pub fn refresh(&mut self) -> XCapResult<()> { - let impl_window = ImplWindow::new(self.hwnd)?; - - self.hwnd = impl_window.hwnd; - self.window_info = impl_window.window_info; - self.id = impl_window.id; - self.title = impl_window.title; - self.app_name = impl_window.app_name; - self.process_id = impl_window.process_id; - self.current_monitor = impl_window.current_monitor; - self.x = impl_window.x; - self.y = impl_window.y; - self.width = impl_window.width; - self.height = impl_window.height; - self.is_minimized = impl_window.is_minimized; - self.is_maximized = impl_window.is_maximized; - self.is_focused = is_window_focused(self.hwnd); - - Ok(()) - } - pub fn capture_image(&self) -> XCapResult { - // TODO: 在win10之后,不同窗口有不同的dpi,所以可能存在截图不全或者截图有较大空白,实际窗口没有填充满图片 - capture_window( - self.hwnd, - self.current_monitor.scale_factor, - &self.window_info, - ) + // 在win10之后,不同窗口有不同的dpi,所以可能存在截图不全或者截图有较大空白,实际窗口没有填充满图片 + // 如果窗口不感知dpi,那么就不需要缩放,如果当前进程感知dpi,那么也不需要缩放 + let scope_guard_handle = open_process(PROCESS_QUERY_LIMITED_INFORMATION, false, self.pid)?; + let window_is_dpi_awareness = get_process_is_dpi_awareness(*scope_guard_handle)?; + let current_process_is_dpi_awareness = + unsafe { get_process_is_dpi_awareness(GetCurrentProcess())? }; + + let scale_factor = if !window_is_dpi_awareness { + 1.0 + } else if current_process_is_dpi_awareness { + 1.0 + } else { + self.current_monitor.scale_factor + }; + + capture_window(self.hwnd, scale_factor, &self.window_info) } } diff --git a/src/windows/mod.rs b/src/windows/mod.rs index 992b377..36192a5 100644 --- a/src/windows/mod.rs +++ b/src/windows/mod.rs @@ -1,4 +1,3 @@ -mod boxed; mod capture; mod utils; diff --git a/src/windows/utils.rs b/src/windows/utils.rs index ad968cd..03bf3df 100644 --- a/src/windows/utils.rs +++ b/src/windows/utils.rs @@ -1,26 +1,71 @@ +use std::mem; + use image::RgbaImage; -use sysinfo::System; -use windows::Win32::Foundation::GetLastError; +use scopeguard::{guard, ScopeGuard}; +use widestring::U16CString; +use windows::{ + core::{s, w, HRESULT, PCWSTR}, + Win32::{ + Devices::Display::{ + DisplayConfigGetDeviceInfo, GetDisplayConfigBufferSizes, QueryDisplayConfig, + DISPLAYCONFIG_DEVICE_INFO_GET_SOURCE_NAME, DISPLAYCONFIG_DEVICE_INFO_GET_TARGET_NAME, + DISPLAYCONFIG_DEVICE_INFO_HEADER, DISPLAYCONFIG_MODE_INFO, DISPLAYCONFIG_PATH_INFO, + DISPLAYCONFIG_SOURCE_DEVICE_NAME, DISPLAYCONFIG_TARGET_DEVICE_NAME, + QDC_ONLY_ACTIVE_PATHS, + }, + Foundation::{CloseHandle, FreeLibrary, GetLastError, HANDLE, HMODULE}, + Graphics::Gdi::MONITORINFOEXW, + System::{ + LibraryLoader::{GetProcAddress, LoadLibraryW}, + Registry::{RegGetValueW, HKEY_LOCAL_MACHINE, RRF_RT_REG_SZ}, + Threading::{OpenProcess, PROCESS_ACCESS_RIGHTS}, + }, + }, +}; use crate::{error::XCapResult, XCapError}; -pub(super) fn wide_string_to_string(wide_string: &[u16]) -> XCapResult { - let string = if let Some(null_pos) = wide_string.iter().position(|pos| *pos == 0) { - String::from_utf16(&wide_string[..null_pos])? - } else { - String::from_utf16(wide_string)? - }; +pub(super) fn get_build_number() -> u32 { + unsafe { + let mut buf_len: u32 = 2048; + let mut buf: Vec = Vec::with_capacity(buf_len as usize); + + let err = RegGetValueW( + HKEY_LOCAL_MACHINE, + w!(r"SOFTWARE\Microsoft\Windows NT\CurrentVersion"), + w!("CurrentBuildNumber"), + RRF_RT_REG_SZ, + None, + Some(buf.as_mut_ptr().cast()), + Some(&mut buf_len), + ); + + if err.is_err() { + return 0; + } + + buf.set_len(buf_len as usize); - Ok(string) + let build_version = U16CString::from_vec_truncate(buf) + .to_string() + .unwrap_or_default(); + + build_version.parse().unwrap_or(0) + } } pub(super) fn get_os_major_version() -> u8 { - System::os_version() - .map(|os_version| { - let strs: Vec<&str> = os_version.split(' ').collect(); - strs[0].parse::().unwrap_or(0) - }) - .unwrap_or(0) + let build_number = get_build_number(); + // https://en.wikipedia.org/wiki/List_of_Microsoft_Windows_versions + if build_number >= 22000 { + 11 + } else if build_number >= 10240 { + 10 + } else if build_number >= 9200 { + 8 + } else { + 7 + } } pub(super) fn log_last_error(label: T) { @@ -51,3 +96,147 @@ pub(super) fn bgra_to_rgba_image( RgbaImage::from_raw(width, height, bgra_to_rgba(buffer)) .ok_or_else(|| XCapError::new("RgbaImage::from_raw failed")) } + +// 定义 GetProcessDpiAwareness 函数的类型 +type GetProcessDpiAwareness = + unsafe extern "system" fn(hprocess: HANDLE, value: *mut u32) -> HRESULT; + +pub(super) fn get_process_is_dpi_awareness(process: HANDLE) -> XCapResult { + unsafe { + let scope_guard_hmodule = load_library(w!("Shcore.dll"))?; + + let get_process_dpi_awareness_proc_address = + GetProcAddress(*scope_guard_hmodule, s!("GetProcessDpiAwareness")).ok_or( + XCapError::new("GetProcAddress GetProcessDpiAwareness failed"), + )?; + + let get_process_dpi_awareness: GetProcessDpiAwareness = + mem::transmute(get_process_dpi_awareness_proc_address); + + let mut process_dpi_awareness = 0; + // https://learn.microsoft.com/zh-cn/windows/win32/api/shellscalingapi/nf-shellscalingapi-getprocessdpiawareness + get_process_dpi_awareness(process, &mut process_dpi_awareness).ok()?; + + // 当前进程不感知 DPI,则回退到 GetDeviceCaps 获取 DPI + Ok(process_dpi_awareness != 0) + } +} + +pub(super) fn load_library( + lib_filename: PCWSTR, +) -> XCapResult> { + unsafe { + let hmodule = LoadLibraryW(lib_filename)?; + + if hmodule.is_invalid() { + return Err(XCapError::new(format!( + "LoadLibraryW error {:?}", + GetLastError() + ))); + } + + let scope_guard_hmodule = guard(hmodule, |val| { + if let Err(err) = FreeLibrary(val) { + log::error!("FreeLibrary {:?} failed {:?}", val, err); + } + }); + + Ok(scope_guard_hmodule) + } +} + +pub(super) fn open_process( + dw_desired_access: PROCESS_ACCESS_RIGHTS, + b_inherit_handle: bool, + dw_process_id: u32, +) -> XCapResult> { + unsafe { + let handle = OpenProcess(dw_desired_access, b_inherit_handle, dw_process_id)?; + + if handle.is_invalid() { + return Err(XCapError::new(format!( + "OpenProcess error {:?}", + GetLastError() + ))); + } + + let scope_guard_handle = guard(handle, |val| { + if let Err(err) = CloseHandle(val) { + log::error!("CloseHandle {:?} failed {:?}", val, err); + } + }); + + Ok(scope_guard_handle) + } +} + +pub(super) fn get_monitor_name(monitor_info_ex_w: MONITORINFOEXW) -> XCapResult { + unsafe { + let mut number_of_paths = 0; + let mut number_of_modes = 0; + GetDisplayConfigBufferSizes( + QDC_ONLY_ACTIVE_PATHS, + &mut number_of_paths, + &mut number_of_modes, + ) + .ok()?; + + let mut paths = vec![DISPLAYCONFIG_PATH_INFO::default(); number_of_paths as usize]; + let mut modes = vec![DISPLAYCONFIG_MODE_INFO::default(); number_of_modes as usize]; + + QueryDisplayConfig( + QDC_ONLY_ACTIVE_PATHS, + &mut number_of_paths, + paths.as_mut_ptr(), + &mut number_of_modes, + modes.as_mut_ptr(), + None, + ) + .ok()?; + + for path in paths { + let mut source = DISPLAYCONFIG_SOURCE_DEVICE_NAME { + header: DISPLAYCONFIG_DEVICE_INFO_HEADER { + r#type: DISPLAYCONFIG_DEVICE_INFO_GET_SOURCE_NAME, + size: mem::size_of::() as u32, + adapterId: path.sourceInfo.adapterId, + id: path.sourceInfo.id, + }, + ..DISPLAYCONFIG_SOURCE_DEVICE_NAME::default() + }; + + if DisplayConfigGetDeviceInfo(&mut source.header) != 0 { + continue; + } + + if source.viewGdiDeviceName != monitor_info_ex_w.szDevice { + continue; + } + + let mut target = DISPLAYCONFIG_TARGET_DEVICE_NAME { + header: DISPLAYCONFIG_DEVICE_INFO_HEADER { + r#type: DISPLAYCONFIG_DEVICE_INFO_GET_TARGET_NAME, + size: mem::size_of::() as u32, + adapterId: path.sourceInfo.adapterId, + id: path.targetInfo.id, + }, + ..DISPLAYCONFIG_TARGET_DEVICE_NAME::default() + }; + + if DisplayConfigGetDeviceInfo(&mut target.header) != 0 { + continue; + } + + let name = + U16CString::from_vec_truncate(target.monitorFriendlyDeviceName).to_string()?; + + if name.is_empty() { + return Err(XCapError::new("Monitor name is empty")); + } + + return Ok(name); + } + + Err(XCapError::new("Get monitor name failed")) + } +}