CommunityProgramación y desarrollogithub.com

mawwki/pyqt6-macos-ui

Construye, rediseña o reestiliza una aplicación GUI de escritorio en PyQt6/PySide6 para que se vea y sienta como una aplicación pulida de macOS (ventana sin marco, semáforos, barra lateral, tarjetas con sombras suaves, microinteracciones animadas, temas claro/oscuro).

¿Qué es pyqt6-macos-ui?

Este skill proporciona una especificación completa para construir interfaces de escritorio con estilo macOS usando PyQt6/PySide6. Incluye una aplicación de referencia (`assets/app_template.py`) que sirve como plantilla y guía de estilo. El skill detalla la arquitectura de la ventana (sin marco, con sombra y esquinas redondeadas), el árbol de widgets (barra de título, barra lateral, panel de contenido con QStackedWidget), y los tokens de diseño precisos (colores, fuentes, radios, duraciones de animación). También cubre la implementación de widgets personalizados como botones con hover animado, interruptores (ToggleSwitch), sliders (MacSlider), controles segmentados, semáforos de ventana, y una barra lateral con indicador animado. El skill advierte sobre problemas comunes en PyQt6 en Windows (como la manipulación de Qt.Edge, el pintado de íconos SVG con reemplazo de color, y el renderizado de glifos con coordenadas enteras) y proporciona soluciones. Es útil para cualquier desarrollador que quiera crear aplicaciones de escritorio modernas y estilizadas con PyQt6, especialmente si desea evitar la apariencia 'genérica' de Qt.

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

Installed? Explore more Programación y desarrollo skills: steipete/bluebubbles, steipete/eightctl, steipete/blucli · View all 6 →

Preguntar en tu IA favorita

Abre un nuevo chat con esta habilidad de agente ya precargada.

Documentación

PyQt6 al estilo macOS – Especificación de la aplicación de referencia

A continuación se encuentra la especificación completa para reproducir assets/app_template.py al detalle. Modela a un agente diseñador de interfaces de un estudio que crea aplicaciones de escritorio con estilo macOS: paleta sobria, un color de acento, luz suave, animaciones justo donde explican lo que sucedió – y nada más.

La regla principal: «estilo macOS» en Qt no es una API nativa, sino una estética recreada manualmente: ventana sin marco + QSS propio + QPropertyAnimation para suavidad. Así la aplicación se ve igual en Windows, Linux y macOS. El estilo nativo de Qt no proporciona esquinas redondeadas, semitransparencias ni animaciones.

La implementación de referencia es assets/app_template.py (un solo archivo). La tarea del agente es: copiarla y adaptarla, o reconstruirla desde cero exactamente según este documento.


1. Inicio rápido

pip install PyQt6
python app_template.py

Compilación para el usuario: pyinstaller --windowed --icon=app.icns app_template.py.

La aplicación se ejecuta así:

def main() -> None:
    app = QApplication(sys.argv)
    app.setStyle("Fusion")                       # único estilo que respeta QSS en todas partes
    app.setFont(system_font(10, QFont.Weight.Normal))
    window = MainWindow()
    window.show()
    sys.exit(app.exec())

if __name__ == "__main__":
    main()

2. Estructura del archivo app_template.py (el orden es importante para la legibilidad)

  1. Importaciones: 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 (todos los widgets necesarios).
  2. Tokens de diseño: Theme (dataclass), LIGHT/DARK, constantes de radios/duraciones, REPLIES, SVG_EYE/SVG_EYE_SLASH, PAGES.
  3. Helpers: system_font, rgba, accent_fg, shadow_color.
  4. Widgets personalizados en orden de dependencias: HoverButton, ToggleSwitch, MacSlider, SegmentedControl, FileDropArea, TrafficLight, SidebarToggle, TitleBar, Sidebar.
  5. build_stylesheet(theme).
  6. MainWindow + main().

3. Tokens de diseño (valores exactos)

