PyQt6 в стиле Material Design 3 — спецификация эталонного приложения
Ниже — полная спецификация, по которой можно воспроизвести assets/app_template.py
побайтно по поведению. Моделируй агента-дизайнера интерфейсов из студии, которая делает
MD3-десктоп-приложения: ролевая палитра (primary / on-primary / containers / surface /
outline), state layers, ripple, скругления по шкале форм, анимации ровно там, где они
объясняют, что произошло, — и не больше.
Главное правило: «стиль MD3» в Qt — это не нативный API, а воссозданная руками эстетика:
frameless-окно + собственный QSS + QPropertyAnimation для плавности. Так приложение
выглядит одинаково на Windows, Linux и macOS. Native style Qt не даёт скруглений,
полупрозрачности, state layers и анимаций.
Референс-реализация — assets/app_template.py (один файл). Задача агента: либо скопировать
и адаптировать его, либо пересобрать с нуля точно по этому документу.
1. Быстрый старт
pip install PyQt6
python app_template.py
Сборка для пользователя: pyinstaller --windowed --icon=app.ico app_template.py.
Приложение запускается так:
def main() -> None:
app = QApplication(sys.argv)
app.setStyle("Fusion") # единственный стиль, уважающий QSS везде
app.setFont(system_font(14, QFont.Weight.Normal))
window = MainWindow()
window.show()
sys.exit(app.exec())
if __name__ == "__main__":
main()
2. Структура файла app_template.py (порядок важен для читаемости)
- Импорты:
math,sys,time,dataclasses,pathlib.Path; QtCore (Qt,QEvent,QPropertyAnimation,QEasingCurve,QRect(F),QPointF,QParallelAnimationGroup,QVariantAnimation,QTimer,pyqtProperty,pyqtSignal); QtGui (QColor,QFont,QPainter,QPainterPath,QFontDatabase,QFontMetricsF,QPen,QPixmap,QIcon,QPalette); QtSvg (QSvgRenderer); QtWidgets (все нужные виджеты). - Дизайн-токены:
Theme(dataclass),LIGHT/DARK, константы радиусов/длительностей,REPLIES,SVG_EYE/SVG_EYE_SLASH,PAGES. - Хелперы:
system_font,rgba,accent_fg,shadow_color,lerp_color,animate_property,draw_checkmark. - Кастомные виджеты в порядке зависимостей:
MdButton,MdSwitch,MdSlider,MdCheckBox,MdSegmentButton/MdSegmentedControl,DropGlyph/FileDropArea,WindowButton,SidebarToggle,TopAppBar,NavButton/NavigationDrawer. build_stylesheet(theme).MainWindow+main().
3. Дизайн-токены (точные значения, MD3 roles)
Theme — frozen dataclass, все цвета — hex-строки, роли MD3:
primary, on_primary, primary_container, on_primary_container,
secondary_container, on_secondary_container, surface, surface_container_low,
surface_container, surface_container_high, surface_container_highest, on_surface,
on_surface_variant, outline, outline_variant, error, on_error, error_container,
on_error_container, shadow_alpha (int 0..255, тень всегда чёрная).
LIGHT = Theme(primary="#6750A4", on_primary="#FFFFFF",
primary_container="#EADDFF", on_primary_container="#21005D",
secondary_container="#E8DEF8", on_secondary_container="#1D192B",
surface="#FEF7FF", surface_container_low="#F7F2FA",
surface_container="#F3EDF7", surface_container_high="#ECE6F0",
surface_container_highest="#E6E0E9", on_surface="#1D1B20",
on_surface_variant="#49454F", outline="#79747E", outline_variant="#CAC4D0",
error="#B3261E", on_error="#FFFFFF",
error_container="#F9DEDC", on_error_container="#410E0B", shadow_alpha=45)
DARK = Theme(primary="#D0BCFF", on_primary="#381E72",
primary_container="#4F378B", on_primary_container="#EADDFF",
secondary_container="#4A4458", on_secondary_container="#E8DEF8",
surface="#141218", surface_container_low="#1D1B20",
surface_container="#211F26", surface_container_high="#2B2930",
surface_container_highest="#36343B", on_surface="#E6E0E9",
on_surface_variant="#CAC4D0", outline="#938F99", outline_variant="#49454F",
error="#F2B8B5", on_error="#601410",
error_container="#8C1D18", on_error_container="#F9DEDC", shadow_alpha=120)
Пары «роль / on-роль» обязательны: текст поверх primary — всегда on_primary, поверх
secondary_container — on_secondary_container, поверх primary_container —
on_primary_container. Никакого хардкода «white»/«black». accent_fg(theme) оставлен как
compat-хелпер и возвращает theme.on_primary.
def rgba(hex_color: str, alpha_0_255: int) -> str: # QSS ждёт альфу 0..255, не 0..1
c = QColor(hex_color)
return f"rgba({c.red()}, {c.green()}, {c.blue()}, {alpha_0_255})"
def shadow_color(theme: Theme) -> QColor:
c = QColor(0, 0, 0); c.setAlpha(theme.shadow_alpha); return c
Константы:
- Формы:
RADIUS_WINDOW = 16,RADIUS_CARD = 12,RADIUS_MENU = 8,RADIUS_FIELD = 4,SIDEBAR_WIDTH = 220. Кнопки/пилюли — radius = height/2; чипы/сегменты — full. EDGE_MARGIN = 12(совпадает с отступом root под тень), бит-маски краёвEDGE_TOP, EDGE_BOTTOM, EDGE_LEFT, EDGE_RIGHT = 1, 2, 4, 8.DUR_FAST = 140(hover),DUR_PRESS = 90,DUR_RELEASE = 200,DUR_MED = 250(страницы, drawer, тумблер, сегменты),DUR_RIPPLE = 450;EASE = OutCubic,EASE_PRESS = OutQuad.PAGES = ["Обзор", "Задачи", "Ввод", "Чат", "Файлы", "Настройки"].REPLIES— 5 коротких ответов бота.SVG_EYE/SVG_EYE_SLASH— Material Symbolsvisibility/visibility_off(viewBox 0 0 24 24,fill="currentColor").
Типографика: system_font(point_size=14, weight) — цепочка Roboto → Google Sans →
Noto Sans → Segoe UI, setStyleStrategy(PreferAntialias). Размеры: заголовок страницы 24,
заголовок карточки 16/Medium, текст кнопки 14/Medium, body 14, вторичный 12, время 11.
Отступы из шкалы 4/8/12/16/24/32.
Тени: окно/панель — blurRadius 24, offset (0,4); карточки (elevation 1) —
blurRadius 8, offset (0,1); цвет shadow_color(theme).
State layers (поверх контейнера, цветом «on-роли»): hover 0.08, press 0.12 (т.е.
0.08*hover + 0.04*press); у слайдера hover 0.10 / press 0.16; у чекбокса 0.06 (unchecked)
/ 0.10 (checked).
4. Каркас окна
self.setWindowFlags(Qt.WindowType.FramelessWindowHint | Qt.WindowType.Window)
self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)
self.setMinimumSize(760, 480)
self.resize(960, 600)
self.setMouseTracking(True)
QApplication.instance().installEventFilter(self) # для ресайза за края
Дерево (_build_ui): QMainWindow → root (QWidget, margins 12) →
panel (QFrame#Panel, drop-shadow) → TopAppBar (56px) + body (QHBoxLayout) →
NavigationDrawer (220px) + separator (1px, outline_variant) + QStackedWidget.
Панель получает build_stylesheet(theme) — QSS наследуется вниз. Дополнительно
_apply_palette(theme) выставляет роли QPalette (Window/Base=surface, Text=on_surface,
Highlight=primary, HighlightedText=on_primary, PlaceholderText=on_surface_variant,
ToolTipBase=surface_container_high…) для нативно рисуемых частей.
Тень и скругление живут на дочернем QFrame#Panel, а не на самом окне: окно прозрачное
и даёт отступ 12px под тень. Навешивать эффект на QMainWindow нельзя.
Перетаскивание и ресайз
Двигать окно — нативный механизм Qt6 (в TopAppBar.mousePressEvent):
handle = self._window.windowHandle()
if handle is not None:
handle.startSystemMove()
Двойной клик по TopAppBar — maximize/normal.
Менять размер за края — нативный QWindow.startSystemResize. В MainWindow установлен
фильтр событий на QApplication: на MouseButtonPress(Left) в зоне ≤ EDGE_MARGIN
вызывает _start_system_resize(edges) и возвращает True; на MouseMove без кнопок
обновляет курсор; на Leave сбрасывает курсор.
_resize_edges_for(pos) → бит-маска краёв (только если окно видимо и не максимизировано).
_cursor_for_edges: центр → Arrow; лево/право → SizeHor; верх/низ → SizeVer;
верх+лево или низ+право → SizeFDiag; иначе → SizeBDiag.
Капкан PyQt6: Qt.Edge нельзя превратить в int и нельзя смешивать с int через |.
Математика краёв — на своих бит-константах EDGE_*, флаг собирается так:
flag = Qt.Edge(0)
if edges & EDGE_TOP: flag |= Qt.Edge.TopEdge
if edges & EDGE_BOTTOM: flag |= Qt.Edge.BottomEdge
if edges & EDGE_LEFT: flag |= Qt.Edge.LeftEdge
if edges & EDGE_RIGHT: flag |= Qt.Edge.RightEdge
handle.startSystemResize(flag)
5. Кастомные виджеты (точные параметры)
MdButton (filled / tonal, state layer + ripple)
QPushButton, высота 40, pill radius 20, шрифт 14/Medium,padding 0 24px, minWidth = text + 48.kind="filled"→ bgprimary/fgon_primary;kind="tonal"→ bgsecondary_container/fgon_secondary_container.- QSS:
background: transparent; color: {fg}— фон рисуется вручную, текст черезsuper().paintEvent(). Q_PROPERTY hover/press/ripple(float). State layer цветом fg:alphaF = 0.08*hover + 0.04*press, клип по скруглённому rect.- Ripple:
mousePressEventзапоминает точку (_ripple_origin),pressedзапускает_ripple_anim0→1 (DUR_RIPPLE); в paint — эллипс из точки радиусомmax_r*ripple(max_r = расстояние до дальнего угла + 4),alphaF = 0.18*(1-ripple), клип по pill. Ссылку на анимацию держать в атрибуте.
MdSwitch (тумблер MD3)
QWidget52×32, сигналtoggled(bool)только по клику;setCheckedмолча анимирует.Q_PROPERTY knobPos(0..1)DUR_MED/EASE, плюс hover/press.- Off: трек
surface_container_highest+ обводка 2pxoutline; ручка 16px цветомoutline, центр x=16. On: трекprimaryбез обводки; ручка 24pxon_primary, центр x=36, внутри галочка цветомprimary(draw_checkmark, pen 2px). Press: ручка 28px. - Трек/ручка интерполируются
lerp_colorпо knobPos; обводка исчезает при t→1 (alpha = 255*(1-t)); state layer-круг 40px вокруг ручки.
MdSlider
QWidgetвысота 28,valueChanged(int). Трек 4px radius 2: неактивныйsurface_container_highest, активныйprimary. Ручка — круг 20pxprimary+ лёгкая тень (чёрный alpha 30, offset y+1). State layer 40pxprimaryalphaF = 0.10*hover + 0.06*press.- Значение из x:
pad = 10(радиус ручки),ratio = (x-pad)/(width-2*pad); при drag — без инерции, иначеfillанимируетсяDUR_FAST.
MdCheckBox
QWidgetвысота 24, шрифт 14, minWidth = 34 + text. Бокс 18×18 radius 2 at x=3.- Unchecked: обводка 2px
on_surface_variant. Checked: заливкаprimary+ галочкаon_primary(pen 2px RoundCap/RoundJoin), галочка «прорисовывается» прогрессивно (check0..1, две сегментные линии черезdrawPath). - State layer 36px:
on_surface0.06 (unchecked) /primary0.10 (checked) * hover. boxScale(OutBack) сжимает бокс при press;setChecked(checked, animate)эмититtoggled; клик вmouseReleaseEventтолько если курсор внутри rect.
MdSegmentedControl / MdSegmentButton
- Контейнер высота 40, pill radius 20, обводка 1px
outline(рисуется в paintEvent). - Сегменты равной ширины (
addWidget(btn, 1)), высота 38, шрифт 14/Medium; margins (1,1,1,1), spacing 0. Разделители 1pxoutlineмежду НЕвыбранными соседями (рисуются в paintEvent, int-координаты). - Индикатор —
QFrame#SegIndicator(bgsecondary_container, radius 19) позади кнопок,QPropertyAnimation(geometry)DUR_MED; вshowEvent—lower()+ геометрия без анимации. - Выбранный сегмент: текст
on_secondary_container+ галочка слева от текста (draw_checkmark); невыбранный:on_surface. Цвет текста анимируется (colorQColor property). State layer сегмента — pill цветом on-роли.
DropGlyph / FileDropArea
- Glyph 38×38: стрелка вниз + лоток, цвет
lerp(on_surface_variant, primary, level), haloprimaryalpha 0.10*level. FileDropArea(QFrame): minHeight 150,setAcceptDrops(True). Фонsurface_container_lowradius 16; пунктир 2pxoutline_variant(dashPattern [4,3.5]); при hover/drag — пунктирprimary+ overlayprimary_containeralpha ≈ 60glow+20press.dragEnterEvent(hasUrls)/dragLeaveEvent/dropEvent(толькоisLocalFile()) →dropped(list[str]); клик →QFileDialog.getOpenFileNames. Вся логика сигналов неизменна.
WindowButton (кнопки окна)
QPushButton40×40, radius 20,Q_PROPERTY hover/press,set_maximized(bool).- Глифы
QPen1.8 RoundCap/RoundJoin, int-координаты: minimize — горизонтальная линия; maximize — квадрат 12×12 (outline), при развороте — два наложенных квадрата; close — крестик X. - Цвет глифа
lerp(on_surface_variant, on_surface, hover); close на hover — фонerror(круг 40px) + глифon_error. Остальные на hover — state layeron_surface0.08. - Порядок справа: minimize, maximize, close.
SidebarToggle
QPushButton40×40, state layeron_surface0.08, глифlerp(on_surface_variant, on_surface, hover).- Глиф: рамка 16×16 radius 2 + разделитель; открытая панель — разделитель слева + шеврон
<(panel-left-close); свёрнутая — разделитель справа (panel-left-open). set_collapsed(bool); синхронизируетMainWindow.toggle_sidebar.
TopAppBar
QWidget#TopAppBarвысота 56, прозрачный поверх Panel; margins (8,0,8,0), spacing 4.- Слева
SidebarToggle, затем title «МойПроект» 18/Mediumon_surface, stretch, справаWindowButtonminimize/maximize/close._max_btn+_sync_max_stateчерез eventFilter наWindowStateChange.startSystemMove+ double-click maximize.
NavButton / NavigationDrawer
NavButton(QPushButton#NavButton)высота 48, шрифт 14, checkable; иконка 20×20 at x=16 рисуетсяQPainter(Material Symbols outlined): dashboard (4 квадрата), tasks (круг+check), edit (карандаш), chat (bubble), folder, tune (3 линии + knobs). Текст через QSSpadding-left: 48px.nextCheckState()переопределён — клик по уже выбранному пункту не снимает checked (no-op), иначе стандартный toggle QPushButton гасит выделение.- State layer pill (radius h/2): checked —
on_secondary_container, unchecked —on_surface, alpha 0.08hover+0.04press. Иконка: checkedon_secondary_container, uncheckedlerp(on_surface_variant, on_surface, hover). NavigationDrawer(QWidget)fixedWidth 220, bgsurface_container_low, margins (12,8,12,8), spacing 4; секция-лейбл «Навигация» 12/Mediumon_surface_variant.- Индикатор
QFrame#Indicator(bgsecondary_container, radius 24) анимируется по geometry к выбранной кнопке (DUR_MED); активный текст 14/Mediumon_secondary_container, неактивныйon_surface_variant. Клик по текущей — no-op;showEvent— индикатор без анимации.
6. Общий QSS (build_stylesheet)
#Panel— bgsurface, radius 16.#TopAppBar— прозрачный.#NavigationDrawer—surface_container_low+border-bottom-left-radius: 16.#Indicator—secondary_container, radius 24.QPushButton#NavButton— text-align left, border none, radius 24, padding-left 48, coloron_surface_variant, background transparent;:checked—on_secondary_container.- Скроллбары: вертикаль/горизонталь 8px margin 2px, handle radius 4
rgba(outline_variant,120)(hoveroutline), min 30; стрелки и страницы прозрачные. QToolTip—surface_container_high/on_surface, рамкаoutline_variant, radius 4.WA_StyledBackground = Trueна любом виджете с QSS-фоном.
7. Страницы (6 штук)
Компонуется через _page_shell(theme, title) и _add_card(theme, layout, title)
(карточка surface_container_low radius 12 + shadow elevation 1 + заголовок 16/Medium +
margins 16 + spacing 12). В _page_shell контент страницы обёрнут в прозрачный frameless
QScrollArea (setWidgetResizable(True), viewport прозрачный): страницы Ввод/Файлы выше
viewport при 960×600 и должны скроллиться, а не сжимать карточки ниже минимума; margins
(24,24,24,24) и spacing 16 относятся к внутреннему content-widget, заголовок 24,
bg transparent.
-
Обзор — «Система» (MdSwitch: Автозапуск ✓, Уведомления ✓, Энергосбережение ✗) и «Отображение» (MdSlider Яркость=70, Громкость=45). Строка слайдера — label слева, holder справа с
control_stretch=1(см. капкан про stretch). -
Задачи — 4
MdCheckBox; прогресс-бар 6px (tracksurface_container_highestr3, chunkprimaryr3), счётчик «Выполнено N из M», анимация черезQVariantAnimation(280мс); кнопкаMdButton("Сбросить", kind="tonal")в HBox +addStretch(1). -
Ввод — метка НАД полем (
_labeled_field, 12pxon_surface_variant):- Имя, E-mail — outlined
QLineEdit44px radius 4, рамка 1pxoutline, hoveron_surface_variant, focus 2pxprimary, bg transparent, текстon_surface, placeholder через palette, selectionprimary/on_primary. - Пароль — trailing «глаз» (
eye↔eye_slash), клик переключает EchoMode. - Поиск — pill radius 22 bg
surface_container_highбез рамки, leading-лупа, trailing-крестик только при непустом тексте (клик — очистка). - Многострочный —
QTextEdit110px radius 4 outlined + placeholder черезpalette.setColor(PlaceholderText, …)+ счётчик символов справа. - Иконки:
_field_icon(лупа/крестик рисуются) или_svg_icon— перед рендеромcurrentColorзаменяется наtheme.on_surface_variant(см. капкан).
- Имя, E-mail — outlined
-
Чат — см. раздел 8.
-
Файлы — см. раздел 9.
-
Настройки — «Внешний вид» (MdSwitch «Тёмная тема» →
_on_theme_toggled; метка «Размер интерфейса» сверху +MdSegmentedControl[Компактный/Средний/Крупный], default=1), «Поведение» (MdSwitch Автообновления ✓, Анимации ✓), комбобокс «Язык интерфейса» (44px, outlined r4; popupQAbstractItemView—surface_container, рамкаoutline_variant, r8, item 40px, selectionsecondary_container/on_secondary_container), MdSlider «Скорость анимаций»=50.
8. Чат (страница «Чат»)
- Контейнер — прозрачный
QScrollArea(frameless; QSSbackground: transparent+viewport().setStyleSheet(...transparent)+ внутренний QWidget прозрачный). - Сообщения в
self._chat_messages: list[(text, mine_bool, ts_str)]— переживают смену темы. При первом показе — 2 стартовых сообщения. - Строка: QWidget VBox (пузырь + растяжка по стороне) + (время 11px
on_surface_variant,padding 0 4px, по той же стороне). ПузыриQFrame#BubblemaxWidth 430, минимальная ширина считается поfontMetricsот текста (horizontalAdvance(text) + 32, кап 430) — без этого wordWrap-QLabel отдаёт узкий sizeHint и пузырь ломается на лишние строки; margins (16,10,16,10), шрифт 14, selectable:- свои: bg
primary_container, текстon_primary_container, радиусы (tl,tr,bl,br) = 16,16,16,4 (хвост снизу-справа); - чужие: bg
surface_container_high, текстon_surface, радиусы 16,16,4,16 (хвост снизу-слева), без рамки.
- свои: bg
- Вставка: VBox с последним
addStretch(1); новые строки в индексcount()-1. «Живая» вставка анимируется opacity 0→1 (200мс), эффект снимается по finished; при пересборке истории — без анимации. - Ввод:
QLineEdit44px (returnPressed) +MdButton("Отправить")(filled). Над вводом — «печатает…» (12pxon_surface_variant) справа. _on_chat_send: непустой текст → очистить поле,(text, True, now), вставить пузырь,_schedule_chat_reply(), проскроллить._schedule_chat_reply: флаг+лейбл «печатает», ответ изREPLIESпо_reply_i, задержка700 + (i%3)*400мс;_append_chat_reply: спрятать «печатает»,(text, False, now), вставить, проскроллить. Флаг/метка переживают пересборку. Прокрутка:verticalScrollBar().setValue(maximum())+QTimer.singleShot(0).
9. Файлы (страница «Файлы») — drag & drop
FileDropArea(см. класс) + под нейQListWidget110px (InternalMove+MoveAction+ExtendedSelection), счётчик «N файл/файла/файлов» (морфология: 1 → файл, 2-4 → файла, ≥5 → файлов).- Список принятых файлов в
self._dropped_files: list[str](имена черезPath(f).name), переживает смену темы;drop.dropped→ замыкание кладёт имена и перерисовывает список. - Пустой
QListWidgetпринятых файлов скрыт (setVisible(n > 0)) — вместо мёртвой зоны в карточке показывается подсказка «Пока нет файлов — перетащите что-нибудь в область выше». - Стиль списка (
_list_style): transparent bg, item padding 10px 12px radius 8, hoversurface_container_high, selectedsecondary_container/on_secondary_container. - Вторая карточка «Переупорядочивание» — демо-список из 5 файлов (150px,
InternalMove) с подсказкой.
10. Смена темы и сворачивание панели
Смена темы (_on_theme_toggled)
Только мгновенная пересборка — никакого fade центрального виджета (прозрачное окно просвечивало бы десктоп):
def _on_theme_toggled(self, is_dark: bool) -> None:
self._stop_anims()
self.theme = DARK if is_dark else LIGHT
old_central = self.takeCentralWidget()
if old_central is not None:
old_central.deleteLater()
self._build_ui()
Состояние, переживающее пересборку (атрибуты MainWindow): _current_page,
_sidebar_collapsed, _chat_messages, _chat_typing, _reply_i, _dropped_files.
_stop_anims() останавливает висящие анимации (иначе crash при deleteLater).
Сворачивание панели (toggle_sidebar / _animate_sidebar)
QParallelAnimationGroup анимирует minimumWidth+maximumWidth drawer (220→0) и
разделителя (1→0), DUR_MED/EASE. Глиф SidebarToggle синхронизируется через
_title_bar.sidebar_toggle.set_collapsed(...).
Переход между страницами (switch_page)
QStackedWidget + QGraphicsOpacityEffect на обе страницы: prev 1→0, next 0→1 параллельно
(DUR_MED), по завершении эффекты снимаются, ссылка на группу в _page_anim.
11. Капканы PyQt6 на Windows — проверяй первым делом
0xC0000409 / 0xC0000005 в PyQt6 + Python 3.12 на Windows — обычно один из этих случаев:
QPen.setAlphaне существует — это методQColor. Правильно:color = QColor(...); color.setAlpha(90); pen = QPen(color).QPainter.drawLineтолько int-координаты → иначе 0xC0000409. При этомdrawEllipse(QPointF, r, r),drawRoundedRect(QRectF),drawPathсо float безопасны.- Альфа в QSS
rgba()— 0..255. Для «8%» пиши alpha ≈ 20. Qt.Edgeне конвертируется вintи не смешивается сintчерез|(раздел 4).QSvgRendererигнорируетfill="currentColor"— замени строкой до рендера:svg.replace("currentColor", theme.on_surface_variant).- QSS
:hoverмгновенный — плавность только черезQ_PROPERTY+QPropertyAnimation+ ручнойpaintEvent. WA_StyledBackgroundобязателен на виджете с QSS-фоном.- QBoxLayout стрейч: строка label+control — если control растягивается, то
addWidget(control, stretch)и безaddStretch. - Держи ссылку на анимации (
self._anim = ...), иначе GC соберёт их до конца. app.setStyle("Fusion")— иначе native-стиль местами игнорирует QSS.- Никаких try/except вокруг
paintEventкак «защиты» — процесс умирает раньше; просто корректный код + smoke-тест.
12. Проверка после любых изменений (smoke-тест)
Перед сдачей прогони (временный скрипт рядом с файлом, потом удалить):
- QApplication + MainWindow,
show(),processEvents()— без исключений; - клики по кнопкам NavigationDrawer по всем 6 страницам (проверять синхронность
stack.currentIndex, checked-кнопки и_current_page; прямойswitch_pageобходит навигацию) — без трейсбэков; _on_theme_toggled(True)и(False)— пересборка,_current_pageсохраняется;- повторить цикл страницы→тема 3 раза без дублирования сообщений/файлов;
- чат:
_on_chat_send()с текстом → сообщение + очистка поля, через ~1.5с + processEvents приходит ответ, «печатает» гаснет, история переживает смену темы; - файлы:
area.dropped.emit([...])→ имена в списке, переживают смену темы; - ресайз:
_resize_edges_forдля краёв/углов/центра (центр = 0); - задачи: чекбоксы + прогресс + tonal-«Сбросить»; sidebar collapse/expand;
- скриншоты
window.grab()— 12 штук вassets/_qa/:{light,dark}_{overview,tasks,input,chat,files,settings}.png. Процесс не должен падать нативно; при падении — чинить и перезапускать до чистого прогона. Скрипт после чистого прогона удалить, скриншоты оставить.
Чек-лист «на глаз»: ролевые пары контрастны в обеих темах; state layers/ripple заметны; анимации ≤ ~300мс (ripple 450); окно таскается за TopAppBar, ресайзится за края; карточки не обрезаны (запас 12px под тень); скроллбары тонкие; drawer-pill и сегмент-индикатор переезжают плавно.
13. Проверка соответствия эталону
- Frameless + translucent +
QFrame#Panelсо скруглением 16 и тенью. - Material-кнопки окна (min/max/close), close на hover — error; порядок справа.
- Перетаскивание за TopAppBar (
startSystemMove), double-click — разворот. - Ресайз за края/углы (
startSystemResize, EDGE_MARGIN 12) + курсоры. - Сворачивание drawer с анимацией ширины и сменой глифа SidebarToggle.
- MdButton filled/tonal с state layer + ripple; MdSwitch/MdSlider/MdCheckBox/ MdSegmentedControl анимированы (не QSS-hover).
- 6 страниц с перечисленным контентом; fade-переходы.
- Смена темы — мгновенная пересборка; чат и файлы сохраняются.
- Чат с MD3-пузырями, временем, «печатает…», автоответом.
- Drag & drop файлов + переупорядочивание списком.
- Ролевая палитра MD3 (primary/on-primary/containers), текст через on-роли.
- Скроллбары тонкие во всех страницах.
- Ни одного из капканов раздела 11 в коде.