zapret-kvn/xray_fluent/ui/window_reveal.py
loop-uh 9ebba8d60a Окно при запуске: по сроку рисуется принудительно, а не открывается белым
Срок скрытия окна (cloak) идёт по тому же циклу событий, что и отрисовка.
Если запуск занимал поток интерфейса дольше срока, таймер срабатывал раньше
первой отрисовки и окно открывалось пустым и белым. Теперь по сроку окно
сначала рисуется синхронно (repaint), затем открывается. Срок не удлинён.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-23 16:23:57 -04:00

173 lines
7.1 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Окно появляется на экране уже нарисованным — при запуске и из трея.
Между ``show()`` и отрисовкой проходит время: Qt сначала разбирает очередь
событий и только потом рисует окно. Всё это время Windows выводит окно таким,
какое оно есть, — пустым и белым. Скрытое в трей окно Qt при возврате рисует
заново целиком, поэтому белый кадр был при каждом щелчке по значку (замер на
win10: белое через 4–79 мс, готовое через 30–226 мс; на обычном компьютере
в разы дольше).
У Windows для этого есть штатное средство — скрытие окна от экрана (cloak):
окно считается показанным, получает события и рисуется, но на экран не
выводится. Перед показом окно скрывается, после отрисовки — открывается, и
пользователь сразу видит готовый кадр.
"""
from __future__ import annotations
import sys
from collections.abc import Callable
from PyQt6.QtCore import QEvent, QObject, QTimer
# DwmSetWindowAttribute: «скрыть окно от экрана, продолжая его рисовать».
DWMWA_CLOAK = 13
# Дольше этого окно скрытым не остаётся, даже если отрисовка так и не пришла:
# белое окно лучше невидимой программы.
FRAME_WAIT_MAX_MS = 1_500
def wait_for_composition() -> None:
"""Ждёт, пока Windows соберёт уже нарисованный кадр окна для экрана.
Без этого шторка снималась раньше, чем кадр доходил до экрана: на стенде
win10 окно после снятия шторки ещё ~20 мс было белым в 4 случаях из 6, с
ожиданием — ни разу из 5. Ждать приходится не дольше одного кадра экрана.
"""
if sys.platform != "win32":
return
try:
import ctypes
ctypes.windll.dwmapi.DwmFlush()
except Exception:
pass
def set_window_cloaked(hwnd: int, cloaked: bool) -> bool:
"""Скрывает окно от экрана или открывает его. True — Windows приняла запрос."""
if sys.platform != "win32" or not hwnd:
return False
try:
import ctypes
from ctypes import wintypes
value = ctypes.c_int(1 if cloaked else 0)
result = ctypes.windll.dwmapi.DwmSetWindowAttribute(
wintypes.HWND(int(hwnd)),
DWMWA_CLOAK,
ctypes.byref(value),
ctypes.sizeof(value),
)
return int(result) == 0
except Exception:
return False
class FrameReveal(QObject):
"""Держит окно скрытым от экрана от ``show()`` до его отрисовки; годится на каждый показ."""
def __init__(
self,
window,
*,
cloak: Callable[[int, bool], bool] = set_window_cloaked,
wait_composed: Callable[[], None] = wait_for_composition,
wait_max_ms: int = FRAME_WAIT_MAX_MS,
) -> None:
super().__init__(window)
self._window = window
self._cloak = cloak
self._wait_composed = wait_composed
self._hwnd = 0
self._cloaked = False
self._deadline = QTimer(self)
self._deadline.setSingleShot(True)
self._deadline.setInterval(max(0, int(wait_max_ms)))
self._deadline.timeout.connect(self._deadline_reached)
def start(self) -> bool:
"""Вызывается из showEvent: окно ещё не выведено на экран."""
if self._cloaked:
return True
try:
hwnd = int(self._window.winId())
except Exception:
return False
if not self._cloak(hwnd, True):
return False
self._hwnd = hwnd
self._cloaked = True
self._window.installEventFilter(self)
self._deadline.start()
return True
def is_cloaked(self) -> bool:
return self._cloaked
def eventFilter(self, watched, event): # noqa: N802 (Qt API)
if self._cloaked and watched is self._window and event.type() == QEvent.Type.Paint:
watched.removeEventFilter(self)
# Событие приходит до самой отрисовки. Открываем окно следующим
# проходом цикла событий — когда кадр уже нарисован и отдан системе —
# и после того, как Windows соберёт его для экрана.
QTimer.singleShot(0, self.reveal)
return False
def _deadline_reached(self) -> None:
# Срок идёт по тому же циклу событий, что и отрисовка: если запуск
# занял поток интерфейса дольше срока, таймер срабатывает раньше первой
# отрисовки, и окно открылось бы пустым и белым. Рисуем его сами, сразу,
# а уже потом открываем.
if self._cloaked:
try:
self._window.repaint()
except Exception:
pass
self.reveal()
def reveal(self) -> None:
if not self._cloaked:
return
self._cloaked = False
self._deadline.stop()
try:
self._window.removeEventFilter(self)
except Exception:
pass
self._wait_composed()
self._cloak(self._hwnd, False)
def is_system_show_event(event) -> bool:
"""Показ по команде Windows (окно развернули из панели задач), а не наш ``show()``.
Такое окно Windows выводит сама, с его прежней картинкой; прятать его
нельзя — Qt может не перерисовать окно, и оно пропало бы до запасного срока.
"""
try:
return bool(event.spontaneous())
except Exception:
return False
def cloak_until_painted(window) -> FrameReveal:
"""Прячет окно от экрана до отрисовки. Вызывается из showEvent при каждом ``show()``."""
reveal = getattr(window, "_frame_reveal", None)
if not isinstance(reveal, FrameReveal):
reveal = FrameReveal(window)
window._frame_reveal = reveal
reveal.start()
return reveal
__all__ = [
"DWMWA_CLOAK",
"FRAME_WAIT_MAX_MS",
"FrameReveal",
"cloak_until_painted",
"is_system_show_event",
"set_window_cloaked",
"wait_for_composition",
]