- 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
302 lines
9.3 KiB
Python
302 lines
9.3 KiB
Python
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()) |