应用程序使用硬件渲染时的后台截图方法

标签: Python 游戏

前言

通常在我们为游戏开发简单的脚本时,因为游戏使用硬件渲染,而导致常用的应用截图方式只能截取到一张全黑的图片。比如使用 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()

你需要:

  1. 安装缺少的库。
  2. 查找目标软件的窗口的名称或句柄(hwnd)。
  3. 找到合适的 window number(在 PrintWindow() ),详见注释。
  4. 调试完成后注释掉 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