mawwki/pyqt6-macos-ui

Créez, reconcevez ou restylez une application GUI de bureau en PyQt6/PySide6 pour lui donner l'apparence et les sensations d'une application macOS soignée (fenêtre sans cadre, feux tricolores, barre latérale, cartes avec ombres douces, micro-interactions animées, thèmes clair/sombre).

Qu'est-ce que pyqt6-macos-ui ?

Ce skill fournit une spécification complète et détaillée pour reproduire le comportement et l'apparence d'une application de bureau de style macOS construite avec PyQt6/PySide6, en utilisant comme référence le fichier `assets/app_template.py`. Il est conçu pour les développeurs qui souhaitent créer des interfaces utilisateur modernes et élégantes qui ne ressemblent pas à une application Qt par défaut, en particulier sur Windows, Linux et macOS. La spécification inclut des jetons de design précis (couleurs, polices, rayons, animations), la structure du code, des widgets personnalisés (boutons, interrupteurs, curseurs, contrôles segmentés, feux tricolores, etc.) avec des détails de rendu et d'animation, ainsi que des pièges spécifiques à PyQt6 (comme la gestion correcte des `Qt.Edge` pour le redimensionnement natif de la fenêtre ou l'utilisation de `accent_fg()` pour l'accessibilité des contrastes). Le skill couvre également six pages d'application (Aperçu, Tâches, Saisie, Chat, Fichiers, Paramètres) avec des interactions détaillées, une barre latérale de navigation, des animations de type macOS, et des conseils pour éviter les plantages courants sur Windows. L'objectif est de permettre à un agent IA ou à un développeur de reproduire ou d'adapter cette interface cohérente, macrée et hautement interactive en suivant une approche de conception de type "studio", avec une règle d'or : le style macOS dans Qt est une esthétique recréée manuellement, pas une API native.

Compatible avec~Claude Code~Codex CLI✓Cursor
npx skills add https://github.com/mawwki/pyqt6-ui-skills/tree/HEAD/pyqt6-macos-ui

Installed? Explore more Développement et programmation skills: steipete/bluebubbles, steipete/eightctl, steipete/blucli · View all 6 →

Demander à votre IA préférée

Ouvre une nouvelle conversation avec cette compétence d'agent déjà préchargée.

Documentation

PyQt6 en style macOS — spécification de l'application de référence

Ci-dessous — la spécification complète pour reproduire assets/app_template.py bit par bit en termes de comportement. Modélise l'agent designer d'interfaces d'un studio qui crée applications de bureau de style macOS : palette sobre, une couleur d'accentuation, lumière douce, animations exactement là où elles expliquent ce qui s'est passé — et pas plus.

Règle principale : « style macOS » dans Qt n'est pas une API native, c'est une esthétique recréée manuellement : fenêtre sans cadre + QSS personnalisé + QPropertyAnimation pour la fluidité. Ainsi, l'application s'affiche de la même manière sur Windows, Linux et macOS. Le style natif Qt ne fournit pas d'arrondis, de semi-transparence ni d'animations.

Référence d'implémentation — assets/app_template.py (un seul fichier). La tâche de l'agent : soit copier et l'adapter, soit le reconstruire à partir de zéro en suivant précisément ce document.


1. Démarrage rapide

pip install PyQt6
python app_template.py

Construction pour l'utilisateur : pyinstaller --windowed --icon=app.icns app_template.py.

L'application se lance ainsi :

def main() -> None:
    app = QApplication(sys.argv)
    app.setStyle("Fusion")                       # seul style respectant QSS partout
    app.setFont(system_font(10, QFont.Weight.Normal))
    window = MainWindow()
    window.show()
    sys.exit(app.exec())

if __name__ == "__main__":
    main()

2. Structure du fichier app_template.py (ordre important pour la lisibilité)

  1. Imports : sys, time, dataclasses, pathlib.Path ; QtCore (Qt, QEvent, QPropertyAnimation, QEasingCurve, QRect( F), QPointF, QParallelAnimationGroup, QVariantAnimation, QTimer, pyqtProperty, pyqtSignal) ; QtGui (QColor, QFont, QPainter, QFontDatabase, QPen, QPixmap, QIcon, QPalette) ; QtSvg (QSvgRenderer) ; QtWidgets (tous les widgets nécessaires).
  2. Jetons de design : Theme (dataclass), LIGHT/DARK, constantes de rayons/durées, REPLIES, SVG_EYE/SVG_EYE_SLASH, PAGES.
  3. Helpers : system_font, rgba, accent_fg, shadow_color.
  4. Widgets personnalisés dans l'ordre des dépendances : HoverButton, ToggleSwitch, MacSlider, SegmentedControl, FileDropArea, TrafficLight, SidebarToggle, TitleBar, Sidebar.
  5. build_stylesheet(theme).
  6. MainWindow + main().

