应用程序使用硬件渲染时的后台截图方法
前言
通常在我们为游戏开发简单的脚本时,因为游戏使用硬件渲染,而导致常用的应用截图方式只能截取到一张全黑的图片。比如使用 ImageGrab,PyQt,pyautogui 等库。
这时使用全屏截图可以避免这个问题。但这样一来,应用始终需要在屏幕上显示,如果同时需要办公或浏览网页,显示器可用空间就会变小就很难受。这时,我们可以使用使用windows API进行截图。在兼顾速度的同时,即使窗口被遮挡也能截图。
本文以一款广为流行的经典放置类游戏 Melvor Idle 为案例.
完整代码
import win32gui
import win32ui
from ctypes import windll
from PIL import Image
def photo_capture():
hwnd = win32gui.FindWindow(None, 'Melvor Idle') # 获取窗口的句柄
# hwnd = 265204 # 或设置窗口句柄
# 如果使用高 DPI 显示器(或 > 100% 缩放尺寸),添加下面一行,否则注释掉
windll.user32.SetProcessDPIAware()
# Change the line below depending on whether you want the whole window
# or just the client area.
# 根据您是想要整个窗口还是只需要 client area 来更改下面的行。
left, top, right, bot = win32gui.GetClientRect(hwnd)
# left, top, right, bot = win32gui.GetWindowRect(hwnd)
w = right - left
h = bot - top
hwndDC = win32gui.GetWindowDC(hwnd) # 根据窗口句柄获取窗口的设备上下文DC(Divice Context)
mfcDC = win32ui.CreateDCFromHandle(hwndDC) # 根据窗口的DC获取mfcDC
saveDC = mfcDC.CreateCompatibleDC() # mfcDC创建可兼容的DC
saveBitMap = win32ui.CreateBitmap() # 创建bitmap准备保存图片
saveBitMap.CreateCompatibleBitmap(mfcDC, w, h) # 为bitmap开辟空间
saveDC.SelectObject(saveBitMap) # 高度saveDC,将截图保存到saveBitmap中
# 选择合适的 window number,如0,1,2,3,直到截图从黑色变为正常画面
result = windll.user32.PrintWindow(hwnd, saveDC.GetSafeHdc(), 3)
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)
if result == 1:
# PrintWindow Succeeded
im.save("test.png") # 调试时可打开,不保存图片可节省大量时间(约0.2s)
return im # 返回图片
else:
print("fail")
photo_capture()
你需要:
- 安装缺少的库。
- 查找目标软件的窗口的名称或句柄(hwnd)。
- 找到合适的 window number(在 PrintWindow() ),详见注释。
- 调试完成后注释掉 im.save("test.png") 优化运行时间。
一、获取并打印窗口句柄
import win32gui
hwnd_title = dict()
def get_all_hwnd(hwnd, mouse):
if win32gui.IsWindow(hwnd) and win32gui.IsWindowEnabled(hwnd) and win32gui.IsWindowVisible(hwnd):
hwnd_title.update({hwnd: win32gui.GetWindowText(hwnd)})
win32gui.EnumWindows(get_all_hwnd, 0)
for h, t in hwnd_title.items():
if t != "":
print(h, t)
运行以上代码,找到窗口的名称,填入。
hwnd = win32gui.FindWindow(None, 'Melvor Idle') # 获取窗口的句柄
二、选择合适的窗口号(window number)
不同的程序运行在不同的桌面,这里的窗口指的是显示器的窗口而不是程序的窗口。通常,这个数字会随着是否使用外接显示器来改变。你可以从零开始依次尝试,大多数人只需要尝试 0,1,2,3,4。我的是3,因为我使用笔记本外接显示器。
# 选择合适的 window number,如0,1,2,3,直到截图从黑色变为正常画面
result = windll.user32.PrintWindow(hwnd, saveDC.GetSafeHdc(), 3)
试着运行一下程序,可以看到目录下保存了一张图片(test.png)。重复更换数字调试直到返回你想要的图片。
三、注释掉非必要代码
找到这行代码,把他删掉或注释掉,这将能节省约0.2秒的时间,极大地提高了运行效率。使用时直接调用函数返回的图片而不是保存后再读取它。
# 调试时可打开,不保存图片可节省大量时间(约0.2s)
im.save("test.png")
如果有需要完整的代码应用实例可以点击在我的博客中查看,是一个自动回血脚本。
拓展 - 未使用GPU渲染时进行截图
pyscreenshot
如果你的程序不使用采用硬件渲染,使用 pyscreenshot 库,方便简洁速度快。详见文档。或直接使用 Pillow。
import pyscreenshot as ImageGrab
# 截取图片
im = ImageGrab.grab(bbox=(0, 0, 1080, 720), childprocess=0)
# 图片保存
im.save("screenshot.jpg")
QtWidget
调用 PyQt5 使用 QtWidget 来截图更为便利,因为它可以在程序被遮挡时进行截图,你不必一直将游戏放在桌面上,并且相对其他库如耗时更短。
from PyQt5.QtWidgets import QApplication
import win32gui
import sys
hwnd = win32gui.FindWindow(None, 'C:\Windows\system32\cmd.exe')
# hwnd = 329824
app = QApplication(sys.argv)
screen = QApplication.primaryScreen()
img = screen.grabWindow(hwnd).toImage()
img.save("screenshot.jpg")
引用
Windows API 截图
Stackoverflow - https://stackoverflow.com/questions/19695214/screenshot-of-inactive-window-printwindow-win32gui/24352388#24352388
其他
凌的博客 - http://www.jiuaitu.com/python/398.html
封面图片
Picspree - Hands typing on laptop