Maksim
Maksim

Reputation: 387

Screenshot of inactive window PrintWindow + win32gui

After hours of googling I managed to "write" this:

import win32gui
from ctypes import windll

hwnd = win32gui.FindWindow(None, 'Steam')

hdc = win32gui.GetDC(hwnd)
hdcMem = win32gui.CreateCompatibleDC(hdc)
    
hbitmap = win32ui.CreateBitmap()
hbitmap = win32gui.CreateCompatibleBitmap(hdcMem, 500, 500)
    
win32gui.SelectObject(hdcMem, hbitmap)
    
windll.user32.PrintWindow(hwnd, hdcMem, 0)

Is this a correct way to do this and how would I save an image?

Upvotes: 23

Views: 43819

Answers (2)

Puddle
Puddle

Reputation: 3345

Here's my fuller script that has the same effect as ALT + PrtSc.

It prints the full window with its 1px border. (Or no border if maximized)

We can copy it to the clipboard just like the combo does.
Or we can convert it to a PIL Image and save it.

import ctypes, win32con, win32gui
import win32clipboard as w32clip
from struct import pack, calcsize
from ctypes import windll, wintypes
from PIL import Image
user32,gdi32 = windll.user32,windll.gdi32
PW_RENDERFULLCONTENT = 2

def getWindowBMAP(hwnd,returnImage=False):
    # get Window size and crop pos/size
    L,T,R,B = win32gui.GetWindowRect(hwnd); W,H = R-L,B-T
    x,y,w,h = (8,8,W-16,H-16) if user32.IsZoomed(hwnd) else (7,0,W-14,H-7)

    # create dc's and bmp's
    dc = user32.GetWindowDC(hwnd)
    dc1,dc2 = gdi32.CreateCompatibleDC(dc),gdi32.CreateCompatibleDC(dc)
    bmp1,bmp2 = gdi32.CreateCompatibleBitmap(dc,W,H),gdi32.CreateCompatibleBitmap(dc,w,h)

    # render dc1 and dc2 (bmp1 and bmp2) (uncropped and cropped)
    obj1,obj2 = gdi32.SelectObject(dc1,bmp1),gdi32.SelectObject(dc2,bmp2) # select bmp's into dc's
    user32.PrintWindow(hwnd,dc1,PW_RENDERFULLCONTENT) # render window to dc1
    gdi32.BitBlt(dc2,0,0,w,h,dc1,x,y,win32con.SRCCOPY) # copy dc1 (x,y,w,h) to dc2 (0,0,w,h)
    gdi32.SelectObject(dc1,obj1); gdi32.SelectObject(dc2,obj2) # restore dc's default obj's

    if returnImage: # create Image from bmp2
        data = ctypes.create_string_buffer((w*4)*h)
        bmi = ctypes.c_buffer(pack("IiiHHIIiiII",calcsize("IiiHHIIiiII"),w,-h,1,32,0,0,0,0,0,0))
        gdi32.GetDIBits(dc2,bmp2,0,h,ctypes.byref(data),ctypes.byref(bmi),win32con.DIB_RGB_COLORS)
        img = Image.frombuffer('RGB',(w,h),data,'raw','BGRX')

    # clean up
    gdi32.DeleteObject(bmp1) # delete bmp1 (uncropped)
    gdi32.DeleteDC(dc1); gdi32.DeleteDC(dc2) # delete created dc's
    user32.ReleaseDC(hwnd,dc) # release retrieved dc

    return (bmp2,w,h,img) if returnImage else (bmp2,w,h)

def copyBitmap(hbmp): # copy HBITMAP to clipboard
    w32clip.OpenClipboard(); w32clip.EmptyClipboard()
    w32clip.SetClipboardData(w32clip.CF_BITMAP,hbmp); w32clip.CloseClipboard()

def copySnapshot(hwnd): # copy Window HBITMAP to clipboard
    hbmp,w,h = getWindowBMAP(hwnd); copyBitmap(hbmp); gdi32.DeleteObject(hbmp)

def getSnapshot(hwnd): # get Window HBITMAP as Image
    hbmp,w,h,img = getWindowBMAP(hwnd,True); gdi32.DeleteObject(hbmp); return img


hwnd = windll.kernel32.GetConsoleWindow()                         # Console
if not hwnd: hwnd = user32.FindWindowW("Notepad",None)            # Notepad
if not hwnd: hwnd = user32.FindWindowW("Chrome_WidgetWin_1",None) # Chrome
if not hwnd: hwnd = user32.FindWindowW("CabinetWClass",None)      # Windows Explorer

if not hwnd: print("Couldn't find Window")
elif user32.IsIconic(hwnd): print("Window is minimized")
else:
    print(win32gui.GetWindowText(hwnd))
    print(win32gui.GetClassName(hwnd))
    copySnapshot(hwnd)
    img = getSnapshot(hwnd)
    img.save("snapshot.png")
    #img.show()
    print("Snapshot Saved!")

Explanation:

