窗口模块与截图
什么是pywin32库
pywin32是一个第三方模块库,主要的作用是方便python开发者快速调用windows API的一个模块库。
同时pywin32也是绝大部分windows上第三方python模块库的前提,比如wmi,如果没有安装pywin32是无法正常使用wmi这个三方模块库的。
安装pywin32
由于pywin32是第三方模块库,Python并没有自带,所以我们使用之前必须先按照,安装也很简单,只需要再pycharm下方终端窗口中输入pip install pywin32 回车就可以自动下载安装了。
看到success这样的字样则是说明安装成功了。
如果你不放心,还可以通过 pip list 可以看到pywin32是否已经安装成功了
pywin32提供的模块
Pywin32提供的模块有很多,常用的几个如下:
- win32api 提供了常用的用户API函数
- win32gui 提供了有关windows用户界面图形操作的API函数
- win32con 提供了定义windows Api内的宏
- win32clipboard 提供了有关粘贴板的API函数
- win32console 提供了有关控制台的API函数
- win32service 提供了有关服务操作的API函数
- win32file 提供了有关文件操作的API函数
我们平时最常用的的模块有3个, win32api, win32gui,win32con,下面我们就导入 win32api,win32con这两个模块写一个简单的弹出框,顺便验证下我们pywin32是否安装成功了。 上代码:
import win32api import win32con win32api.MessageBox(0, "我是渣男教父", "我是标题", win32con.MB_OK | win32con.MB_ICONWARNING)
运行下看看:
导入模块后,我们仅用了一行代码就创建了自定义的弹出框。
什么是窗口句柄
这里我们要先说下句柄的概念,通俗点说句柄就是窗口的身份证号,是一串整数。举个栗子,你有你自己的身份证号,一报身份证号,你应该知道是你了。你也有名字,但是大家都知道名字并不是唯一的,你可以叫张三,别人也可以叫张三,而且没有数字来得方便。所以,窗口句柄就相当于身份证号,每个窗口都有一个唯一编号, 操作系统用这个编号来发送消息。这就是操作系统的消息机制。
获取到窗口句柄,我们就可以通过这个句柄对窗口进行操作了。
查找窗口句柄
pywin32是如何查找窗口句柄的,这里我们用到了win32gui模块,以及模块的FindWindow方法。直接看代码看看
import win32gui hwnd = win32gui.FindWindow(None, '雷电模拟器') print(hwnd) hwnd2 = win32gui.FindWindow(None, '英雄联盟') print(hwnd2)
运行下看看:
5506644 0我们现在详细解析下win32gui.FindWindow(0, wdname)这个函数,这个函数其实就是根据窗口标题获取窗口句柄
# 函数功能:该函数获得一个顶层窗口的句柄,该窗口的类名和窗口名与给定的字符串相匹配。这个函数不查找子窗口。在查找时不区分大小写 # 参数1 窗口类名 # 参数2 窗口标题--必须完整;如果该参数为None,则为所有窗口全匹配 # 返回值:如果函数成功,返回值为窗口句柄;如果函数失败,返回值为0因为我电脑上现在打开了雷电模拟器,有这个窗口,所以结果打印了雷电模拟器窗口对应的句柄数字,我没有打开英雄联盟的窗口,所以没有得到相应的句柄,返回值为0。
激活窗口
我们大多数情况下操作窗口都是需要窗口在最前方,这样对窗口的操作才会行之有效。所以接下来我们来写一个通用的方法来查找并激活窗口,如果查找到就使之前台显示并返回句柄。我们将文件名保存为获取句柄激活.py 直接上代码:
import win32gui import time def activate_window(title): hwnd = win32gui.FindWindow(None, title) if hwnd == 0: print('未找到游戏窗口') return 0 if win32gui.GetForegroundWindow() != hwnd: win32gui.SetForegroundWindow(hwnd) time.sleep(1) return hwnd def activate_emulator(): return activate_window('雷电模拟器') if __name__ == '__main__': print(activate_emulator())运行下:
(先手动将窗口放置到后台,不要最小化,最小化会有问题。)
这样我们就实现了将后台窗口重新激活放置到前台了。
这里又用到了两个win32gui的新方法。
- win32gui.GetForegroundWindow()这个方法是获取当前前台窗口的句柄;
- win32gui.SetForegroundWindow(hwnd)这个方法则是将hwnd对应的窗口前台显示。
程序的逻辑也很简单,首先判断雷电模拟器的窗口是否存在,如果存在则检查当前前台窗口hwnd与雷电模拟器的窗口hwnd是否一致,如果一致则不需要处理,否则就将雷电模拟器前台显示,最后再返回hwnd信息。
截图
平时大家聊qq时经常会用到截图功能,这是一个相当实用的功能。那我们今天也用Python来写一个截图功能。我们先看下桌面截图的代码,这个代码是只截取了部分代码,并不完整,只是用于展示。后面会有完整代码。 上代码:
import win32gui import win32ui import win32con import numpy as np def capture_screen(hwnd): # 根据窗口句柄获取窗口的设备上下文DC(Divice Context) desktop = win32gui.GetDesktopWindow() dc = win32gui.GetWindowDC(desktop) # 根据窗口的DC获取mfcDC mfc_dc = win32ui.CreateDCFromHandle(dc) # mfcDC创建可兼容的DC save_dc = mfc_dc.CreateCompatibleDC() # 创建bitmap准备保存图片 save_bit_map = win32ui.CreateBitmap() left, top, right, bottom = win32gui.GetWindowRect(hwnd) w, h = right - left, bottom - top # 为bitmap开辟空间 save_bit_map.CreateCompatibleBitmap(mfc_dc, w, h) # 高度saveDC,将截图保存到saveBitmap中 save_dc.SelectObject(save_bit_map) # 截取从左上角(0,0)长宽为(w,h)的图片 save_dc.BitBlt((0, 0), (w, h), mfc_dc, (left, top), win32con.SRCCOPY) signed_ints_array = save_bit_map.GetBitmapBits(True) im_opencv = np.frombuffer(signed_ints_array, dtype='uint8') im_opencv.shape = (h, w, 4) save_dc.DeleteDC() win32gui.DeleteObject(save_bit_map.GetHandle()) win32gui.ReleaseDC(hwnd, dc) return im_opencvfrombuffer(buffer, dtype=float, count=-1, offset=0) 作用:将缓冲区data以流的形式读入转化成ndarray对象
Parameters:
- buffer:目标缓冲区对象
- dtype:默认浮点型
- count = -1表示缓冲区中所有数据
- offset =0,从0开始读取
截图方法最终返回的是一个ndarray对象,也就是一个数字组成的矩阵。
截图方法写好了,这个方法用到的知识点比较繁琐,大家暂时知道怎么用就好了,只需要传入窗口句柄,就可以得到相应的图像了。结合前面说到的获取句柄模块 【 获取句柄激活.py 】 ,我们来截图看看最终结果的ndarray是怎样的,上代码:
import win32gui import win32ui import win32con import numpy as np import get_handle_activate as window def capture_screen(hwnd): # 根据窗口句柄获取窗口的设备上下文DC(Divice Context) desktop = win32gui.GetDesktopWindow() dc = win32gui.GetWindowDC(desktop) # 根据窗口的DC获取mfcDC mfc_dc = win32ui.CreateDCFromHandle(dc) # mfcDC创建可兼容的DC save_dc = mfc_dc.CreateCompatibleDC() # 创建bitmap准备保存图片 save_bit_map = win32ui.CreateBitmap() left, top, right, bottom = win32gui.GetWindowRect(hwnd) w, h = right - left, bottom - top # 为bitmap开辟空间 save_bit_map.CreateCompatibleBitmap(mfc_dc, w, h) # 高度saveDC,将截图保存到saveBitmap中 save_dc.SelectObject(save_bit_map) # 截取从左上角(0,0)长宽为(w,h)的图片 save_dc.BitBlt((0, 0), (w, h), mfc_dc, (left, top), win32con.SRCCOPY) signed_ints_array = save_bit_map.GetBitmapBits(True) im_opencv = np.frombuffer(signed_ints_array, dtype='uint8') im_opencv.shape = (h, w, 4) save_dc.DeleteDC() win32gui.DeleteObject(save_bit_map.GetHandle()) win32gui.ReleaseDC(hwnd, dc) return im_opencv def get_emulator_image(): hwnd = window.activate_emulator() return capture_screen(hwnd) if __name__ == '__main__': image = get_emulator_image() print(image)main函数作为代码入口,我们看看截图的流程,其实就是两步,第一步,找到并激活窗口获得它的hwnd,第二步根据它的hwnd截图,是不是很简单。
我们运行下看看效果:
[[[ 65 63 60 255] [ 65 63 60 255] [ 65 63 60 255] ... [153 136 123 255] [195 170 144 255] [195 182 168 255]] [[ 65 63 60 255] [ 65 63 60 255] [ 65 63 60 255] ...以下省略这个数据看起来比较复杂,这里我们先暂时不用管它,接下来我们还会讲到opencv的模块,到时候我们再来处理这个数据。
这里需要注意一点,电脑显示设置中的缩放与布局必须是100%,否则截图会不对。
ps:上面面代码中的桌面截图()方法以及激活窗口()方法是可以复用的,因为我们后面会大量用到激活雷电模拟器和截取模拟器的图像,所以我们又封装了激活模拟器窗口()跟获取模拟器图像()两个函数。这个文件可保存为窗口截图.py。
本章小结
这一章咱们重新梳理了Windows窗口操作和鼠标模拟的核心知识,来捋一捋重点:
pywin32是啥玩意儿?就是个让Python能调用Windows API的库。装上它,Python就能操控Windows的各种功能,比如操作窗口、模拟鼠标、访问剪贴板等等。最常用的是三兄弟:win32api(常用API函数)、win32gui(窗口图形操作)、win32con(Windows常量定义)。
窗口句柄怎么理解的?句柄就是窗口的身份证号,一串唯一的整数。每个窗口都有一个编号,操作系统用这个编号来发送消息和操作窗口。用win32gui.FindWindow(类名, 标题)就能查到句柄,找不到返回0。
怎么激活窗口?很多操作都得窗口在前台才有效。用SetForegroundWindow(hwnd)就能把窗口提到最前面。但要注意窗口不能是最小化状态,最小化了会有问题。可以先用GetWindowPlacement()检查窗口状态,如果最小化了先用SendMessage()恢复。
截图咋实现的?截图流程看着复杂,其实就几步:拿到窗口句柄 → 获取设备上下文 → 创建兼容DC和位图 → 把窗口内容拷贝到位图 → 转成numpy数组。返回的是一个四维矩阵,每个像素点包含RGBA四个通道的值。这个numpy数组后面配合OpenCV就能做各种图像处理了。
鼠标点击怎么模拟的?一套完整的点击分三步:SetCursorPos()移动鼠标到目标位置 →mouse_event(MOUSEEVENTF_LEFTDOWN)按下左键 →mouse_event(MOUSEEVENTF_LEFTUP)释放左键。这三步缺一不可。
屏幕坐标和窗口坐标啥区别?屏幕坐标以整个屏幕左上角为(0,0),窗口坐标以窗口左上角为(0,0)。想点击窗口里的某个位置,得用ClientToScreen(hwnd, 窗口坐标)把窗口坐标转成屏幕坐标,不然鼠标会点错地方。
核心套路是啥?就一句话:找到窗口 → 激活窗口 → 截图/操作。这三板斧练熟了,后面写游戏自动化、做脚本工具,都是这个套路。
这些知识点是游戏自动化的基础,后面学OpenCV图像处理、模板匹配、轮廓检测,都是在这个基础上进行的。把基础打牢了,后面的内容就水到渠成了。
动手试试
选择题
1、获取窗口句柄的函数是哪个?
- A.
GetWindow() - B.
FindWindow() - C.
SearchWindow() - D.
LocateWindow()
答案:B
解析:win32gui.FindWindow(类名, 标题)是用来获取窗口句柄的函数。第一个参数是窗口类名,一般传None就行;第二个参数是窗口标题,必须完整匹配。如果找到了就返回句柄(一个整数),找不到返回0。还有个类似的函数叫FindWindowEx(),是用来查找子窗口的,比FindWindow()多一个父窗口参数。另外要注意,FindWindow()查找时不区分大小写。
2、将窗口置前的函数是哪个?
- A.
SetWindowPos() - B.
SetForegroundWindow() - C.
BringWindowToTop() - D.
ShowWindow()
答案:B
解析:SetForegroundWindow(hwnd)是用来把窗口提到最前面的函数,传入窗口句柄就行。这个函数会让窗口获得焦点并显示在最前面。那其他几个呢?SetWindowPos()功能更强大,可以设置窗口位置、大小、层级等;BringWindowToTop()是把窗口提到Z序顶部,但不一定获得焦点;ShowWindow()是控制窗口的显示状态(显示、隐藏、最小化、最大化等)。一般情况下用SetForegroundWindow()就够了。
编程挑战
3、编写一个程序,获取当前活动窗口的句柄并截图保存。
参考代码:
import win32gui import win32ui import win32con import numpy as np import cv2 as cv import time def get_active_window_handle(): """获取当前活动窗口的句柄""" hwnd = win32gui.GetForegroundWindow() if hwnd == 0: print('未找到活动窗口') return None # 获取窗口标题 title = win32gui.GetWindowText(hwnd) print(f'当前活动窗口:{title},句柄:{hwnd}') return hwnd def capture_window(hwnd, save_path='screenshot.png'): """根据窗口句柄截图并保存""" # 获取窗口位置 left, top, right, bottom = win32gui.GetWindowRect(hwnd) w = right - left h = bottom - top # 获取设备上下文 hwnd_dc = win32gui.GetWindowDC(hwnd) mfc_dc = win32ui.CreateDCFromHandle(hwnd_dc) save_dc = mfc_dc.CreateCompatibleDC() # 创建位图 bitmap = win32ui.CreateBitmap() bitmap.CreateCompatibleBitmap(mfc_dc, w, h) save_dc.SelectObject(bitmap) # 截图 save_dc.BitBlt((0, 0), (w, h), mfc_dc, (0, 0), win32con.SRCCOPY) # 转成numpy数组 signed_ints_array = bitmap.GetBitmapBits(True) img_array = np.frombuffer(signed_ints_array, dtype='uint8') img_array.shape = (h, w, 4) # 清理资源 save_dc.DeleteDC() win32gui.DeleteObject(bitmap.GetHandle()) win32gui.ReleaseDC(hwnd, hwnd_dc) # 去掉Alpha通道,转成BGR格式(OpenCV默认格式) img_bgr = cv.cvtColor(img_array, cv.COLOR_BGRA2BGR) # 保存 cv.imwrite(save_path, img_bgr) print(f'截图已保存到 {save_path},分辨率:{w}x{h}') return img_bgr if __name__ == '__main__': print('请在3秒内切换到要截图的窗口...') time.sleep(3) # 获取当前活动窗口 hwnd = get_active_window_handle() if hwnd: # 截图保存 capture_window(hwnd, '活动窗口截图.png') # 显示截图 img = cv.imread('活动窗口截图.png') cv.imshow('活动窗口截图', img) print('按任意键关闭窗口') cv.waitKey(0) cv.destroyAllWindows()思路解析:
这道题考察的是窗口操作和截图的综合运用。
首先,获取当前活动窗口用win32gui.GetForegroundWindow()就行,这个函数返回当前在最前面的窗口句柄。跟FindWindow()不一样,GetForegroundWindow()不需要知道窗口标题,直接就能拿到当前活动窗口。用GetWindowText(hwnd)还能拿到窗口的标题文字。
其次,截图的流程跟前面学的一样:获取设备上下文 → 创建兼容DC → 创建位图 → 拷贝内容 → 转numpy数组。这里多了一步处理:截图返回的是RGBA四通道的数据,但OpenCV默认用的是BGR三通道格式,所以要用cv.cvtColor(img_array, cv.COLOR_BGRA2BGR)去掉Alpha通道,不然保存的图片会有问题。
然后,代码里加了个3秒倒计时,让你有时间切换到要截图的窗口。这个在实际使用中很实用,比如要截图某个弹出窗口,先运行脚本再快速切换过去。
最后,用cv.imwrite()保存图片,再用cv.imshow()显示出来看看效果。cv.waitKey(0)表示等待按键,按任意键后关闭窗口。
几个实用小技巧:
- 截图前确保窗口不是最小化状态,最小化的窗口截图会是空的或者内容不对。
- 电脑显示设置中的缩放与布局必须是100%,否则截图尺寸会不对。
- 如果截图保存后打不开,检查一下路径里有没有中文,OpenCV不支持含中文路径的文件保存。