Question solved!
PIL.ImageGrab.grab(bbox=None, include_layered_windows=False, all_screens=True)
And it captures every screen.
Answer from Qiasm on Stack OverflowJust wondering if anyone else has run into problems using pyautogui.locateCenterOnScreen with multiple monitors and if so, did you find any solution to help the function. I keep getting a None value returned when the image is clearly on the screen.
Thanks
I came across a problem today that could be solved by a beginner / intermediate python programmer and a little time. Anyone want to take on a challenge?
The pyautogui.screenshot() function on Windows does not support multiple monitors. This has been a problem for many years, but recently the PIL ImageGrab module (which pyautogui depends on) added the all_screens option.
# primary monitor only import pyautogui im = pyautogui.screenshot() # all monitors from PIL import ImageGrab im = ImageGrab.grab(all_screens=True)
This new feature just needs to be added to pyautogui / pyscreeze. Easy(ish). You would learn a lot, get listed as a contributor to a popular open source project (great for resumes), and it would count toward a hacktoberfest tshirt!
Here's the issue you would close: https://github.com/asweigart/pyautogui/issues/9
Here's the offending line at the core of what needs to be updated: https://github.com/asweigart/pyscreeze/blob/master/pyscreeze/init.py#L427
I've faced this same issue. If I had to guess, I'd assume that ImageGrab is getting its coordinates from the SM_SCREEN flag, which gives the coordinates of the primary monitor only, rather than the SM_VIRTUALSCREEN, which gives the entire virtual screen (I haven't looked through the source though, so just a guess).
That said, it's easy enough to get around this by interacting directly with the Windows API, then converting its bitmap output into a more usable PIL object.
Some Code:
def _get_screen_buffer(self, bounds=None):
# Grabs a DC to the entire virtual screen, but only copies to
# the bitmap the the rect defined by the user.
SM_XVIRTUALSCREEN = 76 # coordinates for the left side of the virtual screen.
SM_YVIRTUALSCREEN = 77 # coordinates for the right side of the virtual screen.
SM_CXVIRTUALSCREEN = 78 # width of the virtual screen
SM_CYVIRTUALSCREEN = 79 # height of the virtual screen
hDesktopWnd = windll.user32.GetDesktopWindow() #Entire virtual Screen
left = windll.user32.GetSystemMetrics(SM_XVIRTUALSCREEN)
top = windll.user32.GetSystemMetrics(SM_YVIRTUALSCREEN)
width = windll.user32.GetSystemMetrics(SM_CXVIRTUALSCREEN)
height = windll.user32.GetSystemMetrics(SM_CYVIRTUALSCREEN)
if bounds:
left, top, right, bottom = bounds
width = right - left
height = bottom - top
hDesktopDC = windll.user32.GetWindowDC(hDesktopWnd)
if not hDesktopDC: print 'GetDC Failed'; sys.exit()
hCaptureDC = windll.gdi32.CreateCompatibleDC(hDesktopDC)
if not hCaptureDC: print 'CreateCompatibleBitmap Failed'; sys.exit()
hCaptureBitmap = windll.gdi32.CreateCompatibleBitmap(hDesktopDC, width, height)
if not hCaptureBitmap: print 'CreateCompatibleBitmap Failed'; sys.exit()
windll.gdi32.SelectObject(hCaptureDC, hCaptureBitmap)
SRCCOPY = 0x00CC0020
windll.gdi32.BitBlt(
hCaptureDC,
0, 0,
width, height,
hDesktopDC,
left, top,
0x00CC0020
)
return hCaptureBitmap
def _make_image_from_buffer(self, hCaptureBitmap):
import Image
bmp_info = BITMAPINFO()
bmp_header = BITMAPFILEHEADER()
hdc = windll.user32.GetDC(None)
bmp_info.bmiHeader.biSize = sizeof(BITMAPINFOHEADER)
DIB_RGB_COLORS = 0
windll.gdi32.GetDIBits(hdc,
hCaptureBitmap,
0,0,
None, byref(bmp_info),
DIB_RGB_COLORS
)
bmp_info.bmiHeader.biSizeImage = bmp_info.bmiHeader.biWidth *abs(bmp_info.bmiHeader.biHeight) * (bmp_info.bmiHeader.biBitCount+7)/8;
size = (bmp_info.bmiHeader.biWidth, bmp_info.bmiHeader.biHeight )
print size
pBuf = (c_char * bmp_info.bmiHeader.biSizeImage)()
windll.gdi32.GetBitmapBits(hCaptureBitmap, bmp_info.bmiHeader.biSizeImage, pBuf)
return Image.frombuffer('RGB', size, pBuf, 'raw', 'BGRX', 0, 1)
The first function gets a bitmap of the screen, and the second guy converts it to a PIL object.
If you don't want to figure out the coordinates of the other monitors yourself, I've got a small module called PyRobot which has functions for targeting specific monitors and whatnot. And it's pure Python, so no need to install PyWin32 :)
Audionautics / chriskiehl !
I took your post and some of your library and re-write it for python 3 ! I hope you're okay with it. It works for me using PILLOW (PIL) and the python 3.4 engine.
I just called the file duelMonitory.py containing:
# https://github.com/chriskiehl/pyrobot
# started from Audionautics code on http://stackoverflow.com/questions/3585293/pil-imagegrab-fails-on-2nd-virtual-monitor-of-virtualbox
# updated for PILLOW and Python 3 by Alan Baines (Kizrak)
from PIL import *
#from ctypes import windll, Structure, byref, c_uint
#import ctypes
import ctypes
from ctypes import *
from ctypes.wintypes import *
def get_screen_buffer(bounds=None):
# Grabs a DC to the entire virtual screen, but only copies to
# the bitmap the the rect defined by the user.
SM_XVIRTUALSCREEN = 76 # coordinates for the left side of the virtual screen.
SM_YVIRTUALSCREEN = 77 # coordinates for the right side of the virtual screen.
SM_CXVIRTUALSCREEN = 78 # width of the virtual screen
SM_CYVIRTUALSCREEN = 79 # height of the virtual screen
hDesktopWnd = windll.user32.GetDesktopWindow() #Entire virtual Screen
left = windll.user32.GetSystemMetrics(SM_XVIRTUALSCREEN)
top = windll.user32.GetSystemMetrics(SM_YVIRTUALSCREEN)
width = windll.user32.GetSystemMetrics(SM_CXVIRTUALSCREEN)
height = windll.user32.GetSystemMetrics(SM_CYVIRTUALSCREEN)
if bounds:
left, top, right, bottom = bounds
width = right - left
height = bottom - top
hDesktopDC = windll.user32.GetWindowDC(hDesktopWnd)
if not hDesktopDC:
print ('GetDC Failed')
sys.exit()
hCaptureDC = windll.gdi32.CreateCompatibleDC(hDesktopDC)
if not hCaptureDC:
print ('CreateCompatibleBitmap Failed')
sys.exit()
hCaptureBitmap = windll.gdi32.CreateCompatibleBitmap(hDesktopDC, width, height)
if not hCaptureBitmap:
print ('CreateCompatibleBitmap Failed')
sys.exit()
windll.gdi32.SelectObject(hCaptureDC, hCaptureBitmap)
SRCCOPY = 0x00CC0020
windll.gdi32.BitBlt(
hCaptureDC,
0, 0,
width, height,
hDesktopDC,
left, top,
0x00CC0020
)
return hCaptureBitmap
def make_image_from_buffer(hCaptureBitmap):
from PIL import Image
bmp_info = BITMAPINFO()
bmp_header = BITMAPFILEHEADER()
hdc = windll.user32.GetDC(None)
bmp_info.bmiHeader.biSize = sizeof(BITMAPINFOHEADER)
DIB_RGB_COLORS = 0
windll.gdi32.GetDIBits(hdc,
hCaptureBitmap,
0,0,
None, byref(bmp_info),
DIB_RGB_COLORS
)
bmp_info.bmiHeader.biSizeImage = int( bmp_info.bmiHeader.biWidth *abs(bmp_info.bmiHeader.biHeight) * (bmp_info.bmiHeader.biBitCount+7)/8 );
size = (bmp_info.bmiHeader.biWidth, bmp_info.bmiHeader.biHeight )
print (size)
pBuf = (c_char * bmp_info.bmiHeader.biSizeImage)()
windll.gdi32.GetBitmapBits(hCaptureBitmap, bmp_info.bmiHeader.biSizeImage, pBuf)
return Image.frombuffer('RGB', size, pBuf, 'raw', 'BGRX', 0, 1)
class BITMAPFILEHEADER(ctypes.Structure):
_fields_ = [
('bfType', ctypes.c_short),
('bfSize', ctypes.c_uint32),
('bfReserved1', ctypes.c_short),
('bfReserved2', ctypes.c_short),
('bfOffBits', ctypes.c_uint32)
]
class BITMAPINFOHEADER(ctypes.Structure):
_fields_ = [
('biSize', ctypes.c_uint32),
('biWidth', ctypes.c_int),
('biHeight', ctypes.c_int),
('biPlanes', ctypes.c_short),
('biBitCount', ctypes.c_short),
('biCompression', ctypes.c_uint32),
('biSizeImage', ctypes.c_uint32),
('biXPelsPerMeter', ctypes.c_long),
('biYPelsPerMeter', ctypes.c_long),
('biClrUsed', ctypes.c_uint32),
('biClrImportant', ctypes.c_uint32)
]
class BITMAPINFO(ctypes.Structure):
_fields_ = [
('bmiHeader', BITMAPINFOHEADER),
('bmiColors', ctypes.c_ulong * 3)
]
And my test code was duelMonitor.test.py containing:
from PIL import *
from duelMonitor import *
hCaptureBitmap = get_screen_buffer()
pimage = make_image_from_buffer(hCaptureBitmap)
pimage.save("Hello.png","PNG")
Basically just combination of Audionautics post and his library from https://github.com/chriskiehl/pyrobot webpage and conversion to Python 3 (and PILLOW).
Have fun!
(PS. I would have posted as comment, but I don't have enough points /sadface)
mss package can easily solve this problem
1. Install the package
pip install mss
2. Take screenshots
First Monitor
import os
import os.path
import mss
def on_exists(fname):
# type: (str) -> None
"""
Callback example when we try to overwrite an existing screenshot.
"""
if os.path.isfile(fname):
newfile = fname + ".old"
print("{} -> {}".format(fname, newfile))
os.rename(fname, newfile)
with mss.mss() as sct:
filename = sct.shot(output="mon-{mon}.png", callback=on_exists)
print(filename)
Second Monitor
import mss
import mss.tools
with mss.mss() as sct:
# Get information of monitor 2
monitor_number = 2
mon = sct.monitors[monitor_number]
# The screen part to capture
monitor = {
"top": mon["top"],
"left": mon["left"],
"width": mon["width"],
"height": mon["height"],
"mon": monitor_number,
}
output = "sct-mon{mon}_{top}x{left}_{width}x{height}.png".format(**monitor)
# Grab the data
sct_img = sct.grab(monitor)
# Save to the picture file
mss.tools.to_png(sct_img.rgb, sct_img.size, output=output)
print(output)
Use with OpenCV
import mss
import cv2
import numpy as np
with mss.mss() as sct:
# Get information of monitor 2
monitor_number = 2
mon = sct.monitors[monitor_number]
# The screen part to capture
monitor = {
"top": mon["top"],
"left": mon["left"],
"width": mon["width"],
"height": mon["height"],
"mon": monitor_number,
}
output = "sct-mon{mon}_{top}x{left}_{width}x{height}.png".format(**monitor)
# Grab the data
sct_img = sct.grab(monitor)
img = np.array(sct.grab(monitor)) # BGR Image
# Display the picture
cv2.imshow("OpenCV", img)
cv2.waitKey(0)
3. Important Notes
sct.monitorswill contain more than one item even if you have a single monitor because the first item will be the combined screens
>>> sct.monitors # if we have a single monitor
[{'left': 0, 'top': 0, 'width': 1366, 'height': 768},
{'left': 0, 'top': 0, 'width': 1366, 'height': 768}]
>>> sct.monitors # if we have two monitors
[{'left': 0, 'top': 0, 'width': 3286, 'height': 1080},
{'left': 1920, 'top': 0, 'width': 1366, 'height': 768},
{'left': 0, 'top': 0, 'width': 1920, 'height': 1080}]
found an solution here!
from PIL import ImageGrab
from functools import partial
ImageGrab.grab = partial(ImageGrab.grab, all_screens=True)
pyautogui.screenshot() will capture all screens.