If we did a basic PrintWindow, it renders a Windows 7 style frame/border which is 7 pixels thicker at the Left/Right/Bottom. It's also at the top but only when maximized.

We can use the hidden flag PW_RENDERFULLCONTENT to render correctly, but that extra border space still exists and remains blank. So our image is padded with thick black borders.

So we need to crop our HBITMAP.
To do this we can create a new bitmap and use BitBlt with our first bitmap's DC as the source.

From here we can use win32clipboard to copy the HBITMAP just like ALT + PrtSc.


HBITMAP to PIL:

We can use GetDIBits to get the raw pixel data in an array of size w*h*4.

It needs the BITMAPINFOHEADER struct to recieve the width, height, planes, bitCount.

One way to do it:

class BITMAPINFOHEADER(ctypes.Structure):
    _fields_ = [
        ("biSize", wintypes.DWORD),
        ("biWidth", ctypes.c_long),
        ("biHeight", ctypes.c_long),
        ("biPlanes", wintypes.WORD),
        ("biBitCount", wintypes.WORD),
        ("biCompression", wintypes.DWORD),
        ("biSizeImage", wintypes.DWORD),
        ("biXPelsPerMeter", ctypes.c_long),
        ("biYPelsPerMeter", ctypes.c_long),
        ("biClrUsed", wintypes.DWORD),
        ("biClrImportant", wintypes.DWORD)
    ]
    def __init__(self,w,h,p=1,b=32):
        self.biSize = ctypes.sizeof(BITMAPINFOHEADER)
        self.biWidth,self.biHeight,self.biPlanes,self.biBitCount = w,h,p,b

bmi = BITMAPINFOHEADER(w,-h) # we use -h to flip the image also

But a much more compact way:

bmi = ctypes.c_buffer(pack("IiiHHIIiiII",calcsize("IiiHHIIiiII"),w,-h,1,32,0,0,0,0,0,0))

We use struct to pack values as specific data types.

i = int            (c_long)
I = unsigned int   (DWORD)
H = unsigned short (WORD)

We use calcsize("IiiHHIIiiII") to do the equivalent of ctypes.sizeof(BITMAPINFOHEADER).


Here's how to get the Client only:

PW_CLIENTONLY,PW_RENDERFULLCONTENT = 1,2

def getClientBMAP(hwnd,returnImage=False):
    # get Client size
    L,T,R,B = win32gui.GetClientRect(hwnd); w,h = R-L,B-T

    # create dc's and bmp's
    dc = user32.GetWindowDC(hwnd)
    dc1 = gdi32.CreateCompatibleDC(dc)
    bmp1 = gdi32.CreateCompatibleBitmap(dc,w,h)

    # render dc1 (bmp1)
    obj1 = gdi32.SelectObject(dc1,bmp1) # select bmp into dc
    user32.PrintWindow(hwnd,dc1,PW_CLIENTONLY|PW_RENDERFULLCONTENT) # render window to dc1
    gdi32.SelectObject(dc1,obj1) # restore dc's default obj

    if returnImage: # create Image from bmp1
        data = ctypes.create_string_buffer((w*4)*h)
        bmi = ctypes.c_buffer(pack("IiiHHIIiiII",calcsize("IiiHHIIiiII"),w,-h,1,32,0,0,0,0,0,0))
        gdi32.GetDIBits(dc1,bmp1,0,h,ctypes.byref(data),ctypes.byref(bmi),win32con.DIB_RGB_COLORS)
        img = Image.frombuffer('RGB',(w,h),data,'raw','BGRX')

    # clean up
    gdi32.DeleteDC(dc1) # delete created dc
    user32.ReleaseDC(hwnd,dc) # release retrieved dc

    return (bmp1,w,h,img) if returnImage else (bmp1,w,h)

def getClientSnapshot(hwnd): # get Client HBITMAP as Image
    hbmp,w,h,img = getClientBMAP(hwnd,True); gdi32.DeleteObject(hbmp); return img

Upvotes: 0

hazzey
hazzey

Reputation: 1258

After lots of searching and trying various different methods, the following worked for me.

import win32gui
import win32ui
from ctypes import windll
from PIL import Image

hwnd = win32gui.FindWindow(None, 'Calculator')

# Uncomment the following line if you use a high DPI display or >100% scaling size
# windll.user32.SetProcessDPIAware()

# Change the line below depending on whether you want the whole window
# or just the 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)
mfcDC  = win32ui.CreateDCFromHandle(hwndDC)
saveDC = mfcDC.CreateCompatibleDC()

saveBitMap = win32ui.CreateBitmap()
saveBitMap.CreateCompatibleBitmap(mfcDC, w, h)

saveDC.SelectObject(saveBitMap)

# Change the line below depending on whether you want the whole window
# or just the client area. 
#result = windll.user32.PrintWindow(hwnd, saveDC.GetSafeHdc(), 1)
result = windll.user32.PrintWindow(hwnd, saveDC.GetSafeHdc(), 0)
print result

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")

Upvotes: 71

Related Questions