一、什么是句柄
句柄是在操作系统中的一种标识符,相当于我们每个人的身份证一样,句柄在电脑中也是有唯一性的,我们启动的每一个程序都有自己的句柄号,表示自己的身份
为什么要说句柄,我们如果想做自动化操作时,肯定也不想程序占用了我们整个电脑,稍微操作一下程序步骤就乱掉了,更加希望自动化程序在运行的时候能够只针对某个窗口或者某个程序进行操作,即使我们把自动化的程序放入都后台时也不影响两边的操作,这里就需要用到句柄了
所需的包
#配置清华镜像源
pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple
pip config set global.trusted-host pypi.tuna.tsinghua.edu.cn
#安装依赖库
pip install pywin32
基本使用
#部分参考文档
https://huaweicloud.csdn.net/63803058dacf622b8df86819.html?spm=1001.2101.3001.6661.1&utm_medium=distribute.pc_relevant_t0.none-task-blog-2~default~BlogCommendFromBaidu~activity-1-122498299-blog-111083068.pc_relevant_vip_default&depth_1-utm_source=distribute.pc_relevant_t0.none-task-blog-2~default~BlogCommendFromBaidu~activity-1-122498299-blog-111083068.pc_relevant_vip_default&utm_relevant_index=1
1、获取鼠标所在位置程序的句柄
import time
import win32api
import win32gui
time.sleep(2)
point = win32api.GetCursorPos() #win32api.GetCursorPos 获取鼠标当前的坐标(x,y)
hwnd = win32gui.WindowFromPoint(point) #查看坐标位置窗口的句柄
print(hwnd) #输出句柄
如下图,我执行了3遍分别在执行后将鼠标放在文本、桌面、idea上面,返回了句柄ID
2、通过句柄获取类名
我们每次关闭重新打开一个程序会发现句柄值变了,每次都从头找句柄就太麻烦了
每一个程序在开发之初就存在着一个叫"类名"的概念,类名和句柄每次变更不同,它在定义后几乎是不会发生变化的,所以我们最好是先找到一个程序的类名,后续直接通过类名找到句柄,然后在通过句柄进行真正所需要的操作
vi main.py
import time
import win32api
import win32gui
# 通过句柄获取窗口类名
def get_clasname(hwnd):
clasname = win32gui.GetClassName(hwnd)
print('窗口类名:%s' % (clasname))
return clasname
time.sleep(2)
point = win32api.GetCursorPos()
hwnd = win32gui.WindowFromPoint(point)
#查看窗口类名
get_clasname(hwnd)
可以看到上面我们获取到了文档窗口的类名,现在开始我们直接通过类名去获取句柄
3、通过类名获取句柄
没有找到特定的方法,我们下面大概的思路就是先把主机上所有的句柄id都拿到,通过循环把所有句柄id的类名拿出来然后做对比,对的上的id都留在列表中,所以说如果开启了多个相同程序的窗口,我们也会获取到多个句柄
import time
import win32api
import win32gui
#获取当前主机上的所有句柄id
def get_all_windows():
all_window_handles = []
# 枚举所有窗口句柄,添加到列表中
def enum_windows_proc(hwnd, param):
param.append(hwnd)
return True
# 调用枚举窗口API
win32gui.EnumWindows(enum_windows_proc, all_window_handles)
return all_window_handles #返回的是一个句柄id的列表
#查询传入的句柄id、类名
def get_title(window_handle, class_name):
#查询句柄的类名
window_class = win32gui.GetClassName(window_handle)
#判断窗口类名是否和指定的类名相同,如果相同则返回该窗口句柄,否则返回空值
if window_class == class_name:
return window_handle
#遍历窗口句柄的所有子窗口
def get_child_windows(parent_window_handle):
child_window_handles = []
def enum_windows_proc(hwnd, param):
param.append(hwnd)
return True
#win32gui.EnumChildWindows 遍历窗口句柄的所有子窗口
win32gui.EnumChildWindows(parent_window_handle, enum_windows_proc, child_window_handles)
return child_window_handles
# 根据标题查找窗口句柄
def find_hwnd_by_title(title):
all_windows = get_all_windows() #查询所有句柄
matched_windows = [] #存放所有匹配类名的句柄id
# 在所有窗口中查找标题匹配的窗口句柄
for window_handle in all_windows:
#get_title方法 检查传入句柄对应的类名和我们实际的类名是否对应
window_title = get_title(window_handle, title)
if window_title:
matched_windows.append(window_title) #如果对应就写入列表
# 如果没有匹配到,则在所有子窗口中查找标题匹配的窗口句柄
if matched_windows:
return matched_windows
else:
child_window_handles = []
for parent_window_handle in all_windows:
#不论子窗口是否有数据都追加到列表
child_window_handles.extend(get_child_windows(parent_window_handle))
for child_window_handle in child_window_handles:
if get_title(child_window_handle, title):
matched_windows.append(get_title(child_window_handle, title))
return matched_windows
if __name__ == '__main__':
hwnd = find_hwnd_by_title("Edit")
print(hwnd)
可以看到我们能够直接取到相同类名下所有已经打开的窗口句柄,这样我们甚至可以做个循环加多线程,来实现一个窗口并发的效果
二、模拟按键
上面我们已经拿到了文本文档的一个句柄信息,通过句柄我们可以做很多事情,最常见的就是模拟鼠标和键盘的按键操作,每个操作可能都较为细小琐碎,我们定义一个class类来存放
常见消息类型和标识
#官方参考
https://learn.microsoft.com/zh-cn/windows/win32/inputdev/wm-lbuttondown
消息类型 | 作用 | 消息标识 | 作用 |
WM_MOUSEMOVE | 鼠标 移动 | 移动通用左键右键标识 | |
WM_RBUTTONDOWN | 鼠标 右键按下 | MK_RBUTTON | 左键按下 |
WM_RBUTTONUP | 鼠标 右键释放 | None | 释放时无需标识 |
WM_LBUTTONDOWN | 鼠标 左键按下 | MK_LBUTTON | 右键按下 |
WM_LBUTTONUP | 鼠标 左键释放 | None | 释放时无需标识 |
当按键需要被按下时,需要先声明消息类型,然后标明按键状态
如果鼠标按键需要被释放时,可以直接通过释放按钮来释放
如果指定消息类型是移动时,可以当作已经声明了消息类型,可以直接使用按键标识
使用语法
#在win32api下有个函数PostMessage,是用来与windows api交互的,参数如下
1、要发送消息的目标窗口的句柄
2、发送的消息类型
3、以及消息的参数
win32api.PostMessage(句柄id, 消息类型, 消息标识, 具体的坐标(x,y))
获取目标坐标
#获取坐标
time.sleep(3)
print(win32api.GetCursorPos())
返回
(328, 250)
鼠标按键案例
下面定义了一个类,先去接受我们上面获取到的句柄id,在使用鼠标按键的时候调用win32api.PostMessage函数 去发送给句柄所在的窗口按键信息
在左右键按下的时候才需要定义标识,比如模拟左键时会使用WM_LBUTTONDOWN和MK_LBUTTON ,而松开时使用WM_LBUTTONUP和None
变量pos 是只鼠标按键的坐标,需要通过win32api.MAKELONG 转换数据类型后才能调用
#声明鼠标操作的类
class WinMouse(object):
#初始化函数,接受传入的句柄id
def __init__(self, handle_num: int):
self.handle = handle_num
#鼠标左键按下
def left_button_down(self, pos):
win32api.PostMessage(self.handle, win32con.WM_LBUTTONDOWN, win32con.MK_LBUTTON, pos)
#鼠标左键释放
def left_button_up(self, pos):
win32api.PostMessage(self.handle, win32con.WM_LBUTTONUP, None, pos)
if __name__ == '__main__':
hwnd = find_hwnd_by_title("Edit") #通过类名获取句柄
bd = WinMouse(hwnd[0]) #实例化WinMouse 类,传入句柄值
pos = win32api.MAKELONG(328, 250) #将正常的x,y坐标值转换为特定的数据结构,
#给win32api.PostMessage调用
#按下、等待1s、松开
bd.left_button_down(pos)
time.sleep(1)
bd.left_button_up(pos)
可以看到在下图中,我们运行程序后,不论文本文档是否在前台还是后台,哪怕被遮挡住后也会照常进行鼠标点击(数字太多看不清,大致就是我把鼠标放到末尾,程序在我上面取坐标的地方点一下左键)
其他按键补全
#按下鼠标左键并移动
def mouse_move(self, pos):
win32api.PostMessage(self.handle, win32con.WM_MOUSEMOVE, win32con.MK_LBUTTON, pos)
#按下鼠标右键并移动
def right_button_move(self, pos):
win32api.PostMessage(self.handle, win32con.WM_MOUSEMOVE, win32con.MK_RBUTTON, pos)
#指定坐标按下右键
def right_button_down(self, pos):
win32api.PostMessage(self.handle, win32con.WM_RBUTTONDOWN, win32con.MK_RBUTTON, pos)
#右键释放
def right_button_up(self, pos):
win32api.PostMessage(self.handle, win32con.WM_RBUTTONUP, None, pos)
#模拟左键双击
def left_double_click(self, x_pos:int, y_pos:int, click=2, wait=0.4):
wait = wait / click #click 表示点击次数,wait是的等待时间,意思是双击的间隔
point = win32api.MAKELONG(x_pos, y_pos)
for i in range(click):
self.left_button_down(point)
time.sleep(wait)
self.left_button_up(point)
#右键双击
def right_doubleClick(self, x, y, click=2, wait=0.4):
wait = wait / click
pos = win32api.MAKELONG(x, y)
for i in range(click):
self.right_button_down(pos)
time.sleep(wait)
self.right_button_up(pos)
按键组合函数
上面用一个按左键都要好几行,我们这里在给封装一下
#让他可以直接接收x,y坐标,wait是松开按键的间隔,一般默认即可
#左键单击
def left_click(self, x_pos:int, y_pos:int, wait=0.2):
point = win32api.MAKELONG(x_pos, y_pos)
self.left_button_down(point)
time.sleep(wait)
self.left_button_up(point)
#右键单击
def right_click(self, x_pos:int, y_pos:int, wait=0.2):
point = win32api.MAKELONG(x_pos, y_pos)
self.right_button_down(point)
time.sleep(wait)
self.right_button_up(point)
#模拟左键双击
def left_double_click(self, x_pos:int, y_pos:int, click=2, wait=0.4):
wait = wait / click #click 表示点击次数,wait是的等待时间,意思是双击的间隔
point = win32api.MAKELONG(x_pos, y_pos)
for i in range(click):
self.left_button_down(point)
time.sleep(wait)
self.left_button_up(point)
#右键双击
def right_doubleClick(self, x, y, click=2, wait=0.4):
wait = wait / click
pos = win32api.MAKELONG(x, y)
for i in range(click):
self.right_button_down(pos)
time.sleep(wait)
self.right_button_up(pos)
鼠标滑动拖拽
我们左右键按住到目标坐标后松开的操作稍微复杂一些,单独说下,大致的思路如下
添加偏移值
vi main.py
#计算鼠标从起始点到目标点的偏移过程
def getPointOnLine(start_x, start_y, end_x, end_y, ratio):
x = ((end_x - start_x) * ratio) + start_x
y = ((end_y - start_y) * ratio) + start_y
return int(round(x)), int(round(y))
class WinMouse(object):
def __init__(self, handle_num: int, num_of_steps=80): #添加num_of_steps=80
self.handle = handle_num
self.num_of_steps = num_of_steps #添加偏移值
添加左右键拖动方法
#模拟点击并拖拽目标,接受两对坐标值
def left_click_move(self, x1:int, y1:int, x2:int, y2:int, wait=2):
point1 = win32api.MAKELONG(x1, y1)
self.left_button_down(point1) #起始点按下鼠标左键
#获取我们在init初始化时定义的偏移值
steps = self.num_of_steps
#调用我们上面的方法返回具体,循环0-80的值
#你看这里的循环值是80,也就说会做80次循环操作
#我们传入了起始坐标和目标坐标,而i / steps就相当于起始到结束的偏移位置
#可以理解为从左上角到右下角的点
points = [getPointOnLine(x1, y1, x2, y2, i / steps) for i in range(steps)]
points.append((x2, y2))
wait_time = wait / steps
unique_points = list(set(points))
unique_points.sort(key=points.index)
for point in unique_points:
x, y = point
point = win32api.MAKELONG(x, y)
self.mouse_move(point)
time.sleep(wait_time)
self.left_button_up(point)
#右键单击并滑动批量勾选(与上方函数同理)
def right_click_move(self, start_x, start_y, end_x, end_y, wait=2):
pos = win32api.MAKELONG(start_x, start_y)
self.right_button_down(pos)
steps = self.num_of_steps
points = [getPointOnLine(start_x, start_y, end_x, end_y, i / steps) for i in range(steps)]
points.append((end_x, end_y))
time_per_step = wait / steps
distinct_points = list(set(points))
distinct_points.sort(key=points.index)
for point in distinct_points:
x, y = point
pos = win32api.MAKELONG(x, y)
self.right_button_move(pos)
time.sleep(time_per_step)
self.right_button_up(pos)
拖动有时候突然就不好使了,不清楚原因,稍后补上
三、准备opencv环境
安装模块
pip install opencv-python
pip install pyautogui
pip install pillow
pip install opencv-contrib-python
我这边在安装上opencv-python 后调用cv2下任意方法都提示报黄,没有代码提示,下面列出解决方法
(venv) PS C:\Users\Administrator\IdeaProjects\test> pip install opencv-python
Looking in indexes: https://pypi.tuna.tsinghua.edu.cn/simple
Requirement already satisfied: opencv-python in c:\users\administrator\ideaprojects\test\venv\lib\site-packages (4.7.0.72)
Requirement already satisfied: numpy>=1.21.2 in c:\users\administrator\ideaprojects\test\venv\lib\site-packages (from opencv-python) (1.24.3)
我们在安装成功opencv-python模块后会返回一个安装路径,登录这个路径,进入cv2的目录下,将cv2.pyd 文件放到下面的路径下,重启编辑器即可
c:\users\administrator\ideaprojects\test\venv\lib\site-packages
测试语句
import cv2
# 加载一张图片
img = cv2.imread('11.png', 1) #脚本文件旁边自行准备一个图片
# 显示图片
cv2.imshow('image', img)
cv2.waitKey(0)
cv2.destroyAllWindows()