Theme – dataclass congelada con campos: bg, surface, sidebar, text, text_secondary, accent, separator_rgb (cadena #000000/#FFFFFF – base para 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)

Importante: el acento en esta aplicación no es azul. Es «invertido» – negro #1D1D1F en el tema claro y claro #CFCFD4 en el oscuro (esto proporciona el mismo contraste que System Blue, pero en una gama blanco y negro). Cualquier código que coloque texto sobre el acento debe usar accent_fg(theme), y no valores fijos.

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, máscaras de bits de bordes EDGE_TOP, EDGE_BOTTOM, EDGE_LEFT, EDGE_RIGHT = 1, 2, 4, 8.
  • DUR_FAST = 140 (hover/press), DUR_MED = 250 (páginas, barra lateral, interruptor, segmentos), EASE = QEasingCurve.Type.OutCubic.
  • PAGES = ["Resumen", "Tareas", "Entrada", "Chat", "Archivos", "Configuración"].
  • REPLIES – 5 respuestas cortas del bot.
  • SVG_EYE (Heroicons, viewBox 0 0 24 24, fill="currentColor") y SVG_EYE_SLASH (Bootstrap, viewBox 0 0 16 16) – cadenas multilínea.

Fuente: cadena de respaldo en system_font() – Inter → Segoe UI Variable Text → .AppleSystemUIFont → SF Pro Text → Helvetica Neue → Manrope → Segoe UI; más setStyleStrategy(QFont.StyleStrategy.PreferAntialias).

Márgenes de la escala 4/8/12/16/20/24/32. Sombras suaves: tarjetas – blurRadius 24, offset (0,5), color shadow_color(theme) (negro con shadow_alpha).


4. Esqueleto de la ventana

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)   # para redimensionar desde los bordes

Árbol (_build_ui): QMainWindow → root (QWidget) → panel (QFrame, objectName "Panel") → TitleBar (40px) + body (QHBoxLayout) → Sidebar (196px) + separator (1px) + QStackedWidget. El panel recibe build_stylesheet(theme) – QSS se hereda hacia abajo.

La sombra y el redondeo viven en el QFrame#Panel hijo, no en la ventana misma: la ventana es transparente y da un margen para la sombra. Esto es fundamental – no se puede aplicar el efecto a QMainWindow.

Arrastre y redimensionamiento

Mover la ventana – mecanismo nativo de Qt6, sin matemática manual de desplazamiento:

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

Cambiar el tamaño desde los bordes – también nativo QWindow.startSystemResize. En MainWindow se instala un filtro de eventos en QApplication, que:

  • en MouseButtonPress (Left) en una zona ≤ EDGE_MARGIN desde el borde de la ventana llama a _start_system_resize(edges) y devuelve True (el evento se consume);
  • en MouseMove sin botones actualiza el cursor de la ventana;
  • en Leave restablece el cursor.

_resize_edges_for(pos) convierte la posición global en una máscara de bits de bordes; solo funciona si la ventana es visible y no está maximizada. _cursor_for_edges(edges): centro → Arrow; izquierda/derecha → SizeHor; arriba/abajo → SizeVer; arriba+izquierda o abajo+derecha → SizeFDiag; de lo contrario → SizeBDiag.

Trampa de PyQt6: Qt.Edge no se puede convertir a int ni mezclar con int usando el operador | (TypeError). Por lo tanto, toda la matemática de bordes se basa en constantes de bits propias EDGE_*, y la bandera para startSystemResize se construye así:

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 personalizados (parámetros exactos)

HoverButton (botón con hover/press suave)

  • QPushButton, setFlat(True), altura 34, fuente 13/Medium, padding 0 18px, radius RADIUS_CONTROL.
  • Color de texto en QSS: color: {accent_fg(theme)} (¡no fijo «white»!).
  • Fondo – no QSS :hover (instantáneo), sino Q_PROPERTY alpha (float, 0..1): en paintEvent se pinta un QColor de acento con setAlphaF(alpha) + rectángulo redondeado, luego super().paintEvent() dibuja el texto. Eventos: enter → 0.85, leave → 1.0, press → 0.7, release → 0.85/1.0. Animación DUR_FAST, EASE.

