CommunityCodierung & Entwicklunggithub.com

mawwki/pyqt6-macos-ui

Erstelle, gestalte oder überarbeite eine Desktop-GUI-App mit PyQt6/PySide6, die wie eine polierte macOS-App aussieht und sich anfühlt (rahmenloses Fenster, Verkehrsampel, Seitenleiste, Karten mit weichen Schatten, animierte Mikrointeraktionen, helle/dunkle Themen).

Was ist pyqt6-macos-ui?

Dieses Skill bietet eine vollständige Spezifikation und Referenzimplementierung (assets/app_template.py) für die Erstellung von PyQt6/PySide6-Desktop-Anwendungen im macOS-Stil. Es ermöglicht Entwicklern, rahmenlose Fenster mit benutzerdefinierten Titelleisten und Verkehrsampeln (Schließen, Minimieren, Maximieren) zu erstellen und enthält eine Reihe von benutzerdefinierten Widgets wie HoverButton, ToggleSwitch, MacSlider, SegmentedControl und FileDropArea, die alle mit weichen Schatten, sanften Animationen und einer konsistenten Farbpalette gestaltet sind. Das Skill unterstützt sowohl helle als auch dunkle Themen und enthält eine detaillierte Dokumentation der Design-Tokens (Farben, Schriftarten, Radien, Dauern) sowie Fallstricke bei der PyQt6-Entwicklung unter Windows (z. B. die Verwendung von Qt.Edge anstelle von int-Bitmasken). Es ist ideal für Entwickler, die eine moderne, macOS-ähnliche Benutzeroberfläche erstellen möchten, die auf Windows, Linux und macOS gleich aussieht, und die native Qt-Stile umgehen möchten, die keine abgerundeten Ecken, Transparenz oder Animationen bieten.

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

Installed? Explore more Codierung & Entwicklung skills: steipete/bluebubbles, steipete/eightctl, steipete/blucli · View all 6 →

In Ihrer bevorzugten KI fragen

Öffnet einen neuen Chat, in dem dieser Agent-Skill bereits geladen ist.

Dokumentation

PyQt6 im macOS-Stil — Spezifikation der Referenzanwendung

Unten ist die vollständige Spezifikation, um assets/app_template.py Byte für Byte im Verhalten zu reproduzieren. Modelliere einen UI-Designer-Agenten aus einem Studio, das macOS-ähnliche Desktop-Anwendungen erstellt: gedämpfte Palette, eine Akzentfarbe, weiches Licht, Animationen genau dort, wo sie erklären, was passiert ist – und nicht mehr.

Hauptregel: „macOS-Stil“ in Qt ist keine native API, sondern eine von Hand nachgebildete Ästhetik: rahmenloses Fenster + eigenes QSS + QPropertyAnimation für Flüssigkeit. So sieht die Anwendung auf Windows, Linux und macOS gleich aus. Der native Qt-Stil bietet keine abgerundeten Ecken, Transparenz und Animationen.

Die Referenzimplementierung ist assets/app_template.py (eine Datei). Die Aufgabe des Agenten ist entweder, sie zu kopieren und anzupassen oder sie von Grund auf genau nach diesem Dokument neu zu erstellen.


1. Schnellstart

pip install PyQt6
python app_template.py

Build für den Benutzer: pyinstaller --windowed --icon=app.icns app_template.py.

Die Anwendung wird so gestartet:

def main() -> None:
    app = QApplication(sys.argv)
    app.setStyle("Fusion")                       # einziger Stil, der QSS überall respektiert
    app.setFont(system_font(10, QFont.Weight.Normal))
    window = MainWindow()
    window.show()
    sys.exit(app.exec())

if __name__ == "__main__":
    main()

2. Struktur der Datei app_template.py (Reihenfolge wichtig für Lesbarkeit)

  1. Importe: 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 (alle benötigten Widgets).
  2. Design-Tokens: Theme (dataclass), LIGHT/DARK, Konstanten für Radien/Dauern, REPLIES, SVG_EYE/SVG_EYE_SLASH, PAGES.
  3. Hilfsfunktionen: system_font, rgba, accent_fg, shadow_color.
  4. Benutzerdefinierte Widgets in Abhängigkeitsreihenfolge: HoverButton, ToggleSwitch, MacSlider, SegmentedControl, FileDropArea, TrafficLight, SidebarToggle, TitleBar, Sidebar.
  5. build_stylesheet(theme).
  6. MainWindow + main().

3. Design-Tokens (exakte Werte)

