Rewrite overlay on Win32 (one borderless topmost window per monitor) + logging
- fixes second monitor not fully covered (gap at top): each monitor now gets its own window sized exactly to its rcMonitor in device coords, with per-monitor V2 DPI awareness, so it covers the full screen regardless of layout/DPI - dropped pygame; black fill + 'ESC - exit' hint drawn via GDI - global ESC via low-level keyboard hook (works without window focus) - logging to %TEMP%\black_screen_dim.log (start, monitor rects, brightness, errors) - requirements.txt: pygame removed
This commit is contained in:
1 parent
87bdbd7226
commit
0e88bf62f5
3 files changed
+338
-116
No files matched your search
@@ -20,8 +20,9 @@ pyinstaller "Black Screen Dim.spec"
|
||||
Готовый exe — в `dist\Black Screen Dim\`.
|
||||
|
||||
## Зависимости
|
||||
- `pygame` — окно и отрисовка
|
||||
- `screen-brightness-control` — управление яркостью (только Windows)
|
||||
- чёрная заливка экрана — напрямую через Win32 API (ctypes, входит в стандартную библиотеку), pygame больше не нужен
|
||||
- лог пишется в `%TEMP%\black_screen_dim.log`
|
||||
|
||||
> Управление яркостью работает только на Windows. На других ОС приложение
|
||||
> просто покажет чёрный экран без изменения яркости.
|
||||
|
||||
+333
-114
@@ -1,33 +1,52 @@
|
||||
"""Black Screen Dim — гасит все мониторы чёрным безрамочным окном (Windows).
|
||||
|
||||
Для КАЖДОГО подключённого монитора создаётся отдельное borderless topmost окно,
|
||||
плотно закрывающее монитор целиком (по его rcMonitor, в device-координатах).
|
||||
Яркость гасится и восстанавливается на ВСЕХ мониторах.
|
||||
ESC (глобально, через low-level keyboard hook) — выход.
|
||||
Подробный лог пишется в %TEMP%\\black_screen_dim.log.
|
||||
"""
|
||||
|
||||
import ctypes
|
||||
import ctypes.wintypes as wt
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import tempfile
|
||||
|
||||
import pygame
|
||||
import screen_brightness_control as sbc
|
||||
|
||||
# Windows-only calls are guarded so the module still imports on other OSes.
|
||||
LOG = logging.getLogger("black_screen_dim")
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# Logging
|
||||
# --------------------------------------------------------------------------
|
||||
def setup_logging():
|
||||
log_path = os.path.join(tempfile.gettempdir(), "black_screen_dim.log")
|
||||
try:
|
||||
import ctypes
|
||||
from ctypes import wintypes
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format="%(asctime)s [%(levelname)s] %(message)s",
|
||||
handlers=[
|
||||
logging.FileHandler(log_path, encoding="utf-8"),
|
||||
logging.StreamHandler(sys.stderr),
|
||||
],
|
||||
)
|
||||
except Exception:
|
||||
ctypes = None
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
LOG.info("log file: %s", log_path)
|
||||
return log_path
|
||||
|
||||
|
||||
def resource_path(rel):
|
||||
"""Absolute path to a bundled resource, works both from source and from a
|
||||
PyInstaller-frozen exe (sys._MEIPASS)."""
|
||||
base = getattr(sys, "_MEIPASS", os.path.dirname(os.path.abspath(__file__)))
|
||||
return os.path.join(base, rel)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Brightness — handle EVERY monitor, not just the first one
|
||||
# ---------------------------------------------------------------------------
|
||||
# --------------------------------------------------------------------------
|
||||
# Brightness — every monitor
|
||||
# --------------------------------------------------------------------------
|
||||
def get_all_brightness():
|
||||
"""Return a list with one brightness value per monitor, or None on failure."""
|
||||
try:
|
||||
vals = sbc.get_brightness()
|
||||
except Exception:
|
||||
except Exception as e:
|
||||
LOG.warning("get_brightness failed: %r", e)
|
||||
return None
|
||||
if isinstance(vals, list):
|
||||
return list(vals) if vals else None
|
||||
@@ -35,129 +54,329 @@ def get_all_brightness():
|
||||
|
||||
|
||||
def set_all_brightness(value):
|
||||
"""Set brightness on ALL monitors at once."""
|
||||
try:
|
||||
sbc.set_brightness(int(value))
|
||||
return True
|
||||
except Exception:
|
||||
except Exception as e:
|
||||
LOG.warning("set_brightness(%s) failed: %r", value, e)
|
||||
return False
|
||||
|
||||
|
||||
def restore_all_brightness(original):
|
||||
"""Restore each monitor to its previously saved brightness."""
|
||||
if not original:
|
||||
return
|
||||
for i, b in enumerate(original):
|
||||
try:
|
||||
sbc.set_brightness(int(b), display=i)
|
||||
except Exception:
|
||||
pass
|
||||
except Exception as e:
|
||||
LOG.warning("restore brightness monitor %d failed: %r", i, e)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Window geometry — cover the whole virtual desktop (all monitors)
|
||||
# ---------------------------------------------------------------------------
|
||||
def get_virtual_screen_rect():
|
||||
"""Bounding rectangle of all monitors combined (may start at negative x/y)."""
|
||||
# --------------------------------------------------------------------------
|
||||
# Win32 overlay — one borderless topmost window per monitor
|
||||
# --------------------------------------------------------------------------
|
||||
if sys.platform == "win32":
|
||||
user32 = ctypes.windll.user32
|
||||
gdi32 = ctypes.windll.gdi32
|
||||
kernel32 = ctypes.windll.kernel32
|
||||
try:
|
||||
user32.SetProcessDPIAware()
|
||||
shcore = ctypes.windll.shcore
|
||||
except Exception:
|
||||
pass
|
||||
SM_XVIRTUALSCREEN = 76
|
||||
SM_YVIRTUALSCREEN = 77
|
||||
SM_CXVIRTUALSCREEN = 78
|
||||
SM_CYVIRTUALSCREEN = 79
|
||||
x = user32.GetSystemMetrics(SM_XVIRTUALSCREEN)
|
||||
y = user32.GetSystemMetrics(SM_YVIRTUALSCREEN)
|
||||
w = user32.GetSystemMetrics(SM_CXVIRTUALSCREEN)
|
||||
h = user32.GetSystemMetrics(SM_CYVIRTUALSCREEN)
|
||||
if w <= 0 or h <= 0: # fallback to primary monitor
|
||||
shcore = None
|
||||
|
||||
WS_POPUP = 0x80000000
|
||||
WS_VISIBLE = 0x10000000
|
||||
WS_EX_TOPMOST = 0x00000008
|
||||
WS_EX_TOOLWINDOW = 0x00000080
|
||||
SWP_SHOWWINDOW = 0x0040
|
||||
HWND_TOPMOST = -1
|
||||
WM_PAINT = 0x000F
|
||||
WM_ERASEBKGND = 0x0014
|
||||
WM_DESTROY = 0x0002
|
||||
WM_CLOSE = 0x0010
|
||||
WM_TIMER = 0x0113
|
||||
WM_KEYDOWN_LL = 0x0100
|
||||
VK_ESCAPE = 0x1B
|
||||
WH_KEYBOARD_LL = 13
|
||||
DT_CENTER = 0x00000001
|
||||
DT_VCENTER = 0x00000004
|
||||
DT_SINGLELINE = 0x00000020
|
||||
TRANSPARENT = 1
|
||||
DEFAULT_GUI_FONT = 17
|
||||
|
||||
WNDPROC = ctypes.WINFUNCTYPE(wt.LRESULT, wt.HWND, wt.UINT, wt.WPARAM, wt.LPARAM)
|
||||
MONITORENUMPROC = ctypes.WINFUNCTYPE(
|
||||
wt.BOOL, wt.HMONITOR, wt.HDC, ctypes.POINTER(wt.RECT), wt.LPARAM
|
||||
)
|
||||
HOOKPROC = ctypes.WINFUNCTYPE(wt.LRESULT, ctypes.c_int, wt.WPARAM, wt.LPARAM)
|
||||
|
||||
class WNDCLASSEXW(ctypes.Structure):
|
||||
_fields_ = [
|
||||
("cbSize", wt.UINT),
|
||||
("style", wt.UINT),
|
||||
("lpfnWndProc", WNDPROC),
|
||||
("cbClsExtra", wt.INT),
|
||||
("cbWndExtra", wt.INT),
|
||||
("hInstance", wt.HINSTANCE),
|
||||
("hIcon", wt.HICON),
|
||||
("hCursor", wt.HCURSOR),
|
||||
("hbrBackground", wt.HBRUSH),
|
||||
("lpszMenuName", wt.LPCWSTR),
|
||||
("lpszClassName", wt.LPCWSTR),
|
||||
("hIconSm", wt.HICON),
|
||||
]
|
||||
|
||||
class PAINTSTRUCT(ctypes.Structure):
|
||||
_fields_ = [
|
||||
("hdc", wt.HDC),
|
||||
("fErase", wt.BOOL),
|
||||
("rcPaint", wt.RECT),
|
||||
("fRestore", wt.BOOL),
|
||||
("fIncUpdate", wt.BOOL),
|
||||
("rgbReserved", wt.BYTE * 32),
|
||||
]
|
||||
|
||||
class KBDLLHOOKSTRUCT(ctypes.Structure):
|
||||
_fields_ = [
|
||||
("vkCode", wt.DWORD),
|
||||
("scanCode", wt.DWORD),
|
||||
("flags", wt.DWORD),
|
||||
("time", wt.DWORD),
|
||||
("dwExtraInfo", wt.ULONG_PTR),
|
||||
]
|
||||
|
||||
# --- prototypes (64-bit safe) ---
|
||||
user32.EnumDisplayMonitors.argtypes = [wt.HDC, ctypes.POINTER(wt.RECT), MONITORENUMPROC, wt.LPARAM]
|
||||
user32.EnumDisplayMonitors.restype = wt.BOOL
|
||||
user32.RegisterClassExW.argtypes = [ctypes.POINTER(WNDCLASSEXW)]
|
||||
user32.RegisterClassExW.restype = wt.ATOM
|
||||
user32.CreateWindowExW.argtypes = [
|
||||
wt.DWORD, wt.LPCWSTR, wt.LPCWSTR, wt.DWORD,
|
||||
wt.INT, wt.INT, wt.INT, wt.INT, wt.HWND, wt.HMENU, wt.HINSTANCE, ctypes.c_void_p,
|
||||
]
|
||||
user32.CreateWindowExW.restype = wt.HWND
|
||||
user32.ShowWindow.argtypes = [wt.HWND, wt.INT]
|
||||
user32.ShowWindow.restype = wt.BOOL
|
||||
user32.UpdateWindow.argtypes = [wt.HWND]
|
||||
user32.UpdateWindow.restype = wt.BOOL
|
||||
user32.SetWindowPos.argtypes = [wt.HWND, wt.HWND, wt.INT, wt.INT, wt.INT, wt.INT, wt.UINT]
|
||||
user32.SetWindowPos.restype = wt.BOOL
|
||||
user32.SetTimer.argtypes = [wt.HWND, wt.UINT_PTR, wt.UINT, ctypes.c_void_p]
|
||||
user32.SetTimer.restype = wt.UINT_PTR
|
||||
user32.KillTimer.argtypes = [wt.HWND, wt.UINT_PTR]
|
||||
user32.KillTimer.restype = wt.BOOL
|
||||
user32.GetClientRect.argtypes = [wt.HWND, ctypes.POINTER(wt.RECT)]
|
||||
user32.GetClientRect.restype = wt.BOOL
|
||||
user32.BeginPaint.argtypes = [wt.HWND, ctypes.POINTER(PAINTSTRUCT)]
|
||||
user32.BeginPaint.restype = wt.HDC
|
||||
user32.EndPaint.argtypes = [wt.HWND, ctypes.POINTER(PAINTSTRUCT)]
|
||||
user32.EndPaint.restype = wt.BOOL
|
||||
user32.InvalidateRect.argtypes = [wt.HWND, ctypes.POINTER(wt.RECT), wt.BOOL]
|
||||
user32.InvalidateRect.restype = wt.BOOL
|
||||
user32.PostQuitMessage.argtypes = [wt.INT]
|
||||
user32.PostQuitMessage.restype = None
|
||||
user32.GetMessageW.argtypes = [ctypes.POINTER(wt.MSG), wt.HWND, wt.UINT, wt.UINT]
|
||||
user32.GetMessageW.restype = wt.LONG
|
||||
user32.TranslateMessage.argtypes = [ctypes.POINTER(wt.MSG)]
|
||||
user32.TranslateMessage.restype = wt.BOOL
|
||||
user32.DispatchMessageW.argtypes = [ctypes.POINTER(wt.MSG)]
|
||||
user32.DispatchMessageW.restype = wt.LONG
|
||||
user32.SetWindowsHookExW.argtypes = [wt.INT, HOOKPROC, wt.HINSTANCE, wt.DWORD]
|
||||
user32.SetWindowsHookExW.restype = wt.HHOOK
|
||||
user32.CallNextHookEx.argtypes = [wt.HHOOK, ctypes.c_int, wt.WPARAM, wt.LPARAM]
|
||||
user32.CallNextHookEx.restype = wt.LRESULT
|
||||
user32.UnhookWindowsHookEx.argtypes = [wt.HHOOK]
|
||||
user32.UnhookWindowsHookEx.restype = wt.BOOL
|
||||
user32.DestroyWindow.argtypes = [wt.HWND]
|
||||
user32.DestroyWindow.restype = wt.BOOL
|
||||
user32.ShowCursor.argtypes = [wt.BOOL]
|
||||
user32.ShowCursor.restype = wt.INT
|
||||
user32.DefWindowProcW.argtypes = [wt.HWND, wt.UINT, wt.WPARAM, wt.LPARAM]
|
||||
user32.DefWindowProcW.restype = wt.LRESULT
|
||||
user32.SetProcessDPIAware.argtypes = []
|
||||
user32.SetProcessDPIAware.restype = wt.BOOL
|
||||
gdi32.CreateSolidBrush.argtypes = [wt.COLORREF]
|
||||
gdi32.CreateSolidBrush.restype = wt.HBRUSH
|
||||
gdi32.FillRect.argtypes = [wt.HDC, ctypes.POINTER(wt.RECT), wt.HBRUSH]
|
||||
gdi32.FillRect.restype = wt.INT
|
||||
gdi32.DeleteObject.argtypes = [wt.HGDIOBJ]
|
||||
gdi32.DeleteObject.restype = wt.BOOL
|
||||
gdi32.SetTextColor.argtypes = [wt.HDC, wt.COLORREF]
|
||||
gdi32.SetTextColor.restype = wt.COLORREF
|
||||
gdi32.SetBkMode.argtypes = [wt.HDC, wt.INT]
|
||||
gdi32.SetBkMode.restype = wt.INT
|
||||
gdi32.GetStockObject.argtypes = [wt.INT]
|
||||
gdi32.GetStockObject.restype = wt.HGDIOBJ
|
||||
gdi32.SelectObject.argtypes = [wt.HDC, wt.HGDIOBJ]
|
||||
gdi32.SelectObject.restype = wt.HGDIOBJ
|
||||
gdi32.DrawTextW.argtypes = [wt.HDC, wt.LPCWSTR, wt.INT, ctypes.POINTER(wt.RECT), wt.UINT]
|
||||
gdi32.DrawTextW.restype = wt.INT
|
||||
kernel32.GetModuleHandleW.argtypes = [wt.LPCWSTR]
|
||||
kernel32.GetModuleHandleW.restype = wt.HINSTANCE
|
||||
kernel32.GetLastError.argtypes = []
|
||||
kernel32.GetLastError.restype = wt.DWORD
|
||||
|
||||
_hwnds = []
|
||||
_hint_active = True
|
||||
_hook_id = None
|
||||
_hint_text = "ESC - exit"
|
||||
|
||||
def enum_monitor_rects():
|
||||
rects = []
|
||||
|
||||
def cb(hmon, hdc, lprect, lparam):
|
||||
r = lprect.contents
|
||||
rects.append((r.left, r.top, r.right, r.bottom))
|
||||
return True
|
||||
|
||||
user32.EnumDisplayMonitors(0, 0, MONITORENUMPROC(cb), 0)
|
||||
return rects
|
||||
|
||||
def _wndproc(hwnd, msg, wparam, lparam):
|
||||
if msg == WM_ERASEBKGND:
|
||||
return 1
|
||||
if msg == WM_PAINT:
|
||||
ps = PAINTSTRUCT()
|
||||
hdc = user32.BeginPaint(hwnd, ctypes.byref(ps))
|
||||
rect = wt.RECT()
|
||||
user32.GetClientRect(hwnd, ctypes.byref(rect))
|
||||
brush = gdi32.CreateSolidBrush(0x000000)
|
||||
gdi32.FillRect(hdc, ctypes.byref(rect), brush)
|
||||
gdi32.DeleteObject(brush)
|
||||
if _hint_active:
|
||||
gdi32.SetTextColor(hdc, 0xFFFFFF)
|
||||
gdi32.SetBkMode(hdc, TRANSPARENT)
|
||||
font = gdi32.GetStockObject(DEFAULT_GUI_FONT)
|
||||
old = gdi32.SelectObject(hdc, font)
|
||||
buf = ctypes.create_unicode_buffer(_hint_text)
|
||||
gdi32.DrawTextW(
|
||||
hdc, buf, -1, ctypes.byref(rect),
|
||||
DT_CENTER | DT_VCENTER | DT_SINGLELINE,
|
||||
)
|
||||
if old:
|
||||
gdi32.SelectObject(hdc, old)
|
||||
user32.EndPaint(hwnd, ctypes.byref(ps))
|
||||
return 0
|
||||
if msg == WM_TIMER:
|
||||
global _hint_active
|
||||
_hint_active = False
|
||||
user32.KillTimer(hwnd, 1)
|
||||
for h in _hwnds:
|
||||
user32.InvalidateRect(h, None, False)
|
||||
return 0
|
||||
if msg in (WM_DESTROY, WM_CLOSE):
|
||||
user32.PostQuitMessage(0)
|
||||
return 0
|
||||
return user32.DefWindowProcW(hwnd, msg, wparam, lparam)
|
||||
|
||||
def _low_level_keyboard_proc(nCode, wParam, lParam):
|
||||
if nCode >= 0 and wParam == WM_KEYDOWN_LL:
|
||||
kb = ctypes.cast(lParam, ctypes.POINTER(KBDLLHOOKSTRUCT)).contents
|
||||
if kb.vkCode == VK_ESCAPE:
|
||||
LOG.info("ESC pressed -> exit")
|
||||
user32.PostQuitMessage(0)
|
||||
return user32.CallNextHookEx(_hook_id, nCode, wParam, lParam)
|
||||
|
||||
def run_overlay():
|
||||
global _hwnds, _hook_id
|
||||
# Per-monitor V2 DPI awareness: device-coord rects map 1:1 to each
|
||||
# monitor regardless of mixed DPI / where the primary sits.
|
||||
if shcore is not None:
|
||||
try:
|
||||
shcore.SetProcessDpiAwareness(2) # PROCESS_PER_MONITOR_DPI_AWARE_V2
|
||||
except Exception as e:
|
||||
LOG.warning("SetProcessDpiAwareness failed: %r", e)
|
||||
user32.SetProcessDPIAware()
|
||||
else:
|
||||
user32.SetProcessDPIAware()
|
||||
|
||||
rects = enum_monitor_rects()
|
||||
LOG.info("monitors found: %s", rects)
|
||||
if not rects:
|
||||
LOG.warning("no monitors enumerated, falling back to primary")
|
||||
w = user32.GetSystemMetrics(0) # SM_CXSCREEN
|
||||
h = user32.GetSystemMetrics(1) # SM_CYSCREEN
|
||||
x = y = 0
|
||||
return x, y, w, h
|
||||
rects = [(0, 0, w, h)]
|
||||
|
||||
wndproc = WNDPROC(_wndproc)
|
||||
cls_name = "BlackScreenDimCls"
|
||||
hinst = kernel32.GetModuleHandleW(None)
|
||||
wc = WNDCLASSEXW()
|
||||
wc.cbSize = ctypes.sizeof(WNDCLASSEXW)
|
||||
wc.lpfnWndProc = wndproc
|
||||
wc.hInstance = hinst
|
||||
wc.hbrBackground = gdi32.CreateSolidBrush(0x000000)
|
||||
wc.lpszClassName = cls_name
|
||||
if not user32.RegisterClassExW(ctypes.byref(wc)):
|
||||
LOG.error("RegisterClassExW failed: %d", kernel32.GetLastError())
|
||||
return
|
||||
|
||||
def make_window_cover_all_monitors(hwnd, rect):
|
||||
"""Move/resize the window to the virtual-screen rect and pin it topmost so it
|
||||
sits above the taskbar and every monitor."""
|
||||
user32 = ctypes.windll.user32
|
||||
HWND_TOPMOST = -1
|
||||
x, y, w, h = rect
|
||||
user32.SetWindowPos(int(hwnd), HWND_TOPMOST, x, y, w, h, 0)
|
||||
try:
|
||||
user32.SetForegroundWindow(int(hwnd))
|
||||
except Exception:
|
||||
pass
|
||||
for (left, top, right, bottom) in rects:
|
||||
w = right - left
|
||||
h = bottom - top
|
||||
hwnd = user32.CreateWindowExW(
|
||||
WS_EX_TOPMOST | WS_EX_TOOLWINDOW,
|
||||
cls_name, "Black Screen Dim",
|
||||
WS_POPUP | WS_VISIBLE,
|
||||
left, top, w, h,
|
||||
0, 0, hinst, 0,
|
||||
)
|
||||
if not hwnd:
|
||||
LOG.error("CreateWindowExW failed for %s: %d",
|
||||
(left, top, right, bottom), kernel32.GetLastError())
|
||||
continue
|
||||
user32.ShowWindow(hwnd, 5) # SW_SHOW
|
||||
user32.UpdateWindow(hwnd)
|
||||
user32.SetWindowPos(hwnd, HWND_TOPMOST, left, top, w, h, SWP_SHOWWINDOW)
|
||||
user32.SetTimer(hwnd, 1, 1000, None)
|
||||
_hwnds.append(hwnd)
|
||||
LOG.info("created %d overlay window(s)", len(_hwnds))
|
||||
|
||||
# hide cursor
|
||||
for _ in range(12):
|
||||
if user32.ShowCursor(False) < 0:
|
||||
break
|
||||
# global ESC hook
|
||||
kb_proc = HOOKPROC(_low_level_keyboard_proc)
|
||||
_hook_id = user32.SetWindowsHookExW(WH_KEYBOARD_LL, kb_proc, hinst, 0)
|
||||
if not _hook_id:
|
||||
LOG.warning("SetWindowsHookExW failed: %d", kernel32.GetLastError())
|
||||
|
||||
msg = wt.MSG()
|
||||
while user32.GetMessageW(ctypes.byref(msg), 0, 0, 0) > 0:
|
||||
user32.TranslateMessage(ctypes.byref(msg))
|
||||
user32.DispatchMessageW(ctypes.byref(msg))
|
||||
|
||||
if _hook_id:
|
||||
user32.UnhookWindowsHookEx(_hook_id)
|
||||
for h in _hwnds:
|
||||
user32.DestroyWindow(h)
|
||||
# restore cursor
|
||||
for _ in range(12):
|
||||
if user32.ShowCursor(True) >= 0:
|
||||
break
|
||||
|
||||
|
||||
def main():
|
||||
# --- brightness: save every monitor, then dim them all to 0 ---
|
||||
original_brightness = get_all_brightness()
|
||||
if original_brightness is not None:
|
||||
setup_logging()
|
||||
LOG.info("starting Black Screen Dim")
|
||||
original = get_all_brightness()
|
||||
LOG.info("saved brightness: %s", original)
|
||||
if original is not None:
|
||||
set_all_brightness(0)
|
||||
|
||||
# --- black screen over the whole virtual desktop ---
|
||||
pygame.init()
|
||||
|
||||
if sys.platform == "win32" and ctypes is not None:
|
||||
x, y, w, h = get_virtual_screen_rect()
|
||||
try:
|
||||
if sys.platform == "win32":
|
||||
run_overlay()
|
||||
else:
|
||||
info = pygame.display.Info()
|
||||
x, y, w, h = 0, 0, info.current_w, info.current_h
|
||||
|
||||
screen = pygame.display.set_mode((w, h), pygame.NOFRAME)
|
||||
pygame.display.set_caption("Black Screen Dim")
|
||||
|
||||
try:
|
||||
icon = pygame.image.load(resource_path("icon.ico"))
|
||||
pygame.display.set_icon(icon)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
pygame.mouse.set_visible(False)
|
||||
|
||||
# Pin the borderless window across all monitors and bring it to front.
|
||||
if sys.platform == "win32" and ctypes is not None:
|
||||
try:
|
||||
hwnd = pygame.display.get_wm_info().get("window")
|
||||
if hwnd:
|
||||
make_window_cover_all_monitors(hwnd, (x, y, w, h))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
font = pygame.font.SysFont(None, 48)
|
||||
hint_text = "ESC - exit"
|
||||
text_surf = font.render(hint_text, True, (255, 255, 255))
|
||||
text_rect = text_surf.get_rect(center=(w // 2, h // 2))
|
||||
show_hint_until = time.time() + 1.0 # ~1 second
|
||||
|
||||
running = True
|
||||
while running:
|
||||
for event in pygame.event.get():
|
||||
if event.type == pygame.QUIT:
|
||||
running = False
|
||||
if event.type == pygame.KEYDOWN and event.key == pygame.K_ESCAPE:
|
||||
running = False
|
||||
|
||||
screen.fill((0, 0, 0))
|
||||
if time.time() < show_hint_until:
|
||||
screen.blit(text_surf, text_rect)
|
||||
|
||||
pygame.display.flip()
|
||||
|
||||
# --- exit: restore every monitor's brightness and the cursor ---
|
||||
if original_brightness is not None:
|
||||
restore_all_brightness(original_brightness)
|
||||
|
||||
try:
|
||||
pygame.mouse.set_visible(True)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
pygame.quit()
|
||||
sys.exit()
|
||||
LOG.warning("non-Windows platform; nothing to display")
|
||||
except Exception as e:
|
||||
LOG.exception("overlay error: %r", e)
|
||||
finally:
|
||||
if original is not None:
|
||||
restore_all_brightness(original)
|
||||
LOG.info("restored brightness: %s", original)
|
||||
LOG.info("exiting")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
+3
-1
@@ -1,2 +1,4 @@
|
||||
pygame>=2.5.0
|
||||
# Windows-only runtime dependency.
|
||||
# The black overlay uses the Win32 API directly via ctypes (standard library),
|
||||
# so pygame is no longer required.
|
||||
screen-brightness-control>=0.22.0
|
||||
Reference in new issue
Block a user