General information:
I packaged my project as an exe using pyinstaller and ran it. At some point on the 6th day, I reported a ScreenShotError error when taking a screenshot, and an AttributeError error occurred in subsequent screenshots.
ERROR 2023/09/13 22:06:25 [pool_exception_util.py:18] :
Traceback (most recent call last):
File "utils\pool_exception_util.py", line 15, in __call__
File "core\mouse_monitor.py", line 229, in async_do_click
File "core\mouse_monitor.py", line 266, in save_fs_img
File "mss\base.py", line 90, in grab
File "mss\windows.py", line 252, in _grab_impl
mss.exception.ScreenShotError: gdi32.GetDIBits() failed.
ERROR 2023/09/13 22:06:36 [pool_exception_util.py:18] :
Traceback (most recent call last):
File "utils\pool_exception_util.py", line 15, in __call__
File "core\mouse_monitor.py", line 229, in async_do_click
File "core\mouse_monitor.py", line 266, in save_fs_img
File "mss\base.py", line 90, in grab
File "mss\windows.py", line 250, in _grab_impl
AttributeError: '_thread._local' object has no attribute 'data'
ERROR 2023/09/13 22:06:37 [pool_exception_util.py:18] :
Traceback (most recent call last):
File "utils\pool_exception_util.py", line 15, in __call__
File "core\mouse_monitor.py", line 229, in async_do_click
File "core\mouse_monitor.py", line 266, in save_fs_img
File "mss\base.py", line 90, in grab
File "mss\windows.py", line 250, in _grab_impl
AttributeError: '_thread._local' object has no attribute 'data'
and so on ...
First of all, thank you very much for providing a high-performance screenshot tool, I like it very much!
The following is a detailed description and part of the code for the problem I encountered.
I used pynput.mouse to monitor the mouse. When the mouse is left-clicked on the specified area in the program window I specified, I will take a screenshot and save some other additional coordinate information. Then I used pyinstaller to package my project as an exe and run it, but after about 6 days of running it threw a ScreenShotError at some point, and all subsequent clicks would throw an exception AttributeError: _data, and subsequently I obtained the system zoom and foreground window coordinates. , the xy value passed by pynput will become 0 !
I checked the relevant issues and seemed to not find a solution to the problem related to me. Then I tried to read your source code and found that the location where the exception was thrown was in mss/windows. py:252:
gdi.BitBlt(memdc, 0, 0, width, height, srcdc, monitor["left"], monitor["top"], SRCCOPY | CAPTUREBLT)
bits = gdi.GetDIBits(memdc, self._handles.bmp, 0, height, self._handles.data, self._handles.bmi, DIB_RGB_COLORS)
if bits != height:
raise ScreenShotError("gdi32.GetDIBits() failed.")
it judged bits != height. I checked my logs. When the last error was reported, the monitor parameter I passed was correct: (3, 22, 794, 629)
INFO 2023/09/13 22:06:25 [mouse_monitor.py:120] origin window rect:(3, 22, 794, 629)
INFO 2023/09/13 22:06:25 [mouse_monitor.py:128] sys scaling: 1.0
INFO 2023/09/13 22:06:25 [mouse_monitor.py:146] di area:min:(290,34) max:(769,290) click coord(relative client):(332,287)
INFO 2023/09/13 22:06:25 [mouse_monitor.py:159] clicked in di area, start processing
INFO 2023/09/13 22:06:25 [mouse_monitor.py:228] save screenshot
INFO 2023/09/13 22:06:25 [mouse_monitor.py:261] screenshot coord:[3, 22, 794, 629]
WARNING 2023/09/13 22:06:25 [pool_exception_util.py:17] An error occurred, see error.log
so it can only be the value of bits has a problem. But sorry, I don't know much about win32api, so I went to Microsoft's official website to check the relevant documents and roughly understood the role of BitBlt/GetDIBits/GetWindowDC. I guess the root cause may be that an invalid handle was obtained when windows.py:112 was initialized causing this error:
self._handles.srcdc = self.user32.GetWindowDC(0)
I briefly checked the code of pynput.mouse and learned that it is a single-thread processing callback function, which means that the first time this error occurred in its internal sub-thread, it affected all my subsequent code that calls win32api, just like this:
INFO 2023/09/13 22:06:27 [mouse_monitor.py:120] origin window rect:(0, 0, 0, 0)
INFO 2023/09/13 22:06:27 [mouse_monitor.py:128] sys scaling: 0.0
INFO 2023/09/13 22:06:27 [mouse_monitor.py:146] di area:min:(0, 0) max:(0, 0) click coord(relative client):(0, 0)
INFO 2023/09/13 22:06:27 [mouse_monitor.py:159] clicked in di area, start processing
INFO 2023/09/13 22:06:27 [mouse_monitor.py:228] save screenshot
INFO 2023/09/13 22:06:27 [mouse_monitor.py:261] screenshot coord:[0, 0, 0, 0]
WARNING 2023/09/13 22:06:27 [pool_exception_util.py:17] An error occurred, see error.log
Of course, the above It's just my speculation based on the documentation and source code. I hope you can reply. Thank you again!
def get_physical_resolution():
hDC = win32gui.GetDC(0)
wide = win32print.GetDeviceCaps(hDC, win32con.DESKTOPHORZRES)
high = win32print.GetDeviceCaps(hDC, win32con.DESKTOPVERTRES)
return {"wide": wide, "high": high}
def get_virtual_resolution():
wide = win32api.GetSystemMetrics(0)
high = win32api.GetSystemMetrics(1)
return {"wide": wide, "high": high}
def get_win10_scaling_old():
"""
I know this method will conflict with SetProcessDpiAwareness(2), so I have changed the acquisition method. This is the method at that time.
"""
warnings.warn('This method will conflict with mss setting dpi awareness and is no longer used. Only the writing method is retained.', DeprecationWarning)
real_resolution = get_physical_resolution()
screen_size = get_virtual_resolution()
proportion = round(real_resolution['wide'] / screen_size['wide'], 2)
return proportion
import mss
import yaml
import time
import logging
import mss.tools
import pynput.mouse as pm
import utils.active_window as aw
from utils.conf_util import load_conf
from utils.thread_pool_util import THREAD_POOL
from utils.screenutils import get_scaling, get_win_version
from utils.win10_scaling_util import initialize_dpi_awareness
from utils.pool_exception_util import ThreadPoolExceptionLogger
base_conf = {}
save_timestamp_list = []
def start_catch_mouse():
if get_win_version() == 'win10': # win7 try to get shcore will raise exception
initialize_dpi_awareness()
pml = pm.Listener(on_click=on_click)
pml.start()
def loading_conf():
global base_conf
base_conf = load_conf()
def on_click(x, y, button, pressed):
if not pressed and button.name == 'left':
loading_conf()
# listen program name
listen_exe_name = base_conf['exeName'].strip().lower()
pname, wtext = aw.get_active_window_process_name()
if pname.strip().lower() == listen_exe_name:
# sys scaling
scaling = get_scaling()
logging.info(f'sys scaling: {scaling}')
# listen program title
main_window_text = base_conf['mainWindowText'].strip().lower()
if wtext.strip().lower() == main_window_text:
rect = aw.get_active_window_client_rect()
if rect and type(rect) == tuple:
logging.info(f'origin window rect:{rect}')
rect = list(rect)
rect[0] = rect[0] if rect[0] >= 0 else 0
rect[1] = rect[1] if rect[1] >= 0 else 0
rx = x - rect[0]
ry = y - rect[1]
if wtext.strip().lower() == main_window_text: # 在主界面单击
dl_coord = base_conf['dataListCoord']
dl_minx = round(dl_coord[0] * scaling)
dl_miny = round(dl_coord[1] * scaling)
dl_maxx = round(dl_minx + (dl_coord[2]) * scaling)
dl_maxy = round(dl_miny + dl_coord[3] * scaling)
logging.info(
f'di area:min:({dl_minx},{dl_miny}) max:({dl_maxx},{dl_maxy}) click coord(relative):({rx},{ry})')
if dl_minx <= rx <= dl_maxx and dl_miny <= ry <= dl_maxy: # 在di区域点击
logging.info('clicked in di area, start processing')
# 新线程执行双击操作
THREAD_POOL.submit(ThreadPoolExceptionLogger(async_do_click), x, y, rect, scaling)
def async_do_click(x, y, rect):
logging.info('save screenshot')
save_img_id, adj_rect = save_fs_img(rect)
save_data(x, y, save_img_id, adj_rect)
global save_timestamp_list
save_timestamp_list.append(save_img_id)
def save_fs_img(rect):
save_img_dir = 'path/to/save/img'
save_img_id = int(round(time.time() * 1000))
logging.info(f'screenshot coord:{rect}')
grab_coord = tuple(rect)
img_path = f'{save_img_dir}/fs-{save_img_id}.png'
with mss.mss() as m:
img = m.grab(grab_coord)
mss.tools.to_png(img.rgb, img.size, output=img_path)
return save_img_id, grab_coord
def save_data(x, y, save_img_id, rect):
data = {
'x': x,
'y': y,
'rect': rect,
}
main_dir = base_conf['saveMainDir']
data_dir = base_conf['saveDataDir']
data_name = save_img_id
with open(f'{main_dir}/{data_dir}/{data_name}.yml', 'w', encoding='utf8') as f:
yaml.dump(data, f, allow_unicode=True)
def get_active_window_client_rect():
hwnd = win32gui.GetForegroundWindow()
client_rect = win32gui.GetClientRect(hwnd)
left, top = win32gui.ClientToScreen(hwnd, (client_rect[0], client_rect[1]))
right, bottom = win32gui.ClientToScreen(hwnd, (client_rect[2], client_rect[3]))
return left, top, right, bottom
Pay now to fund the work behind this issue.
Get updates on progress being made.
Maintainer is rewarded once the issue is completed.
You're funding impactful open source efforts
You want to contribute to this effort
You want to get funding like this too