Files
black-screen/black_screen_dim.py
T

381 lines
14 KiB
Python

"""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 tempfile
import screen_brightness_control as sbc
LOG = logging.getLogger("black_screen_dim")
# --------------------------------------------------------------------------
# Logging
# --------------------------------------------------------------------------
def setup_logging():
log_path = os.path.join(tempfile.gettempdir(), "black_screen_dim.log")
try:
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:
logging.basicConfig(level=logging.INFO)
LOG.info("log file: %s", log_path)
return log_path
# --------------------------------------------------------------------------
# Brightness — every monitor
# --------------------------------------------------------------------------
def get_all_brightness():
try:
vals = sbc.get_brightness()
except Exception as e:
LOG.warning("get_brightness failed: %r", e)
return None
if isinstance(vals, list):
return list(vals) if vals else None
return [vals]
def set_all_brightness(value):
try:
sbc.set_brightness(int(value))
return True
except Exception as e:
LOG.warning("set_brightness(%s) failed: %r", value, e)
return False
def restore_all_brightness(original):
if not original:
return
for i, b in enumerate(original):
try:
sbc.set_brightness(int(b), display=i)
except Exception as e:
LOG.warning("restore brightness monitor %d failed: %r", i, e)
# --------------------------------------------------------------------------
# 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:
shcore = ctypes.windll.shcore
except Exception:
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
LRESULT = ctypes.c_int64
HMONITOR = ctypes.c_void_p
HHOOK = ctypes.c_void_p
HCURSOR = ctypes.c_void_p
ULONG_PTR = ctypes.c_void_p
UINT_PTR = ctypes.c_void_p
WNDPROC = ctypes.WINFUNCTYPE(LRESULT, wt.HWND, wt.UINT, wt.WPARAM, wt.LPARAM)
MONITORENUMPROC = ctypes.WINFUNCTYPE(
wt.BOOL, HMONITOR, wt.HDC, ctypes.POINTER(wt.RECT), wt.LPARAM
)
HOOKPROC = ctypes.WINFUNCTYPE(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", 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", 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, UINT_PTR, wt.UINT, ctypes.c_void_p]
user32.SetTimer.restype = UINT_PTR
user32.KillTimer.argtypes = [wt.HWND, 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 = HHOOK
user32.CallNextHookEx.argtypes = [HHOOK, ctypes.c_int, wt.WPARAM, wt.LPARAM]
user32.CallNextHookEx.restype = LRESULT
user32.UnhookWindowsHookEx.argtypes = [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 = LRESULT
user32.SetProcessDPIAware.argtypes = []
user32.SetProcessDPIAware.restype = wt.BOOL
gdi32.CreateSolidBrush.argtypes = [wt.COLORREF]
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
user32.DrawTextW.argtypes = [wt.HDC, wt.LPCWSTR, wt.INT, ctypes.POINTER(wt.RECT), wt.UINT]
user32.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(None, None, MONITORENUMPROC(cb), 0)
return rects
def _wndproc(hwnd, msg, wparam, lparam):
global _hint_active
if msg == WM_PAINT:
# Background (black) is erased by DefWindowProc via the class brush.
ps = PAINTSTRUCT()
hdc = user32.BeginPaint(hwnd, ctypes.byref(ps))
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)
user32.DrawTextW(
hdc, buf, -1, ctypes.byref(ps.rcPaint),
DT_CENTER | DT_VCENTER | DT_SINGLELINE,
)
if old:
gdi32.SelectObject(hdc, old)
user32.EndPaint(hwnd, ctypes.byref(ps))
return 0
if msg == WM_TIMER:
_hint_active = False
user32.KillTimer(hwnd, 1)
for h in _hwnds:
user32.InvalidateRect(h, None, True)
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
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
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():
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)
try:
if sys.platform == "win32":
run_overlay()
else:
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__":
main()