Theme — frozen dataclass mit Feldern: bg, surface, sidebar, text, text_secondary, accent, separator_rgb (Zeichenkette #000000/#FFFFFF — Basis für 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)

Wichtig: Der Akzent in dieser Anwendung ist nicht blau. Er ist „invertiert“ – schwarz #1D1D1F im hellen Thema und hell #CFCFD4 im dunklen (dies bietet denselben Kontrast wie System Blue, aber in Schwarz-Weiß). Jeglicher Code, der Text über den Akzent legt, muss accent_fg(theme) verwenden, nicht hartcodieren.

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})"

Konstanten:

  • RADIUS_WINDOW = 14, RADIUS_CARD = 12, RADIUS_CONTROL = 8, SIDEBAR_WIDTH = 196.
  • EDGE_MARGIN = 6, Bitmasken für Kanten EDGE_TOP, EDGE_BOTTOM, EDGE_LEFT, EDGE_RIGHT = 1, 2, 4, 8.
  • DUR_FAST = 140 (hover/press), DUR_MED = 250 (Seiten, Seitenleiste, Schalter, Segmente), EASE = QEasingCurve.Type.OutCubic.
  • PAGES = ["Übersicht", "Aufgaben", "Eingabe", "Chat", "Dateien", "Einstellungen"].
  • REPLIES — 5 kurze Bot-Antworten.
  • SVG_EYE (Heroicons, viewBox 0 0 24 24, fill="currentColor") und SVG_EYE_SLASH (Bootstrap, viewBox 0 0 16 16) — mehrzeilige Zeichenketten.

Schriftart: Fallback-Kette in system_font() — Inter → Segoe UI Variable Text → .AppleSystemUIFont → SF Pro Text → Helvetica Neue → Manrope → Segoe UI; plus setStyleStrategy(QFont.StyleStrategy.PreferAntialias).

Abstände aus der Skala 4/8/12/16/20/24/32. Schatten weich: Karten — blurRadius 24, offset (0,5), Farbe shadow_color(theme) (schwarz mit shadow_alpha).


4. Fensterrahmen

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)   # für Größenänderung an den Kanten

Baum (_build_ui): QMainWindow → root (QWidget) → panel (QFrame, objectName "Panel") → TitleBar (40px) + body (QHBoxLayout) → Sidebar (196px) + separator (1px) + QStackedWidget. Das Panel erhält build_stylesheet(theme) — QSS wird nach unten vererbt.

Schatten und Abrundung leben auf dem untergeordneten QFrame#Panel, nicht auf dem Fenster selbst: Das Fenster ist transparent und bietet Platz für den Schatten. Dies ist grundlegend – das Anhängen eines Effekts an QMainWindow ist nicht möglich.

Ziehen und Größenänderung

Fenster bewegen — nativer Mechanismus von Qt6, keine manuelle Verschiebungsmathematik:

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

Größe an den Kanten ändern — ebenfalls natives QWindow.startSystemResize. In MainWindow ist ein Ereignisfilter auf QApplication installiert, der:

  • bei MouseButtonPress (Links) im Bereich ≤ EDGE_MARGIN vom Fensterrand _start_system_resize(edges) aufruft und True zurückgibt (Ereignis wird verbraucht);
  • bei MouseMove ohne Tasten den Cursor des Fensters aktualisiert;
  • bei Leave den Cursor zurücksetzt.

_resize_edges_for(pos) übersetzt die globale Position in eine Bitmaske der Kanten; funktioniert nur, wenn das Fenster sichtbar und nicht maximiert ist. _cursor_for_edges(edges): Mitte → Arrow; links/rechts → SizeHor; oben/unten → SizeVer; oben+links oder unten+rechts → SizeFDiag; sonst → SizeBDiag.

PyQt6-Falle: Qt.Edge kann nicht in int umgewandelt und nicht mit dem |-Operator mit int gemischt werden (TypeError). Daher wird die gesamte Kantenmathematik mit eigenen Bitkonstanten EDGE_* durchgeführt, und das Flag für startSystemResize wird so zusammengesetzt:

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. Benutzerdefinierte Widgets (genaue Parameter)