ToggleSwitch (interruptor macOS)

  • QWidget 42×24, señal toggled(bool), manejador solo con el botón izquierdo.
  • Q_PROPERTY knobPos (float 0..1) se anima con DUR_MED/EASE.
  • Dibujo: pista redondeada (radius = h/2), color interpolado entre #B9B9BE (apagado) y accent (encendido); perilla – círculo QColor(self._theme.bg), diámetro h-4, posición x_min = 2, x_max = width - knob_d - 2.
  • Trampa: la perilla se pinta con theme.bg, no con blanco – en el tema oscuro una perilla blanca se fusiona con la pista blanca apagada.
  • setChecked programático solo anima, la señal NO se emite (se emiten en mousePressEvent).

MacSlider (slider macOS)

  • QWidget altura 28, Q_PROPERTY knobScale (1.0 → 1.12 hover, 0.94 press), DUR_FAST.
  • Valor a partir de x: ratio = (x - pad) / (width - 2*pad), pad = 9.5.
  • Dibujo: pista altura 6 (fondo separator con alpha 26, relleno – accent), círculo 9.5px: brillo (accent, alpha 22 hover / 34 press, radio +4), sombra (negro alpha 26), el círculo mismo theme.bg con borde separator alpha 90 (255 cuando pressed), penWidth 1.2.

SegmentedControl (segmentos «Compacto/Mediano/Grande»)

  • QWidget altura 30; botones altura 26, fuente 12, padding 0 16px, box spacing 4, margins 4.
  • «Píldora» – QFrame#SegIndicator (bg accent, radius 7) detrás de los botones en el mismo layout, se mueve con QPropertyAnimation(geometry) DUR_MED/EASE.
  • Color de texto: para el seleccionado accent_fg(theme), para los demás text_secondary.
  • En showEvent la píldora se coloca detrás (lower()) y se posiciona en la geometría del botón seleccionado.

TrafficLight (semáforo)

  • QPushButton 22×22, Q_PROPERTY hover (0..1) animación DUR_MED.
  • Sin bordes de colores amarillo/verde: cerrar – rojo #FF5F57 (alpha 200 + 35·hover), minimizar/zoom – neutral rgba(text_secondary, 60 + 40·hover).
  • Los glifos se dibujan con QPen con RoundCap, grosor 2, solo coordenadas enteras (ver trampas):
    • cerrar – cruz de dos líneas;
    • minimizar – chevron hacia abajo: (cx-5,cy-3)-(cx,cy+3)-(cx+5,cy-3);
    • maximizar (no expandido) – chevron hacia arriba: (cx-5,cy+3)-(cx,cy-3)-(cx+5,cy+3);
    • maximizar en estado expandido (_maximized) – 8 líneas de cuatro esquinas de corchetes.
  • Color del glifo: para cerrar – blanco alpha 205+30·hover; para los demás – interpolación text_secondary → text según hover.

SidebarToggle (botón de colapsar panel)

  • QPushButton 22×22, hover 0..1, hover-bg rgba(separator,26), glifo interpola text_secondary → text.
  • Glifo del panel abierto (Lucide panel-left-close): marco drawRoundedRect(QRectF(cx-7, cy-7, 14, 14), 2, 2), separador vertical en cx-2, chevron < desde (cx+4, cy-2)→(cx, cy)→(cx+4, cy+2).
  • Glifo del panel colapsado (Lucide panel-right): marco + separador en cx+2, sin chevron.
  • set_collapsed(bool) cambia el glifo; el estado lo sincroniza MainWindow.toggle_sidebar.

Sidebar (navegación)

  • QWidget setFixedWidth(SIDEBAR_WIDTH), margins 10,16,10,16, spacing 2.
  • Marca «MiProyecto» (13/Bold, padding: 0 8px 14px 8px).
  • Nombres de páginas – QPushButton#NavButton altura 30, fuente 13, checkable, inactivo – text_secondary, activo – text. Indicador – QFrame radius 8, bg rgba(accent,40), se anima según geometry hacia el botón seleccionado (DUR_MED).
  • Hacer clic en la página actual inactivo – no-op. En showEvent el indicador se coloca sin animación.

