最近要用python模拟人的操作给窗口发送拖拽文件的消息,网上搜了一大圈也没搜到现成可用的代码。幸好以前做过vc开发,熟悉点win32编程,于是装上vs和msdn,从消息WM_DROPFILES查起,慢慢得实现了这个功能。
WM_DROPFILES是向win32窗口拖拽一个文件松开鼠标左键后会触发发送给窗口的消息,前提是目标窗口是支持拖拽消息的响应,在win32中是通过窗口样式:WS_EX_ACCEPTFILES或者调用API:DragAcceptFiles(HWND hWnd,BOOL fAccept)设置过的窗口才会正常响应拖拽消息。
官方定义发送消息WM_DROPFILES方式如下:
PostMessage(
(HWND) hWndControl, // 这里是目标窗口的句柄,可以通过FindWindowEX或者FindWindow函数获取
(UINT) WM_DROPFILES, // 这里是消息ID,实际就是个数值0x0233
(WPARAM) wParam, // 重要的是这个参数,是指定消息中的一些必要信息和被拖拽文件路径的结构体
(LPARAM) lParam // 这里设置为0
);
//虽然官方是用PostMessage,但是我实际测试使用SendMessage也是可以的。
根据文档确定wParam是一个指针,指向DROPFILES结构体紧跟上文件路径列表的一段数据,其中文路径列表以‘\0’间隔,列表最终以'\0\0'结束。也就是:DROPFILESD:\test.txt\0\0。需要注意的是这个内存地址得是目标进程地址空间中的内存地址,不能是你python程序中的内存地址。win32程序每个程序都有自己的内存地址空间。所以在发送之前还得先通过api在目标进程中申请一段内存地址存放这个消息体,然后把这个内存地址传入wParam参数。
typedef struct _DROPFILES {
DWORD pFiles; //消息体重文件路开始位置偏移量
POINT pt; //消息触发的坐标点。
BOOL fNC; //坐标点是基于全屏还是基于对应窗口。TRUE:基于全屏。FALSE:基于目标窗口
BOOL fWide; //好像是指定文件路径中是否包含unicode字符。我设置为TRUE和FALSE都可以。
} DROPFILES, *LPDROPFILES;
接下来就是实现过程,先构建消息体,然后在目标进程申请内存区域,再把消息体拷贝进目标进程申请的内存区域,然后调用python的win32库里的SendMessage或者PostMessage来向对应窗口发送这个消息就行了。代码如下:
import ctypes
import struct
import logging
from ctypes.wintypes import *
import win32clipboard
import win32con
import win32gui
from win32con import PAGE_READWRITE, MEM_COMMIT, PROCESS_ALL_ACCESS
from ctypes.wintypes import FILETIME
#下面这些API在pywin32库中没有,需要通过windll间接获取函数,类似于win32开发中动态使用dll中函数的办法
GetWindowThreadProcessId = ctypes.windll.user32.GetWindowThreadProcessId
VirtualAllocEx = ctypes.windll.kernel32.VirtualAllocEx
VirtualFreeEx = ctypes.windll.kernel32.VirtualFreeEx
OpenProcess = ctypes.windll.kernel32.OpenProcess
WriteProcessMemory = ctypes.windll.kernel32.WriteProcessMemory
ReadProcessMemory = ctypes.windll.kernel32.
def dragFileToWnd(file, hwnd):
"""
dragFileToWnd(file, hwnd)
file:文件绝对路径或者相对路径。
hwnd:窗口的句柄,可以通过win32gui.FindWindowEx 精确查找获取。返回值就是这个hwnd
"""
filepath = bytes(file + "\0\0", encoding="GBK") # 文件路径结尾必须紧跟两个0,为了支持中文编码用了GBK
# 用ctypes的struct来模拟DROPFILES结构体。
#其中第一个参数0x14是指消息体内存区域中的文件路径是从第几个字节开始,
#也就是对应的DROPFILES的pFiles参数,后面参数类似原理,
#我把鼠标松开的点位定义在了窗口的(10,10)位置,所以是0x0A,0x0A
DropFilesInfo = struct.pack("iiiii" + str(len(filepath)) + "s",*[0x14, 0x0A, 0x0A, 00, 00, filepath])
# 从结构体创建缓冲区,相当于获取结构体的地址,
#然后把整个结构体看成一段内存区域,也可以用ctypes.addressof(DropFilesInfo)直接获取地址值
s_buff = ctypes.create_string_buffer(DropFilesInfo)
pid = ctypes.c_uint(0)
GetWindowThreadProcessId(hwnd, ctypes.addressof(pid)) #获取进程ID,以便后续在进程地址空间中申请内存。
print("pid:%x" % pid.value)
hProcHnd = OpenProcess(PROCESS_ALL_ACCESS, False, pid)#打开进程,获取进程句柄。
print("open Process:%x" % hProcHnd)
pMem = VirtualAllocEx(hProcHnd, 0, len(DropFilesInfo), MEM_COMMIT,PAGE_READWRITE) # 在目标进程中申请一段大小能容纳DROPFILES和文件路径的内存区域
copied = ctypes.c_int(0)
WriteProcessMemory(hProcHnd, pMem, s_buff, len(DropFilesInfo), ctypes.addressof(copied)) # 将消息体写入
print("copied:", copied.value)
win32gui.SendMessage(hwnd, win32con.WM_DROPFILES, pMem) # 模拟发送拖拽消息给对应进程。
VirtualFreeEx(hProcHnd,pMem,0,win32con.MEM_RELEASE) #用完后要释放对应内存区域,防止内存泄漏。
#调用测试,模拟将d盘下test.txt 文件拖入打开着的notepad记事本窗口。当路径指定的文件不存在,notepad会弹出新建的提示。
file = "D:\\test.txt"
hwnd1 = win32gui.FindWindow("Notepad2U", "1.txt - Notepad2-mod (管理员)") #
dragFileToWnd(file, hwnd1)
补充FindWindow函数说明:
HWND FindWindow( LPCTSTR lpClassName, LPCTSTR lpWindowName );
这个函数用于通过窗口类名和窗口标题查找到指定的窗口句柄。可以通过spy++或者spylite24来捕获。