HoverButton (Taste mit fließendem hover/press)

  • QPushButton, setFlat(True), Höhe 34, Schriftart 13/Medium, padding 0 18px, Radius RADIUS_CONTROL.
  • Text in QSS: color: {accent_fg(theme)} (nicht hartcodiert „white“!).
  • Hintergrund — nicht QSS :hover (sofortig), sondern Q_PROPERTY alpha (float, 0..1): im paintEvent wird eine Akzentfarbe QColor mit setAlphaF(alpha) + abgerundetes Rechteck gemalt, dann zeichnet super().paintEvent() den Text. Ereignisse: enter → 0.85, leave → 1.0, press → 0.7, release → 0.85/1.0. Animation DUR_FAST, EASE.

ToggleSwitch (macOS-Schalter)

  • QWidget 42×24, Signal toggled(bool), Handler nur bei linker Maustaste.
  • Q_PROPERTY knobPos (float 0..1) wird mit DUR_MED/EASE animiert.
  • Zeichnung: Spur abgerundet (radius = h/2), Farbe interpoliert zwischen #B9B9BE (aus) und accent (ein); Knopf — Kreis QColor(self._theme.bg), Durchmesser h-4, Position x_min = 2, x_max = width - knob_d - 2.
  • Falle: Der Knopf wird mit theme.bg gefärbt, nicht mit Weiß – im dunklen Thema verschmilzt ein weißer Knopf mit der weißen, ausgeschalteten Spur.
  • Programmatisches setChecked animiert nur, Signal wird NICHT emittiert (emittiert in mousePressEvent).

MacSlider (macOS-Schieberegler)

  • QWidget Höhe 28, Q_PROPERTY knobScale (1.0 → 1.12 hover, 0.94 press), DUR_FAST.
  • Wert aus x: ratio = (x - pad) / (width - 2*pad), pad = 9.5.
  • Zeichnung: Spurhöhe 6 (Hintergrund separator mit alpha 26, Füllung — accent), Kreis 9.5px: Glanz (accent, alpha 22 hover / 34 press, radius +4), Schatten (schwarz alpha 26), der Kreis selbst theme.bg mit Rand separator alpha 90 (255 bei pressed), penWidth 1.2.

SegmentedControl (Segmente „Kompakt/Mittel/Groß“)

  • QWidget Höhe 30; Tasten Höhe 26, Schriftart 12, padding 0 16px, Box spacing 4, margins 4.
  • „Pille“ — QFrame#SegIndicator (bg accent, radius 7) hinter den Tasten im selben Layout, bewegt sich mit QPropertyAnimation(geometry) DUR_MED/EASE.
  • Textfarbe: bei ausgewählter accent_fg(theme), bei den restlichen text_secondary.
  • In showEvent die Pille nach unten schieben (lower()) und auf die Geometrie der ausgewählten Taste setzen.

TrafficLight (Ampel)

  • QPushButton 22×22, Q_PROPERTY hover (0..1) Animation DUR_MED.
  • Ohne Gelb/Grün-Farbrahmen: close — rot #FF5F57 (alpha 200 + 35·hover), minimize/zoom — neutral rgba(text_secondary, 60 + 40·hover).
  • Glyphen werden mit QPen mit RoundCap gezeichnet, Breite 2, nur ganzzahlige Koordinaten (siehe Fallen):
    • close — Kreuz aus zwei Linien;
    • minimize — Chevron nach unten: (cx-5,cy-3)-(cx,cy+3)-(cx+5,cy-3);
    • maximize (nicht maximiert) — Chevron nach oben: (cx-5,cy+3)-(cx,cy-3)-(cx+5,cy+3);
    • maximize im maximierten Zustand (_maximized) — 8 Linien von vier Eckklammern.
  • Glyphenfarbe: für close — weiß alpha 205+30·hover; für die anderen — Interpolation text_secondary → text nach hover.

SidebarToggle (Schaltfläche zum Einklappen der Seitenleiste)

  • QPushButton 22×22, hover 0..1, hover-bg rgba(separator,26), Glyph interpoliert text_secondary → text.
  • Glyph der geöffneten Seitenleiste (Lucide panel-left-close): Rahmen drawRoundedRect(QRectF(cx-7, cy-7, 14, 14), 2, 2), vertikaler Trenner bei cx-2, Chevron < aus (cx+4, cy-2)→(cx, cy)→(cx+4, cy+2).
  • Glyph der eingeklappten (Lucide panel-right): Rahmen + Trenner bei cx+2, kein Chevron.
  • set_collapsed(bool) schaltet den Glyph um; der Zustand wird von MainWindow.toggle_sidebar synchronisiert.

