Skip to content

Commit 38ea964

Browse files
Merge pull request #185 from bquenin/feature/python-hwnd-support
feat(python): add window_hwnd parameter for HWND-based capture
2 parents eadfc62 + 3588654 commit 38ea964

2 files changed

Lines changed: 85 additions & 10 deletions

File tree

windows-capture-python/src/lib.rs

Lines changed: 77 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -132,12 +132,13 @@ pub struct NativeWindowsCapture {
132132
dirty_region_settings: DirtyRegionSettings,
133133
monitor_index: Option<usize>,
134134
window_name: Option<String>,
135+
window_hwnd: Option<isize>,
135136
}
136137

137138
#[pymethods]
138139
impl NativeWindowsCapture {
139140
#[new]
140-
#[pyo3(signature = (on_frame_arrived_callback, on_closed, cursor_capture=None, draw_border=None, secondary_window=None, minimum_update_interval=None, dirty_region=None, monitor_index=None, window_name=None))]
141+
#[pyo3(signature = (on_frame_arrived_callback, on_closed, cursor_capture=None, draw_border=None, secondary_window=None, minimum_update_interval=None, dirty_region=None, monitor_index=None, window_name=None, window_hwnd=None))]
141142
#[inline]
142143
#[allow(clippy::too_many_arguments)]
143144
pub fn new(
@@ -150,12 +151,22 @@ impl NativeWindowsCapture {
150151
dirty_region: Option<bool>,
151152
mut monitor_index: Option<usize>,
152153
window_name: Option<String>,
154+
window_hwnd: Option<isize>,
153155
) -> PyResult<Self> {
154-
if window_name.is_some() && monitor_index.is_some() {
155-
return Err(PyException::new_err("You can't specify both the monitor index and the window name"));
156+
// Count how many capture targets are specified
157+
let targets_specified = [monitor_index.is_some(), window_name.is_some(), window_hwnd.is_some()]
158+
.iter()
159+
.filter(|&&x| x)
160+
.count();
161+
162+
if targets_specified > 1 {
163+
return Err(PyException::new_err(
164+
"You can only specify one of: monitor_index, window_name, or window_hwnd"
165+
));
156166
}
157167

158-
if window_name.is_none() && monitor_index.is_none() {
168+
// Default to primary monitor if no target specified
169+
if targets_specified == 0 {
159170
monitor_index = Some(1);
160171
}
161172

@@ -198,14 +209,39 @@ impl NativeWindowsCapture {
198209
dirty_region_settings,
199210
monitor_index,
200211
window_name,
212+
window_hwnd,
201213
})
202214
}
203215

204216
/// Start capture.
205217
#[inline]
206218
pub fn start(&mut self) -> PyResult<()> {
207-
if self.window_name.is_some() {
208-
let window = match Window::from_contains_name(self.window_name.as_ref().unwrap()) {
219+
if let Some(hwnd) = self.window_hwnd {
220+
// Capture by window handle (HWND)
221+
let window = Window::from_raw_hwnd(hwnd as *mut std::ffi::c_void);
222+
223+
let settings = Settings::new(
224+
window,
225+
self.cursor_capture,
226+
self.draw_border,
227+
SecondaryWindowSettings::Default,
228+
MinimumUpdateIntervalSettings::Default,
229+
DirtyRegionSettings::Default,
230+
ColorFormat::Bgra8,
231+
(self.on_frame_arrived_callback.clone(), self.on_closed.clone()),
232+
);
233+
234+
match InnerNativeWindowsCapture::start(settings) {
235+
Ok(()) => (),
236+
Err(e) => {
237+
return Err(PyException::new_err(format!(
238+
"InnerNativeWindowsCapture::start threw an exception: {e}",
239+
)));
240+
}
241+
}
242+
} else if let Some(ref name) = self.window_name {
243+
// Capture by window name (substring match)
244+
let window = match Window::from_contains_name(name) {
209245
Ok(window) => window,
210246
Err(e) => {
211247
return Err(PyException::new_err(format!("Failed to find window: {e}")));
@@ -232,6 +268,7 @@ impl NativeWindowsCapture {
232268
}
233269
}
234270
} else {
271+
// Capture by monitor index
235272
let monitor = match Monitor::from_index(self.monitor_index.unwrap()) {
236273
Ok(monitor) => monitor,
237274
Err(e) => {
@@ -266,8 +303,39 @@ impl NativeWindowsCapture {
266303
/// Start capture on a dedicated thread.
267304
#[inline]
268305
pub fn start_free_threaded(&mut self) -> PyResult<NativeCaptureControl> {
269-
let capture_control = if self.window_name.is_some() {
270-
let window = match Window::from_contains_name(self.window_name.as_ref().unwrap()) {
306+
let capture_control = if let Some(hwnd) = self.window_hwnd {
307+
// Capture by window handle (HWND)
308+
let window = Window::from_raw_hwnd(hwnd as *mut std::ffi::c_void);
309+
310+
let settings = Settings::new(
311+
window,
312+
self.cursor_capture,
313+
self.draw_border,
314+
SecondaryWindowSettings::Default,
315+
MinimumUpdateIntervalSettings::Default,
316+
DirtyRegionSettings::Default,
317+
ColorFormat::Bgra8,
318+
(self.on_frame_arrived_callback.clone(), self.on_closed.clone()),
319+
);
320+
321+
let capture_control = match InnerNativeWindowsCapture::start_free_threaded(settings) {
322+
Ok(capture_control) => capture_control,
323+
Err(e) => {
324+
if let GraphicsCaptureApiError::FrameHandlerError(InnerNativeWindowsCaptureError::PythonError(
325+
ref e,
326+
)) = e
327+
{
328+
return Err(PyException::new_err(format!("Capture session threw an exception: {e}",)));
329+
}
330+
331+
return Err(PyException::new_err(format!("Capture session threw an exception: {e}",)));
332+
}
333+
};
334+
335+
NativeCaptureControl::new(capture_control)
336+
} else if let Some(ref name) = self.window_name {
337+
// Capture by window name (substring match)
338+
let window = match Window::from_contains_name(name) {
271339
Ok(window) => window,
272340
Err(e) => {
273341
return Err(PyException::new_err(format!("Failed to find window: {e}")));
@@ -301,6 +369,7 @@ impl NativeWindowsCapture {
301369

302370
NativeCaptureControl::new(capture_control)
303371
} else {
372+
// Capture by monitor index
304373
let monitor = match Monitor::from_index(self.monitor_index.unwrap()) {
305374
Ok(monitor) => monitor,
306375
Err(e) => {

windows-capture-python/windows_capture/__init__.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,7 @@ def __init__(
179179
dirty_region: Optional[bool] = None,
180180
monitor_index: Optional[int] = None,
181181
window_name: Optional[str] = None,
182+
window_hwnd: Optional[int] = None,
182183
) -> None:
183184
"""
184185
Constructs All The Necessary Attributes For The WindowsCapture Object
@@ -200,9 +201,13 @@ def __init__(
200201
monitor_index : int
201202
Index Of The Monitor To Capture
202203
window_name : str
203-
Name Of The Window To Capture
204+
Name Of The Window To Capture (substring match)
205+
window_hwnd : int
206+
Window Handle (HWND) To Capture - more reliable than window_name
207+
for windows with dynamic titles
204208
"""
205-
if window_name is not None:
209+
# Clear monitor_index if a window target is specified
210+
if window_name is not None or window_hwnd is not None:
206211
monitor_index = None
207212

208213
self.frame_handler: Optional[types.FunctionType] = None
@@ -217,6 +222,7 @@ def __init__(
217222
dirty_region,
218223
monitor_index,
219224
window_name,
225+
window_hwnd,
220226
)
221227

222228
def start(self) -> None:

0 commit comments

Comments
 (0)