-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathvmconnect_capture.py
More file actions
477 lines (386 loc) · 15.9 KB
/
vmconnect_capture.py
File metadata and controls
477 lines (386 loc) · 15.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
import win32gui
import win32ui
import win32con
import win32process
import psutil
from PIL import Image
import numpy as np
from typing import Optional, Tuple, List, Union
import ctypes
import win32api
import time
top_offset = 108
bottom_offset = 37
# Virtual key code constants
VK_RETURN = 0x0D
VK_SHIFT = 0x10
VK_CONTROL = 0x11
VK_MENU = 0x12 # Alt key
VK_LWIN = 0x5B # Left Windows key
VK_RWIN = 0x5C # Right Windows key
VK_R = 0x52 # R key
# Key mapping dictionary
KEY_MAP = {
"enter": VK_RETURN,
"windows": VK_LWIN,
}
# Define input structure for SendInput
PUL = ctypes.POINTER(ctypes.c_ulong)
class KeyBdInput(ctypes.Structure):
_fields_ = [("wVk", ctypes.c_ushort),
("wScan", ctypes.c_ushort),
("dwFlags", ctypes.c_ulong),
("time", ctypes.c_ulong),
("dwExtraInfo", PUL)]
class HardwareInput(ctypes.Structure):
_fields_ = [("uMsg", ctypes.c_ulong),
("wParamL", ctypes.c_short),
("wParamH", ctypes.c_ushort)]
class MouseInput(ctypes.Structure):
_fields_ = [("dx", ctypes.c_long),
("dy", ctypes.c_long),
("mouseData", ctypes.c_ulong),
("dwFlags", ctypes.c_ulong),
("time", ctypes.c_ulong),
("dwExtraInfo", PUL)]
class Input_I(ctypes.Union):
_fields_ = [("ki", KeyBdInput),
("mi", MouseInput),
("hi", HardwareInput)]
class Input(ctypes.Structure):
_fields_ = [("type", ctypes.c_ulong),
("ii", Input_I)]
def click_at_coordinates(x: int, y: int) -> bool:
"""Send a mouse click to the specified coordinates in the VMConnect window
Args:
x: X coordinate within window
y: Y coordinate within window
Returns:
bool: True if click was sent successfully, False otherwise
"""
# Find VMConnect window
hwnd = find_vmconnect_window()
if not hwnd:
print("Could not find VMConnect window")
return False
# Convert coordinates relative to window
window_rect = win32gui.GetWindowRect(hwnd)
client_rect = win32gui.GetClientRect(hwnd)
border_width = ((window_rect[2] - window_rect[0]) - client_rect[2]) // 2
title_height = (window_rect[3] - window_rect[1]) - client_rect[3] - border_width
# Add window frame offset to coordinates
screen_x = window_rect[0] + border_width + x
screen_y = window_rect[1] + title_height + y
#offset by an experimental amount
screen_y = screen_y + 46
screen_x = screen_x - 2
# Get the current cursor position
old_pos = win32api.GetCursorPos()
try:
# Move cursor to target position
win32api.SetCursorPos((screen_x, screen_y))
# Create mouse input structures
extra = ctypes.c_ulong(0)
ii_ = Input_I()
# Mouse down
ii_.mi = MouseInput(0, 0, 0, win32con.MOUSEEVENTF_LEFTDOWN, 0, ctypes.pointer(extra))
x = Input(ctypes.c_ulong(0), ii_)
ctypes.windll.user32.SendInput(1, ctypes.pointer(x), ctypes.sizeof(x))
time.sleep(0.1) # Small delay between down and up
# Mouse up
ii_.mi = MouseInput(0, 0, 0, win32con.MOUSEEVENTF_LEFTUP, 0, ctypes.pointer(extra))
x = Input(ctypes.c_ulong(0), ii_)
ctypes.windll.user32.SendInput(1, ctypes.pointer(x), ctypes.sizeof(x))
# Move cursor back
win32api.SetCursorPos(old_pos)
return True
except Exception as e:
print(f"Click failed: {str(e)}")
return False
def is_window_valid(hwnd: int) -> bool:
"""Check if the window handle is valid and visible"""
try:
return (
win32gui.IsWindow(hwnd) and
win32gui.IsWindowVisible(hwnd) and
win32gui.GetWindowRect(hwnd) != (0, 0, 0, 0)
)
except Exception:
return False
def find_vmconnect_window() -> Optional[int]:
"""Find the VMConnect window handle"""
def callback(hwnd, hwnds):
if win32gui.IsWindowVisible(hwnd):
window_text = win32gui.GetWindowText(hwnd)
if "Virtual Machine Connection" in window_text:
_, process_id = win32process.GetWindowThreadProcessId(hwnd)
try:
process = psutil.Process(process_id)
if process.name().lower() == "vmconnect.exe":
hwnds.append(hwnd)
except (psutil.NoSuchProcess, psutil.AccessDenied):
pass
return True
hwnds = []
win32gui.EnumWindows(callback, hwnds)
if not hwnds:
print("No VMConnect windows found")
return None
# Return first valid window
for hwnd in hwnds:
if is_window_valid(hwnd):
return hwnd
print("Found VMConnect windows but none are valid/visible")
return None
def capture_window_screenshot(hwnd: int) -> Optional[Image.Image]:
"""Capture a screenshot of the specified window"""
window_dc = None
mfc_dc = None
save_dc = None
save_bitmap = None
try:
# Get window dimensions
left, top, right, bottom = win32gui.GetWindowRect(hwnd)
width = right - left
height = bottom - top
# Get the DPI scale factor - this seems like it doesn't work right now
# scale_factor = get_dpi_scale(hwnd)
# print(f"DPI Scale factor: {scale_factor}")
# Apply scaling
width = int(width * 1.5)
height = int(height * 1.5)
print(f"Window dimensions: {width}x{height} at ({left},{top})")
# Get the target window DC
window_dc = win32gui.GetDC(hwnd)
if not window_dc:
print("Failed to get window DC")
return None
# Create DC objects
mfc_dc = win32ui.CreateDCFromHandle(window_dc)
save_dc = mfc_dc.CreateCompatibleDC()
# Create bitmap object
save_bitmap = win32ui.CreateBitmap()
save_bitmap.CreateCompatibleBitmap(mfc_dc, width, height)
save_dc.SelectObject(save_bitmap)
# Try to copy window content using PrintWindow
try:
# PW_CLIENTONLY = 1
# PW_RENDERFULLCONTENT = 2
result = ctypes.windll.user32.PrintWindow(
hwnd,
save_dc.GetSafeHdc(),
2
)
except Exception as e:
print(f"PrintWindow failed with exception: {e}")
result = 0
if result:
try:
# Convert to PIL Image
bmpinfo = save_bitmap.GetInfo()
bmpstr = save_bitmap.GetBitmapBits(True)
# Create full image first
full_img = Image.frombuffer(
'RGB',
(bmpinfo['bmWidth'], bmpinfo['bmHeight']),
bmpstr, 'raw', 'BGRX', 0, 1)
# Crop to remove top and bottom offsets
img = full_img.crop((0, top_offset, bmpinfo['bmWidth'], bmpinfo['bmHeight'] - bottom_offset))
return img
except Exception as e:
print(f"Failed to convert bitmap to image: {e}")
return None
else:
print("PrintWindow failed to copy window content")
error_code = ctypes.get_last_error()
print(f"Last error code: {error_code}")
return None
except Exception as e:
print(f"Error capturing screenshot: {e}")
return None
finally:
# Clean up in finally block to ensure resources are released
if save_bitmap:
win32gui.DeleteObject(save_bitmap.GetHandle())
if save_dc:
save_dc.DeleteDC()
if mfc_dc:
mfc_dc.DeleteDC()
if window_dc:
win32gui.ReleaseDC(hwnd, window_dc)
def get_vmconnect_screenshot() -> Optional[Image.Image]:
"""Get a screenshot from the active VMConnect window"""
hwnd = find_vmconnect_window()
if not hwnd:
print("No VMConnect window found")
return None
return capture_window_screenshot(hwnd)
def set_foreground_vmconnect() -> Optional[int]:
"""Set VMConnect window as foreground window and return its handle"""
hwnd = find_vmconnect_window()
if not hwnd:
print("Could not find VMConnect window")
return None
try:
# Bring window to foreground
if not win32gui.IsWindowVisible(hwnd):
win32gui.ShowWindow(hwnd, win32con.SW_SHOW)
# send a control key down/up event
win32gui.SendMessage(hwnd, win32con.WM_KEYDOWN, VK_CONTROL, 0)
win32gui.SendMessage(hwnd, win32con.WM_KEYUP, VK_CONTROL, 0)
win32gui.SetForegroundWindow(hwnd)
time.sleep(0.5) # Give window time to come to foreground
return hwnd
except Exception as e:
print(f"Failed to set foreground window: {e}")
error_code = ctypes.get_last_error()
print(f"Last error code: {error_code}")
return None
def send_special_key(key_code: Union[int, List[int]], press_only: bool = False) -> bool:
"""Send special key press to VMConnect window
Args:
key_code: Virtual key code(s) to send. Can be single int or list of ints for key combinations
press_only: If True, only sends key press without release (for modifiers)
Returns:
bool: True if successful, False otherwise
"""
if not set_foreground_vmconnect():
return False
try:
extra = ctypes.c_ulong(0)
ii_ = Input_I()
# Convert single key to list
if isinstance(key_code, int):
key_codes = [key_code]
else:
key_codes = key_code
# Press all keys
for kc in key_codes:
ii_.ki = KeyBdInput(kc, 0, 0, 0, ctypes.pointer(extra))
x = Input(ctypes.c_ulong(1), ii_)
ctypes.windll.user32.SendInput(1, ctypes.pointer(x), ctypes.sizeof(x))
time.sleep(0.05)
if not press_only:
# Release all keys in reverse order
for kc in reversed(key_codes):
ii_.ki = KeyBdInput(kc, 0, win32con.KEYEVENTF_KEYUP, 0, ctypes.pointer(extra))
x = Input(ctypes.c_ulong(1), ii_)
ctypes.windll.user32.SendInput(1, ctypes.pointer(x), ctypes.sizeof(x))
time.sleep(0.05)
return True
except Exception as e:
print(f"Failed to send special key: {e}")
return False
def send_text(text: str) -> bool:
"""Send text input to VMConnect window
Args:
text: String to type into the window
Returns:
bool: True if successful, False otherwise
"""
if not set_foreground_vmconnect():
print("Could not set VMConnect window to foreground")
return False
try:
extra = ctypes.c_ulong(0)
ii_ = Input_I()
for char in text:
# Get virtual key code and shift state
vk = win32api.VkKeyScan(char)
if vk == -1:
print(f"No virtual key code found for character: {char}")
continue
needs_shift = (vk >> 8) & 0x1 # Check if shift modifier is needed
vk = vk & 0xFF # Get base virtual key code
# Press shift if needed
if needs_shift:
ii_.ki = KeyBdInput(VK_SHIFT, 0, 0, 0, ctypes.pointer(extra))
x = Input(ctypes.c_ulong(1), ii_)
ctypes.windll.user32.SendInput(1, ctypes.pointer(x), ctypes.sizeof(x))
time.sleep(0.05)
# Send key down
ii_.ki = KeyBdInput(vk, 0, 0, 0, ctypes.pointer(extra))
x = Input(ctypes.c_ulong(1), ii_)
ctypes.windll.user32.SendInput(1, ctypes.pointer(x), ctypes.sizeof(x))
time.sleep(0.05)
# Send key up
ii_.ki = KeyBdInput(vk, 0, win32con.KEYEVENTF_KEYUP, 0, ctypes.pointer(extra))
x = Input(ctypes.c_ulong(1), ii_)
ctypes.windll.user32.SendInput(1, ctypes.pointer(x), ctypes.sizeof(x))
time.sleep(0.05)
# Release shift if it was pressed
if needs_shift:
ii_.ki = KeyBdInput(VK_SHIFT, 0, win32con.KEYEVENTF_KEYUP, 0, ctypes.pointer(extra))
x = Input(ctypes.c_ulong(1), ii_)
ctypes.windll.user32.SendInput(1, ctypes.pointer(x), ctypes.sizeof(x))
time.sleep(0.05)
return True
except Exception as e:
print(f"Failed to send text: {e}")
return False
def press_key(key: str) -> bool:
"""Send a keyboard key press to the VMConnect window
Args:
key: String identifier of the key to press ('enter' or 'windows')
Returns:
bool: True if key press was sent successfully, False otherwise
"""
# Convert key string to virtual key code
if key.lower() not in KEY_MAP:
print(f"Unsupported key: {key}")
return False
if not set_foreground_vmconnect():
print("Could not set VMConnect window to foreground")
return False
vk_code = KEY_MAP[key.lower()]
try:
# Create keyboard input structures
extra = ctypes.c_ulong(0)
ii_ = Input_I()
# Key down
ii_.ki = KeyBdInput(vk_code, 0, 0, 0, ctypes.pointer(extra))
x = Input(ctypes.c_ulong(1), ii_)
ctypes.windll.user32.SendInput(1, ctypes.pointer(x), ctypes.sizeof(x))
time.sleep(0.05) # Small delay between down and up
# Key up
ii_.ki = KeyBdInput(vk_code, 0, win32con.KEYEVENTF_KEYUP, 0, ctypes.pointer(extra))
x = Input(ctypes.c_ulong(1), ii_)
ctypes.windll.user32.SendInput(1, ctypes.pointer(x), ctypes.sizeof(x))
return True
except Exception as e:
print(f"Key press failed: {str(e)}")
return False
def open_run_dialog() -> bool:
"""Send Windows+R key combination to open the Run dialog
Returns:
bool: True if key combination was sent successfully, False otherwise
"""
if not set_foreground_vmconnect():
print("Could not set VMConnect window to foreground")
return False
try:
extra = ctypes.c_ulong(0)
ii_ = Input_I()
# Press Windows key
ii_.ki = KeyBdInput(VK_LWIN, 0, 0, 0, ctypes.pointer(extra))
x = Input(ctypes.c_ulong(1), ii_)
ctypes.windll.user32.SendInput(1, ctypes.pointer(x), ctypes.sizeof(x))
time.sleep(0.05)
# Press R key
ii_.ki = KeyBdInput(VK_R, 0, 0, 0, ctypes.pointer(extra))
x = Input(ctypes.c_ulong(1), ii_)
ctypes.windll.user32.SendInput(1, ctypes.pointer(x), ctypes.sizeof(x))
time.sleep(0.05)
# Release R key
ii_.ki = KeyBdInput(VK_R, 0, win32con.KEYEVENTF_KEYUP, 0, ctypes.pointer(extra))
x = Input(ctypes.c_ulong(1), ii_)
ctypes.windll.user32.SendInput(1, ctypes.pointer(x), ctypes.sizeof(x))
time.sleep(0.05)
# Release Windows key
ii_.ki = KeyBdInput(VK_LWIN, 0, win32con.KEYEVENTF_KEYUP, 0, ctypes.pointer(extra))
x = Input(ctypes.c_ulong(1), ii_)
ctypes.windll.user32.SendInput(1, ctypes.pointer(x), ctypes.sizeof(x))
return True
except Exception as e:
print(f"Failed to send Win+R: {str(e)}")
return False