feat: Initialize wxauto WeChat automation project with job extraction tools
- Add wxauto package with WeChat UI automation and message handling capabilities - Implement job_extractor.py for automated job posting extraction from WeChat groups - Add job_extractor_gui.py providing graphical interface for job extraction tool - Create comprehensive documentation in Chinese covering GUI usage, multi-group support, and quick start guides - Add build configuration files (build_exe.py, build_exe.spec) for packaging as standalone executable - Include utility scripts for WeChat interaction (auto_send_msg.py, get_history.py, receive_file_transfer.py) - Add project configuration files (pyproject.toml, setup.cfg, requirements.txt) - Include test files (test_api.py, test_com_fix.py) for API and compatibility validation - Add Apache 2.0 LICENSE and comprehensive README documentation - Configure .gitignore to exclude build artifacts, logs, and temporary files
This commit is contained in:
302
wxauto/utils/win32.py
Normal file
302
wxauto/utils/win32.py
Normal file
@@ -0,0 +1,302 @@
|
||||
import os
|
||||
import time
|
||||
import shutil
|
||||
import win32ui
|
||||
import win32gui
|
||||
import win32api
|
||||
import win32con
|
||||
import win32process
|
||||
import win32clipboard
|
||||
import pyperclip
|
||||
import ctypes
|
||||
from PIL import Image
|
||||
from wxauto import uiautomation as uia
|
||||
|
||||
def GetAllWindows():
|
||||
"""
|
||||
获取所有窗口的信息,返回一个列表,每个元素包含 (窗口句柄, 类名, 窗口标题)
|
||||
"""
|
||||
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)
|
||||
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):
|
||||
"""通过窗口句柄获取进程可执行文件路径 - 使用pywin32"""
|
||||
try:
|
||||
thread_id, process_id = win32process.GetWindowThreadProcessId(hwnd)
|
||||
|
||||
# 获取进程句柄
|
||||
PROCESS_QUERY_INFORMATION = 0x0400
|
||||
PROCESS_VM_READ = 0x0010
|
||||
process_handle = win32api.OpenProcess(
|
||||
PROCESS_QUERY_INFORMATION | PROCESS_VM_READ,
|
||||
False,
|
||||
process_id
|
||||
)
|
||||
|
||||
# 获取可执行文件路径
|
||||
exe_path = win32process.GetModuleFileNameEx(process_handle, 0)
|
||||
|
||||
# 关闭句柄
|
||||
win32api.CloseHandle(process_handle)
|
||||
|
||||
return exe_path
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
return None
|
||||
|
||||
def GetVersionByPath(file_path):
|
||||
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:
|
||||
version = None
|
||||
return version
|
||||
|
||||
def GetWindowRect(hwnd):
|
||||
return win32gui.GetWindowRect(hwnd)
|
||||
|
||||
def capture(hwnd, bbox):
|
||||
# 获取窗口的屏幕坐标
|
||||
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):
|
||||
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):
|
||||
if not HWND:
|
||||
return
|
||||
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 FindTopLevelControl(classname=None, name=None, timeout=3):
|
||||
hwnd = FindWindow(classname, name, timeout)
|
||||
if hwnd:
|
||||
return uia.ControlFromHandle(hwnd)
|
||||
else:
|
||||
return None
|
||||
|
||||
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):
|
||||
units = list(units)
|
||||
retry_count = 5
|
||||
while retry_count > 0:
|
||||
try:
|
||||
win32clipboard.OpenClipboard()
|
||||
try:
|
||||
u = win32clipboard.EnumClipboardFormats(unit)
|
||||
finally:
|
||||
win32clipboard.CloseClipboard()
|
||||
break
|
||||
except Exception as e:
|
||||
retry_count -= 1
|
||||
units.append(u)
|
||||
if u:
|
||||
units = ClipboardFormats(u, *units)
|
||||
return units
|
||||
|
||||
def ReadClipboardData():
|
||||
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 as e:
|
||||
retry_count -= 1
|
||||
return Dict
|
||||
|
||||
def SetClipboardText(text: str):
|
||||
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),
|
||||
]
|
||||
|
||||
pDropFiles = DROPFILES()
|
||||
pDropFiles.pFiles = ctypes.sizeof(DROPFILES)
|
||||
pDropFiles.fWide = True
|
||||
matedata = bytes(pDropFiles)
|
||||
|
||||
def SetClipboardFiles(paths):
|
||||
for file in paths:
|
||||
if not os.path.exists(file):
|
||||
raise FileNotFoundError(f"file ({file}) not exists!")
|
||||
files = ("\0".join(paths)).replace("/", "\\")
|
||||
data = files.encode("U16")[2:]+b"\0\0"
|
||||
t0 = time.time()
|
||||
while True:
|
||||
if time.time() - t0 > 10:
|
||||
raise TimeoutError(f"设置剪贴板文件超时! --> {paths}")
|
||||
try:
|
||||
win32clipboard.OpenClipboard()
|
||||
win32clipboard.EmptyClipboard()
|
||||
win32clipboard.SetClipboardData(win32clipboard.CF_HDROP, matedata+data)
|
||||
break
|
||||
except:
|
||||
pass
|
||||
finally:
|
||||
try:
|
||||
win32clipboard.CloseClipboard()
|
||||
except:
|
||||
pass
|
||||
|
||||
def PasteFile(folder):
|
||||
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:
|
||||
pass
|
||||
finally:
|
||||
win32clipboard.CloseClipboard()
|
||||
|
||||
def IsRedPixel(uicontrol):
|
||||
rect = uicontrol.BoundingRectangle
|
||||
hwnd = uicontrol.GetAncestorControl(lambda x,y:x.ClassName=='WeChatMainWndForPC').NativeWindowHandle
|
||||
bbox = (rect.left, rect.top, rect.right, rect.bottom)
|
||||
img = capture(hwnd, bbox)
|
||||
return any(p[0] > p[1] and p[0] > p[2] for p in img.getdata())
|
||||
Reference in New Issue
Block a user