Sidebar (Navigation)

  • QWidget setFixedWidth(SIDEBAR_WIDTH), margins 10,16,10,16, spacing 2.
  • Marke „MeinProjekt“ (13/Bold, padding: 0 8px 14px 8px).
  • Seitennamen — QPushButton#NavButton Höhe 30, Schriftart 13, checkable, inaktiv — text_secondary, aktiv — text. Indikator — QFrame Radius 8, bg rgba(accent,40), animiert über geometry zur ausgewählten Taste (DUR_MED).
  • Inaktiver Klick auf die aktuelle Seite — no-op. In showEvent wird der Indikator ohne Animation gesetzt.

6. Allgemeines QSS (build_stylesheet)

  • #Panel — bg des Themas, Radius des Fensters. #TitleBar — transparent. #Sidebar — bg der Seitenleiste.
  • #Indicator — rgba(accent, 40), radius 8.
  • QPushButton#NavButton — linksbündig, radius 8, padding-left 10; :checked — text; :hover — rgba(separator,26).
  • QPushButton#ToolButton — sekundäre Farbe, radius 6, hover-Hervorhebung.
  • Bildlaufleisten (global hinzugefügt, damit Chat und Listen nicht „dick“ sind): vertikal/horizontal 8px, handle radius 3 rgba(separator,60) (hover 120), min 30, Pfeile und Seiten — transparent.
  • setAttribute(Qt.WidgetAttribute.WA_StyledBackground, True) auf jedem Widget mit QSS-Hintergründen setzen.

7. Seiten (6 Stück)

Wird über _page_shell(theme, title) (margins 28/24/28/24, spacing 16, Überschrift 22/Bold) und _add_card(theme, layout, title) (Karte + Überschrift 14/DemiBold + shadow) zusammengestellt.

  1. Übersicht — Karte „System“ (Schalter: Autostart ✓, Benachrichtigungen ✓, Energiesparen ✗) und „Anzeige“ (Schieberegler Helligkeit=70, Lautstärke=45). Die Schieberegler-Zeile — label links, control rechts mit control_stretch=1 (siehe Falle zu stretch).

  2. Aufgaben — 4 QCheckBox mit Indikator 18×18 (borderrgba(separator,120), hover → accent, checked → bg accent); Fortschrittsbalken (12px, chunk accent), Zähler „Erledigt N von M“, Animation QProgressBar über QVariantAnimation (280ms); Taste HoverButton("Zurücksetzen").

  3. Eingabe — Felder:

    • Name, E-Mail — normale QLineEdit (34px, radius 8, hover Rahmen rgba(sep,140), focus → Rahmen accent + tint rgba(accent,8), placeholder text_secondary, selection accent+accent_fg).
    • Passwort — _build_password_field: trailing-Aktion „Auge“ — Symbol wechselt eye ↔ eye_slash, Klick schaltet EchoMode.Password ↔ Normal um.
    • Suche — _build_search_field: pill-radius 17, leading-Symbol Lupe, trailing-Symbol Kreuz, nur sichtbar bei nicht leerem Text (Klick — leeren).
    • Mehrzeilig — QTextEdit (radius 10, placeholder über palette.setColor(QPalette.ColorRole.PlaceholderText, ...) — QSS-Selektor QTextEdit[placeholderText] ist unzuverlässig) + Zeichenzähler rechts.
    • Feld-Symbole werden in _field_icon gezeichnet (18px: Lupe = Kreis+f-Stift; Kreuz = Kreis rgba(text_secondary,110) + „✕“ in Farbe theme.bg) oder aus SVG gerendert (_svg_icon): vor dem Rendern wird im SVG currentColor durch theme.text_secondary ersetzt (siehe Falle).
  4. Chat — siehe Abschnitt 8.

  5. Dateien — siehe Abschnitt 9.

  6. Einstellungen — Karte „Darstellung“ (Schalter „Dunkles Thema“ → _on_theme_toggled, Segment „Schnittstellengröße“), „Verhalten“ (Schalter Auto-Updates ✓, Animationen ✓), Kombobox „Sprache der Benutzeroberfläche“ (QSS: Feld surface+Rahmen, QAbstractItemView — selection-background-color: accent; selection-color: accent_fg), Schieberegler „Animationsgeschwindigkeit“=50.


8. Chat (Seite „Chat“)

  • Container — transparentes QScrollArea (rahmenlos; um weißen Hintergrund zu vermeiden: QSS background: transparent + viewport().setStyleSheet(...transparent) + inneres QWidget transparent).
  • Nachrichten gespeichert in `self._chat_messages: list[(text, m

[truncated for translation]

Individual skills in this repo

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

Verwandte Skills