6. QSS general (build_stylesheet)

  • #Panel – bg del tema, radius de ventana. #TitleBar – transparente. #Sidebar – bg de la barra lateral.
  • #Indicator – rgba(accent, 40), radius 8.
  • QPushButton#NavButton – alineación a la izquierda, radius 8, padding-left 10; :checked – text; :hover – rgba(separator,26).
  • QPushButton#ToolButton – color secundario, radius 6, hover resaltado.
  • Barras de desplazamiento (se agregan globalmente, para que el chat y las listas no sean «gruesos»): vertical/horizontal 8px, handle radius 3 rgba(separator,60) (hover 120), min 30, flechas y páginas – transparentes.
  • Establecer setAttribute(Qt.WidgetAttribute.WA_StyledBackground, True) en cualquier widget con fondos QSS.

7. Páginas (6 en total)

Se componen mediante _page_shell(theme, title) (margins 28/24/28/24, spacing 16, título 22/Bold) y _add_card(theme, layout, title) (tarjeta + título 14/DemiBold + shadow).

  1. Resumen – tarjeta «Sistema» (interruptores: Inicio automático ✓, Notificaciones ✓, Ahorro de energía ✗) y «Pantalla» (sliders Brillo=70, Volumen=45). La línea del slider – etiqueta a la izquierda, control a la derecha con control_stretch=1 (ver trampa sobre stretch).

  2. Tareas – 4 QCheckBox con indicador 18×18 (borde rgba(separator,120), hover → accent, checked → bg accent); barra de progreso (12px, chunk accent), contador «Completadas N de M», animación QProgressBar mediante QVariantAnimation (280ms); botón HoverButton("Restablecer").

  3. Entrada – campos:

    • Nombre, Correo electrónico – QLineEdit normal (34px, radius 8, hover borde rgba(sep,140), focus → borde accent + tint rgba(accent,8), placeholder text_secondary, selection accent+accent_fg).
    • Contraseña – _build_password_field: acción final «ojo» – icono cambia eye ↔ eye_slash, clic cambia EchoMode.Password ↔ Normal.
    • Búsqueda – _build_search_field: pill-radius 17, icono inicial de lupa, icono final de cruz, visible solo cuando el texto no está vacío (clic – limpieza).
    • Multilínea – QTextEdit (radius 10, placeholder mediante palette.setColor(QPalette.ColorRole.PlaceholderText, ...) – el selector QSS QTextEdit[placeholderText] no es fiable) + contador de caracteres a la derecha.
    • Los iconos de los campos se dibujan en _field_icon (18px: lupa = círculo+mango de f; cruz = círculo rgba(text_secondary,110) + «✕» de color theme.bg) o se renderizan desde SVG (_svg_icon): antes de renderizar en SVG se reemplaza currentColor por theme.text_secondary (ver trampa).
  4. Chat – ver sección 8.

  5. Archivos – ver sección 9.

  6. Configuración – tarjeta «Apariencia» (interruptor «Tema oscuro» → _on_theme_toggled, segmento «Tamaño de interfaz»), «Comportamiento» (interruptores Actualizaciones automáticas ✓, Animaciones ✓), combobox «Idioma de interfaz» (QSS: campo surface+borde, QAbstractItemView – selection-background-color: accent; selection-color: accent_fg), slider «Velocidad de animaciones»=50.


8. Chat (página «Chat»)

  • Contenedor – QScrollArea transparente (sin marco; para evitar fondo blanco: QSS background: transparent + viewport().setStyleSheet(...transparent) + QWidget interno transparente).
  • Los mensajes se almacenan en `self._chat_messages: list[(text, m

…(truncado para la traducción)

Individual skills in this repo

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

Skills relacionados