3. Jetons de design (valeurs exactes)

Theme — dataclass figée avec les champs : bg, surface, sidebar, text, text_secondary, accent, separator_rgb (chaîne #000000/#FFFFFF — base pour rgba()), separator_alpha (0..255), shadow_alpha (0..255).

LIGHT = Theme(bg="#F5F5F7", surface="#FFFFFF", sidebar="#ECECEF", text="#1D1D1F",
              text_secondary="#6E6E73", accent="#1D1D1F",
              separator_rgb="#000000", separator_alpha=20, shadow_alpha=55)
DARK  = Theme(bg="#1E1E1E", surface="#2A2A2C", sidebar="#242426", text="#F5F5F7",
              text_secondary="#98989D", accent="#CFCFD4",
              separator_rgb="#FFFFFF", separator_alpha=20, shadow_alpha=150)

Important : l'accent dans cette application n'est pas bleu. Il est « inversé » — noir #1D1D1F dans le thème clair et clair #CFCFD4 dans le thème sombre (cela donne le même contraste que le System Blue, mais dans une gamme noir et blanc). Tout code qui place du texte sur un accent doit utiliser accent_fg(theme), pas une valeur codée en dur.

def accent_fg(theme: Theme) -> str:
    color = QColor(theme.accent)
    luminance = (0.299 * color.red() + 0.587 * color.green() + 0.114 * color.blue()) / 255
    return "#1D1D1F" if luminance > 0.6 else "#FFFFFF"

def rgba(hex_color: str, alpha_0_255: int) -> str:
    c = QColor(hex_color)
    return f"rgba({c.red()}, {c.green()}, {c.blue()}, {alpha_0_255})"

Constantes :

  • RADIUS_WINDOW = 14, RADIUS_CARD = 12, RADIUS_CONTROL = 8, SIDEBAR_WIDTH = 196.
  • EDGE_MARGIN = 6, masques de bits des bords EDGE_TOP, EDGE_BOTTOM, EDGE_LEFT, EDGE_RIGHT = 1, 2, 4, 8.
  • DUR_FAST = 140 (hover/press), DUR_MED = 250 (pages, barre latérale, interrupteur, segments), EASE = QEasingCurve.Type.OutCubic.
  • PAGES = ["Обзор", "Задачи", "Ввод", "Чат", "Файлы", "Настройки"].
  • REPLIES — 5 réponses courtes du bot.
  • SVG_EYE (Heroicons, viewBox 0 0 24 24, fill="currentColor") et SVG_EYE_SLASH (Bootstrap, viewBox 0 0 16 16) — chaînes multilignes.

Police : chaîne de fallback dans system_font() — Inter → Segoe UI Variable Text → .AppleSystemUIFont → SF Pro Text → Helvetica Neue → Manrope → Segoe UI ; et setStyleStrategy(QFont.StyleStrategy.PreferAntialias).

Marges basées sur l'échelle 4/8/12/16/20/24/32. Ombres douces : cartes — blurRadius 24, offset (0,5), couleur shadow_color(theme) (noir avec shadow_alpha).


4. Structure de la fenêtre

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)   # pour le redimensionnement par les bords

Arbre (_build_ui) : QMainWindow → root (QWidget) → panel (QFrame, objectName "Panel") → TitleBar (40px) + body (QHBoxLayout) → Sidebar (196px) + separator (1px) + QStackedWidget. Le panneau reçoit build_stylesheet(theme) — QSS est hérité vers le bas.

L'ombre et l'arrondi vivent sur le QFrame#Panel enfant, pas sur la fenêtre elle-même : la fenêtre est transparente et fournit une marge pour l'ombre. C'est fondamental — appliquer un effet sur QMainWindow est impossible.

Glisser-déposer et redimensionnement

Déplacer la fenêtre — mécanisme natif Qt6, aucune mathématique manuelle de décalage :

def mousePressEvent(self, event):   # dans TitleBar
    if event.button() == Qt.MouseButton.LeftButton:
        handle = self._window.windowHandle()
        if handle is not None:
            handle.startSystemMove()

