Files
wechatauto-replica/wechatauto/utils/win32.py
T

403 lines
12 KiB
Python

import os
import time
import struct
import shutil
import traceback
import ctypes
from typing import List, Optional, Sequence, Tuple, Union
import win32ui
import win32gui
import win32api
import win32con
import win32process
import win32clipboard
import pyperclip
import psutil
from PIL import Image
def GetAllWindows(name=None, classname=None) -> List[Tuple[int, str, str]]:
"""获取所有顶层窗口的信息,返回 (窗口句柄, 类名, 窗口标题) 列表"""
windows = []
def enum_windows_proc(hwnd, extra):
class_name = win32gui.GetClassName(hwnd)
window_title = win32gui.GetWindowText(hwnd)
windows.append((hwnd, class_name, window_title))
win32gui.EnumWindows(enum_windows_proc, None)
if name:
windows = [i for i in windows if i[-1] == name]
if classname:
windows = [i for i in windows if i[1] == classname]
return windows
def GetCursorWindow():
x, y = win32api.GetCursorPos()
hwnd = win32gui.WindowFromPoint((x, y))
window_title = win32gui.GetWindowText(hwnd)
class_name = win32gui.GetClassName(hwnd)
return hwnd, window_title, class_name
def set_cursor_pos(x, y):
win32api.SetCursorPos((x, y))
def Click(rect):
x = (rect.left + rect.right) // 2
y = (rect.top + rect.bottom) // 2
set_cursor_pos(x, y)
win32api.mouse_event(win32con.MOUSEEVENTF_LEFTDOWN, x, y, 0, 0)
win32api.mouse_event(win32con.MOUSEEVENTF_LEFTUP, x, y, 0, 0)
def GetPathByHwnd(hwnd) -> Optional[str]:
try:
thread_id, process_id = win32process.GetWindowThreadProcessId(hwnd)
process = psutil.Process(process_id)
return process.exe()
except Exception:
return None
def GetVersionByPath(file_path) -> Optional[str]:
try:
info = win32api.GetFileVersionInfo(file_path, '\\')
version = "{}.{}.{}.{}".format(win32api.HIWORD(info['FileVersionMS']),
win32api.LOWORD(info['FileVersionMS']),
win32api.HIWORD(info['FileVersionLS']),
win32api.LOWORD(info['FileVersionLS']))
except Exception:
version = None
return version
def capture(hwnd, bbox) -> Image.Image:
"""截取指定窗口的指定区域,返回 PIL 图像"""
window_rect = win32gui.GetWindowRect(hwnd)
win_left, win_top, win_right, win_bottom = window_rect
win_width = win_right - win_left
win_height = win_bottom - win_top
# 获取窗口的设备上下文
hwndDC = win32gui.GetWindowDC(hwnd)
mfcDC = win32ui.CreateDCFromHandle(hwndDC)
saveDC = mfcDC.CreateCompatibleDC()
# 创建位图对象保存整个窗口截图
saveBitMap = win32ui.CreateBitmap()
saveBitMap.CreateCompatibleBitmap(mfcDC, win_width, win_height)
saveDC.SelectObject(saveBitMap)
# 使用PrintWindow捕获整个窗口(包括被遮挡或最小化的窗口)
result = ctypes.windll.user32.PrintWindow(hwnd, saveDC.GetSafeHdc(), 3)
# 转换为PIL图像
bmpinfo = saveBitMap.GetInfo()
bmpstr = saveBitMap.GetBitmapBits(True)
im = Image.frombuffer(
'RGB',
(bmpinfo['bmWidth'], bmpinfo['bmHeight']),
bmpstr, 'raw', 'BGRX', 0, 1)
# 释放资源
win32gui.DeleteObject(saveBitMap.GetHandle())
saveDC.DeleteDC()
mfcDC.DeleteDC()
win32gui.ReleaseDC(hwnd, hwndDC)
# 计算bbox相对于窗口左上角的坐标
bbox_left, bbox_top, bbox_right, bbox_bottom = bbox
crop_left = bbox_left - win_left
crop_top = bbox_top - win_top
crop_right = bbox_right - win_left
crop_bottom = bbox_bottom - win_top
# 裁剪目标区域
cropped_im = im.crop((crop_left, crop_top, crop_right, crop_bottom))
return cropped_im
def GetText(HWND) -> str:
length = win32gui.SendMessage(HWND, win32con.WM_GETTEXTLENGTH) * 2
buffer = win32gui.PyMakeBuffer(length)
win32api.SendMessage(HWND, win32con.WM_GETTEXT, length, buffer)
address, length_ = win32gui.PyGetBufferAddressAndLen(buffer[:-1])
text = win32gui.PyGetString(address, length_)[:int(length / 2)]
buffer.release()
return text
def GetAllWindowExs(HWND) -> Optional[List[list]]:
if not HWND:
return None
handles = []
win32gui.EnumChildWindows(
HWND, lambda hwnd, param: param.append([hwnd, win32gui.GetClassName(hwnd), GetText(hwnd)]), handles)
return handles
def FindWindow(classname=None, name=None, timeout=0) -> int:
t0 = time.time()
while True:
HWND = win32gui.FindWindow(classname, name)
if HWND:
break
if time.time() - t0 > timeout:
break
time.sleep(0.01)
return HWND
def FindWinEx(HWND, classname=None, name=None) -> list:
hwnds_classname = []
hwnds_name = []
def find_classname(hwnd, classname):
classname_ = win32gui.GetClassName(hwnd)
if classname_ == classname:
if hwnd not in hwnds_classname:
hwnds_classname.append(hwnd)
def find_name(hwnd, name):
name_ = GetText(hwnd)
if name in name_:
if hwnd not in hwnds_name:
hwnds_name.append(hwnd)
if classname:
win32gui.EnumChildWindows(HWND, find_classname, classname)
if name:
win32gui.EnumChildWindows(HWND, find_name, name)
if classname and name:
hwnds = [hwnd for hwnd in hwnds_classname if hwnd in hwnds_name]
else:
hwnds = hwnds_classname + hwnds_name
return hwnds
def ClipboardFormats(unit=0, *units) -> List[int]:
units = list(units)
retry_count = 5
while retry_count > 0:
try:
win32clipboard.OpenClipboard()
try:
u = win32clipboard.EnumClipboardFormats(unit)
finally:
win32clipboard.CloseClipboard()
break
except Exception:
retry_count -= 1
units.append(u)
if u:
units = ClipboardFormats(u, *units)
return units
def ReadClipboardData() -> dict:
Dict = {}
formats = ClipboardFormats()
for i in formats:
if i == 0:
continue
retry_count = 5
while retry_count > 0:
try:
win32clipboard.OpenClipboard()
try:
data = win32clipboard.GetClipboardData(i)
Dict[str(i)] = data
finally:
win32clipboard.CloseClipboard()
break
except Exception:
retry_count -= 1
return Dict
def SetClipboardData(data_dict: dict) -> None:
try:
# 打开剪贴板
win32clipboard.OpenClipboard()
# 清空剪贴板
win32clipboard.EmptyClipboard()
# 遍历数据字典,设置各种格式的数据
for format_id, data in data_dict.items():
# 将字符串格式ID转换为整数
format_num = int(format_id)
if isinstance(data, str):
# 如果是字符串,使用Unicode格式
win32clipboard.SetClipboardData(format_num, data)
elif isinstance(data, bytes):
# 如果是字节数据,直接设置
win32clipboard.SetClipboardData(format_num, data)
except Exception as e:
print(f"设置剪贴板数据时出错: {e}")
finally:
# 关闭剪贴板
try:
win32clipboard.CloseClipboard()
except Exception:
pass
def SetClipboardText(text: str) -> None:
pyperclip.copy(text)
class DROPFILES(ctypes.Structure):
_fields_ = [
("pFiles", ctypes.c_uint),
("x", ctypes.c_long),
("y", ctypes.c_long),
("fNC", ctypes.c_int),
("fWide", ctypes.c_bool),
]
def set_files_to_clipboard(file_paths: Union[str, Sequence[str]]) -> bool:
"""将文件路径列表设置到剪贴板的 CF_HDROP 格式"""
# 如果传入的是单个字符串,转换为列表
if isinstance(file_paths, str):
file_paths = [file_paths]
# 验证文件路径是否存在
valid_paths = []
for path in file_paths:
if os.path.exists(path):
# 转换为绝对路径
abs_path = os.path.abspath(path)
valid_paths.append(abs_path)
else:
raise ValueError(f"文件路径不存在: {path}")
if not valid_paths:
return False
try:
# 打开剪贴板
win32clipboard.OpenClipboard()
# 清空剪贴板
win32clipboard.EmptyClipboard()
# 计算偏移量(DROPFILES结构大小为20字节)
offset = 20
# 构建DROPFILES头部
dropfiles_header = struct.pack('<LLLLL',
offset, # pFiles偏移量
0, # pt.x
0, # pt.y
0, # fNC
1) # fWide (使用Unicode)
# 构建文件路径字符串(Unicode,以双null结尾)
file_list = []
for path in valid_paths:
# 转换为Unicode字节
file_list.append(path.encode('utf-16le'))
file_list.append(b'\x00\x00') # Unicode null终止符
# 添加额外的双null作为列表结束标记
file_list.append(b'\x00\x00')
# 合并所有数据
file_data = b''.join(file_list)
hdrop_data = dropfiles_header + file_data
# 设置到剪贴板
win32clipboard.SetClipboardData(win32con.CF_HDROP, hdrop_data)
return True
except Exception:
traceback.print_exc()
return False
finally:
# 关闭剪贴板
try:
win32clipboard.CloseClipboard()
except Exception:
pass
def SetClipboardFiles(paths) -> bool:
return set_files_to_clipboard(paths)
def PasteFile(folder) -> bool:
folder = os.path.realpath(folder)
if not os.path.exists(folder):
os.makedirs(folder)
t0 = time.time()
while True:
if time.time() - t0 > 10:
raise TimeoutError(f"读取剪贴板文件超时!")
try:
win32clipboard.OpenClipboard()
if win32clipboard.IsClipboardFormatAvailable(win32clipboard.CF_HDROP):
files = win32clipboard.GetClipboardData(win32clipboard.CF_HDROP)
for file in files:
filename = os.path.basename(file)
dest_file = os.path.join(folder, filename)
shutil.copy2(file, dest_file)
return True
else:
print("剪贴板中没有文件")
return False
except Exception:
pass
finally:
try:
win32clipboard.CloseClipboard()
except Exception:
pass
def enum_windows_by_pid(pid: int) -> List[int]:
window_list = []
def enum_callback(hwnd, lParam):
# 获取窗口的进程ID
_, process_id = win32process.GetWindowThreadProcessId(hwnd)
# 如果是目标进程,检查窗口是否可见
if process_id == pid:
if is_window_visible(hwnd):
window_list.append(hwnd)
return True
win32gui.EnumWindows(enum_callback, None)
return window_list
def is_window_visible(hwnd) -> bool:
# 检查窗口是否可见
style = win32gui.GetWindowLong(hwnd, win32con.GWL_STYLE)
# 检查窗口是否有 WS_VISIBLE 标志,且不是最小化的
if style & win32con.WS_VISIBLE and not win32gui.IsIconic(hwnd):
return True
return False
def get_windows_by_pid(pid: int) -> List[int]:
while True:
try:
windows = enum_windows_by_pid(pid)
return windows
except Exception:
time.sleep(0.1)