Changer la taille par les bords — également natif QWindow.startSystemResize. Dans MainWindow est installé un filtre d'événements sur QApplication, qui :

  • sur MouseButtonPress (Left) dans la zone ≤ EDGE_MARGIN du bord de la fenêtre appelle _start_system_resize(edges) et renvoie True (l'événement est consommé) ;
  • sur MouseMove sans bouton met à jour le curseur de la fenêtre ;
  • sur Leave réinitialise le curseur.

_resize_edges_for(pos) convertit la position globale en masque de bits des bords ; fonctionne uniquement si la fenêtre est visible et non maximisée. _cursor_for_edges(edges) : centre → Arrow ; gauche/droite → SizeHor ; haut/bas → SizeVer ; haut+gauche ou bas+droite → SizeFDiag ; sinon → SizeBDiag.

Piège PyQt6 : Qt.Edge ne peut pas être converti en int et ne peut pas être mélangé avec int par l'opérateur | (TypeError). Par conséquent, toute la mathématique des bords est basée sur ses propres constantes de bits EDGE_*, et le flag pour startSystemResize est assemblé ainsi :

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. Widgets personnalisés (paramètres exacts)

HoverButton (bouton avec hover/press progressif)

  • QPushButton, setFlat(True), hauteur 34, police 13/Medium, padding 0 18px, rayon RADIUS_CONTROL.
  • Texte en QSS : color: {accent_fg(theme)} (pas « white » en dur !).
  • Fond — pas du QSS :hover (instantané), mais Q_PROPERTY alpha (float, 0..1) : dans paintEvent on peint un QColor accentué avec setAlphaF(alpha) + rectangle arrondi, puis super().paintEvent() dessine le texte. Événements : enter → 0.85, leave → 1.0, press → 0.7, release → 0.85/1.0. Animation DUR_FAST, EASE.

ToggleSwitch (interrupteur macOS)

  • QWidget 42×24, signal toggled(bool), gestionnaire seulement sur bouton gauche.
  • Q_PROPERTY knobPos (float 0..1) animé DUR_MED/EASE.
  • Dessin : piste arrondie (rayon = h/2), couleur interpolée entre #B9B9BE (éteint) et accent (allumé) ; poignée — cercle QColor(self._theme.bg), diamètre h-4, position x_min = 2, x_max = width - knob_d - 2.
  • Piège : la poignée est peinte avec theme.bg, pas du blanc — dans le thème sombre, une poignée blanche se confond avec la piste blanche éteinte.
  • setChecked programmatique anime seulement, le signal N'EST PAS émis (émis dans mousePressEvent).

MacSlider (curseur macOS)

  • QWidget hauteur 28, Q_PROPERTY knobScale (1.0 → 1.12 hover, 0.94 press), DUR_FAST.
  • Valeur depuis x : ratio = (x - pad) / (width - 2*pad), pad = 9.5.
  • Dessin : piste hauteur 6 (fond separator alpha 26, remplissage — accent), cercle 9.5px : glow (accent, alpha 22 hover / 34 press, rayon +4), ombre (noir alpha 26), le cercle lui-même theme.bg avec bord separator alpha 90 (255 quand pressed), penWidth 1.2.

SegmentedControl (segments « Compact/Moyen/Grand »)

  • QWidget hauteur 30 ; boutons hauteur 26, police 12, padding 0 16px, box spacing 4, margins 4.
  • « Pilule » — QFrame#SegIndicator (bg accent, rayon 7) derrière les boutons dans la même disposition, se déplace avec QPropertyAnimation(geometry) DUR_MED/EASE.
  • Couleur du texte : pour le sélectionné accent_fg(theme), pour les autres text_secondary.
  • Dans showEvent, abaisser la pilule (lower()) et la placer sur la géométrie du bouton sélectionné.

TrafficLight (feu tricolore)

  • QPushButton 22×22, Q_PROPERTY hover (0..1) animation DUR_MED.
  • Sans bordures de couleurs jaune/vert : close — rouge #FF5F57 (alpha 200 + 35·hover), minimize/zoom — neutre rgba(text_secondary, 60 + 40·hover).
  • Glyphes dessinés avec QPen avec RoundCap, largeur 2, coordonnées entières uniquement (voir pièges) :
    • close — croix de deux lignes ;
    • minimize — chevron vers le bas : (cx-5,cy-3)-(cx,cy+3)-(cx+5,cy-3) ;
    • maximize (non déplié) — chevron vers le haut : (cx-5,cy+3)-(cx,cy-3)-(cx+5,cy+3) ;
    • maximize en état déplié (_maximized) — 8 lignes de quatre coins-crochets.
  • Couleur du glyphe : pour close — blanc alpha 205+30·hover ; pour les autres — interpolation text_secondary → text selon hover.

SidebarToggle (bouton de réduction du panneau)

  • QPushButton 22×22, hover 0..1, hover-bg rgba(separator,26), glyphe interpolant text_secondary → text.
  • Glyphe du panneau ouvert (Lucide panel-left-close) : cadre drawRoundedRect(QRectF(cx-7, cy-7, 14, 14), 2, 2), séparateur vertical sur cx-2, chevron < depuis (cx+4, cy-2)→(cx, cy)→(cx+4, cy+2).
  • Glyphe du panneau réduit (Lucide panel-right) : cadre + séparateur sur cx+2, pas de chevron.
  • set_collapsed(bool) change le glyphe ; l'état est synchronisé par MainWindow.toggle_sidebar.

Sidebar (navigation)

  • QWidget setFixedWidth(SIDEBAR_WIDTH), margins 10,16,10,16, spacing 2.
  • Marque « MonProjet » (13/Bold, padding: 0 8px 14px 8px).
  • Noms des pages — QPushButton#NavButton hauteur 30, police 13, checkable, inactif — text_secondary, actif — text. Indicateur — QFrame rayon 8, bg rgba(accent,40), animé par geometry vers le bouton sélectionné (DUR_MED).
  • Clic inactif sur la page courante — pas d'opération. Dans showEvent, l'indicateur est placé sans animation.

6. QSS global (build_stylesheet)

  • #Panel — bg du thème, rayon de la fenêtre. #TitleBar — transparent. #Sidebar — bg de la barre latérale.
  • #Indicator — rgba(accent, 40), rayon 8.
  • QPushButton#NavButton — alignement à gauche, rayon 8, padding-left 10 ; :checked — text ; :hover — rgba(separator,26).
  • QPushButton#ToolButton — couleur secondaire, rayon 6, hover surbrillance.
  • Barres de défilement (ajoutées globalement, pour que le chat et les listes ne soient pas « épaisses ») : verticale/horizontale 8px, poignée rayon 3 rgba(separator,60) (hover 120), min 30, flèches et pages — transparentes.
  • Définir setAttribute(Qt.WidgetAttribute.WA_StyledBackground, True) sur tout widget avec des fonds QSS.

7. Pages (6 pièces)

Composée via _page_shell(theme, title) (margins 28/24/28/24, spacing 16, titre en gras 22) et _add_card(theme, layout, title) (carte + titre 14/DemiBold + ombre).

  1. Aperçu — carte « Système » (interrupteurs : Démarrage auto ✓, Notifications ✓, Économie d'énergie ✗) et « Affichage » (curseurs Luminosité=70, Volume=45). Ligne du curseur — label à gauche, contrôle à droite avec control_stretch=1 (voir piège sur stretch).

  2. Tâches — 4 QCheckBox avec indicateur 18×18 (border rgba(separator,120), hover → accent, checked → bg accent) ; barre de progression (12px, chunk accent), compteur « Terminé N sur M », animation QProgressBar via QVariantAnimation (280ms) ; bouton HoverButton("Réinitialiser").

  3. Saisie — champs :

    • Nom, E-mail — QLineEdit normal (34px, rayon 8, hover bordure rgba(sep,140), focus → bordure accent + teinte rgba(accent,8), placeholder text_secondary, selection accent+accent_fg).
    • Mot de passe — _build_password_field : action de fin « œil » — icône change eye ↔ eye_slash, clic bascule EchoMode.Password ↔ Normal.
    • Recherche — _build_search_field : pill-rayon 17, icône de loupe au début, icône de croix à la fin, visible uniquement quand le texte n'est pas vide (clic — effacement).
    • Multiligne — QTextEdit (rayon 10, placeholder via palette.setColor(QPalette.ColorRole.PlaceholderText, ...) — le sélecteur QSS QTextEdit[placeholderText] n'est pas fiable) + compteur de caractères à droite.
    • Les icônes des champs sont dessinées dans _field_icon (18px : loupe = cercle+poignée ; croix = cercle rgba(text_secondary,110) + « ✕ » couleur theme.bg) ou rendues depuis SVG (_svg_icon) : avant le rendu, on remplace dans le SVG currentColor par theme.text_secondary (voir piège).
  4. Chat — voir section 8.

  5. Fichiers — voir section 9.

  6. Paramètres — carte « Apparence » (interrupteur « Thème sombre » → _on_theme_toggled, segment « Taille de l'interface »), « Comportement » (interrupteurs Mises à jour auto ✓, Animations ✓), combo box « Langue de l'interface » (QSS : champ surface+bordure, QAbstractItemView — selection-background-color: accent; selection-color: accent_fg), curseur « Vitesse des animations »=50.


8. Chat (page « Chat »)

  • Conteneur — QScrollArea transparente (sans cadre ; pour éviter le fond blanc : QSS background: transparent + viewport().setStyleSheet(...transparent) + QWidget interne transparent).
  • Messages stockés dans `self._chat_messages: list[(text, m

... (suite du contenu tronquée)

Individual skills in this repo

This repo contains 1 individual skill — each has its own dedicated page.

Skills associés