mawwki/pyqt6-macos-ui

Construa, redesenhe ou reformate uma GUI desktop em PyQt6/PySide6 para parecer e se comportar como um aplicativo macOS polido (janela sem moldura, botões de tráfego, barra lateral, cartões com sombras suaves, micro-interações animadas, temas claro/escuro).

O que é pyqt6-macos-ui?

Este skill capacita um agente de IA a gerar interfaces gráficas de desktop com a estética visual do macOS usando PyQt6/PySide6, sem depender de APIs nativas da Apple. Ele define uma especificação completa de aplicativo de referência (`app_template.py`) que cobre desde tokens de design (cores, tipografia, raios, durações de animação) até widgets personalizados (ToggleSwitch, MacSlider, SegmentedControl, TrafficLight, Sidebar) e lógica de interação (hover suave, arrastar para redimensionar, sidebar colapsável). O principal objetivo é evitar a aparência padrão do Qt, substituindo-a por uma aparência moderna com janela sem moldura (FramelessWindowHint), fundo translúcido, sombras suaves e animações via QPropertyAnimation. O skill também documenta armadilhas críticas do PyQt6 no Windows (como a manipulação de flags `Qt.Edge` com inteiros) e fornece um schema de build (QSS) que unifica a estilização de todos os widgets. Suporta as plataformas Windows, Linux e macOS (com fallback de fontes para cada uma).

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

Installed? Explore more Programação e Desenvolvimento skills: steipete/bluebubbles, steipete/eightctl, steipete/blucli · View all 6 →

Perguntar na sua IA favorita

Abre um novo chat com esta habilidade de agente já pré-carregada.

Documentação

PyQt6 em estilo macOS — especificação da aplicação de referência

Abaixo está a especificação completa para reproduzir assets/app_template.py byte a byte em termos de comportamento. Modele um agente designer de interfaces de um estúdio que cria aplicativos desktop no estilo macOS: paleta contida, uma cor de destaque, iluminação suave, animações exatamente onde elas explicam o que aconteceu — e nada mais.

Regra principal: "estilo macOS" no Qt não é uma API nativa, mas uma estética recriada manualmente: janela sem moldura + QSS personalizado + QPropertyAnimation para suavidade. Assim, o aplicativo se parece igual no Windows, Linux e macOS. O estilo nativo do Qt não fornece cantos arredondados, transparência nem animações.

A implementação de referência é assets/app_template.py (um arquivo). A tarefa do agente: copiá-lo e adaptá-lo, ou recriá-lo do zero exatamente de acordo com este documento.


1. Início rápido

pip install PyQt6
python app_template.py

Compilação para o usuário: pyinstaller --windowed --icon=app.icns app_template.py.

O aplicativo é iniciado assim:

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

if __name__ == "__main__":
    main()

2. Estrutura do arquivo app_template.py (ordem importante para legibilidade)

  1. Importações: 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 os widgets necessários).
  2. Tokens de design: Theme (dataclass), LIGHT/DARK, constantes de raios/durações, REPLIES, SVG_EYE/SVG_EYE_SLASH, PAGES.
  3. Helpers: system_font, rgba, accent_fg, shadow_color.
  4. Widgets personalizados na ordem de dependências: HoverButton, ToggleSwitch, MacSlider, SegmentedControl, FileDropArea, TrafficLight, SidebarToggle, TitleBar, Sidebar.
  5. build_stylesheet(theme).
  6. MainWindow + main().

3. Tokens de design (valores exatos)

Theme é uma dataclass congelada com campos: bg, surface, sidebar, text, text_secondary, accent, separator_rgb (string #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: o destaque neste aplicativo não é azul. Ele é "invertido" — preto #1D1D1F no tema claro e claro #CFCFD4 no escuro (isso fornece o mesmo contraste que System Blue, mas em uma escala de cinza). Qualquer código que coloque texto sobre o destaque deve usar accent_fg(theme), e não um valor fixo.

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 das bordas EDGE_TOP, EDGE_BOTTOM, EDGE_LEFT, EDGE_RIGHT = 1, 2, 4, 8.
  • DUR_FAST = 140 (hover/press), DUR_MED = 250 (páginas, sidebar, toggle, segmentos), EASE = QEasingCurve.Type.OutCubic.
  • PAGES = ["Visão Geral", "Tarefas", "Entrada", "Chat", "Arquivos", "Configurações"].
  • REPLIES — 5 respostas curtas do bot.
  • SVG_EYE (Heroicons, viewBox 0 0 24 24, fill="currentColor") e SVG_EYE_SLASH (Bootstrap, viewBox 0 0 16 16) — strings multilinha.

Fonte: cadeia de fallbacks em system_font() — Inter → Segoe UI Variable Text → .AppleSystemUIFont → SF Pro Text → Helvetica Neue → Manrope → Segoe UI; além de setStyleStrategy(QFont.StyleStrategy.PreferAntialias).

Margens da escala 4/8/12/16/20/24/32. Sombras suaves: cartões — blurRadius 24, offset (0,5), cor shadow_color(theme) (preto com shadow_alpha).


4. Estrutura da janela

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 pelas bordas

Árvore (_build_ui): QMainWindow → root (QWidget) → panel (QFrame, objectName "Panel") → TitleBar (40px) + body (QHBoxLayout) → Sidebar (196px) + separator (1px) + QStackedWidget. O painel recebe build_stylesheet(theme) — QSS é herdado para baixo.

Sombra e cantos arredondados ficam no filho QFrame#Panel, e não na janela: a janela é transparente e dá espaço para a sombra. Isso é essencial — não se pode aplicar o efeito no QMainWindow.

Arrastar e redimensionar

Mover a janela — mecanismo nativo do Qt6, sem matemática manual de deslocamento:

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

Redimensionar pelas bordas — também nativo QWindow.startSystemResize. No MainWindow está instalado um filtro de eventos no QApplication, que:

  • no MouseButtonPress (Left) em zona ≤ EDGE_MARGIN da borda da janela chama _start_system_resize(edges) e retorna True (evento é consumido);
  • no MouseMove sem botões atualiza o cursor da janela;
  • no Leave reseta o cursor.

_resize_edges_for(pos) converte a posição global em uma máscara de bits das bordas; funciona apenas se a janela estiver visível e não maximizada. _cursor_for_edges(edges): centro → Arrow; esquerda/direita → SizeHor; cima/baixo → SizeVer; cima+esquerda ou baixo+direita → SizeFDiag; senão → SizeBDiag.

Armadilha do PyQt6: Qt.Edge não pode ser convertido em int nem misturado com int usando o operador | (TypeError). Portanto, toda a matemática de bordas é feita com constantes de bit EDGE_*, e a flag para startSystemResize é montada assim:

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 exatos)

HoverButton (botão com hover/press suave)

  • QPushButton, setFlat(True), altura 34, fonte 13/Medium, padding 0 18px, radius RADIUS_CONTROL.
  • Texto no QSS: color: {accent_fg(theme)} (não fixo "white"!).
  • Fundo — não é QSS :hover (instantâneo), mas Q_PROPERTY alpha (float, 0..1): no paintEvent pinta-se um QColor de destaque com setAlphaF(alpha) + retângulo arredondado, depois super().paintEvent() desenha o texto. Eventos: enter → 0.85, leave → 1.0, press → 0.7, release → 0.85/1.0. Animação DUR_FAST, EASE.

ToggleSwitch (alternador macOS)

  • QWidget 42×24, sinal toggled(bool), manipulador apenas com botão esquerdo.
  • Q_PROPERTY knobPos (float 0..1) animado com DUR_MED/EASE.
  • Desenho: trilha arredondada (radius = h/2), cor interpolada entre #B9B9BE (desligado) e accent (ligado); botão circular. QColor(self._theme.bg), diâmetro h-4, posição x_min = 2, x_max = width - knob_d - 2.
  • Armadilha: o botão é pintado com theme.bg, e não com branco — no tema escuro, o botão branco se mistura com a trilha branca desligada.
  • setChecked programático apenas anima, NÃO emite sinal (os sinais são emitidos no mousePressEvent).

MacSlider (slider macOS)

  • QWidget altura 28, Q_PROPERTY knobScale (1.0 → 1.12 hover, 0.94 press), DUR_FAST.
  • Valor de x: ratio = (x - pad) / (width - 2*pad), pad = 9.5.
  • Desenho: trilha altura 6 (fundo separator com alpha 26, preenchimento — accent), círculo 9.5px: glow (accent, alpha 22 hover / 34 press, radius +4), sombra (preto alpha 26), círculo em theme.bg com borda separator alpha 90 (255 quando pressionado), penWidth 1.2.

SegmentedControl (segmentos "Compacto/Médio/Grande")

  • QWidget altura 30; botões altura 26, fonte 12, padding 0 16px, box spacing 4, margins 4.
  • "Pílula" — QFrame#SegIndicator (bg accent, radius 7) atrás dos botões no mesmo layout, movida com QPropertyAnimation(geometry) DUR_MED/EASE.
  • Cor do texto: no selecionado accent_fg(theme), nos outros text_secondary.
  • No showEvent, abaixar a pílula (lower()) e definir para a geometria do botão selecionado.

TrafficLight (semáforo)

  • QPushButton 22×22, Q_PROPERTY hover (0..1) animação DUR_MED.
  • Sem bordas amarelo/verde: fechar — vermelho #FF5F57 (alpha 200 + 35·hover), minimizar/zoom — neutro rgba(text_secondary, 60 + 40·hover).
  • Glifos desenhados com QPen com RoundCap, largura 2, apenas coordenadas inteiras (veja armadilhas):
    • fechar — 'X' de duas linhas;
    • minimizar — seta para baixo: (cx-5,cy-3)-(cx,cy+3)-(cx+5,cy-3);
    • maximizar (não expandido) — seta para cima: (cx-5,cy+3)-(cx,cy-3)-(cx+5,cy+3);
    • maximizar no estado expandido (_maximized) — 8 linhas de quatro cantos de colchetes.
  • Cor do glifo: para fechar — branco alpha 205+30·hover; para os outros — interpolação text_secondary → text de acordo com hover.

SidebarToggle (botão de recolher painel)

  • QPushButton 22×22, hover 0..1, hover-bg rgba(separator,26), glifo interpola text_secondary → text.
  • Glifo do painel aberto (Lucide panel-left-close): moldura drawRoundedRect(QRectF(cx-7, cy-7, 14, 14), 2, 2), separador vertical em cx-2, seta < de (cx+4, cy-2)→(cx, cy)→(cx+4, cy+2).
  • Glifo recolhido (Lucide panel-right): moldura + separador em cx+2, sem seta.
  • set_collapsed(bool) alterna o glifo; estado sincronizado com MainWindow.toggle_sidebar.

Sidebar (navegação)

  • QWidget setFixedWidth(SIDEBAR_WIDTH), margins 10,16,10,16, spacing 2.
  • Marca "MeuProjeto" (13/Bold, padding: 0 8px 14px 8px).
  • Nomes das páginas — QPushButton#NavButton altura 30, fonte 13, checkable, inativo — text_secondary, ativo — text. Indicador — QFrame radius 8, bg rgba(accent,40), animado por geometry até o botão selecionado (DUR_MED).
  • Clique inativo na página atual — no-op. No showEvent o indicador é posicionado sem animação.

6. QSS geral (build_stylesheet)

  • #Panel — bg do tema, radius da janela. #TitleBar — transparente. #Sidebar — bg da sidebar.
  • #Indicator — rgba(accent, 40), radius 8.
  • QPushButton#NavButton — alinhamento à esquerda, radius 8, padding-left 10; :checked — text; :hover — rgba(separator,26).
  • QPushButton#ToolButton — cor secundária, radius 6, destaque hover.
  • Scrollbars (adicionadas globalmente para chat e listas não serem "grossos"): vertical/horizontal 8px, handle radius 3 rgba(separator,60) (hover 120), min 30, setas e páginas — transparentes.
  • Definir setAttribute(Qt.WidgetAttribute.WA_StyledBackground, True) em qualquer widget com fundos QSS.

7. Páginas (6 no total)

Montadas via _page_shell(theme, title) (margins 28/24/28/24, spacing 16, título 22/Bold) e _add_card(theme, layout, title) (cartão + título 14/DemiBold + sombra).

  1. Visão Geral — cartão "Sistema" (toggles: Inicialização Automática ✓, Notificações ✓, Economia de Energia ✗) e "Exibição" (sliders Brilho=70, Volume=45). Linha do slider — label à esquerda, controle à direita com control_stretch=1 (veja armadilha sobre stretch).

  2. Tarefas — 4 QCheckBox com indicador 18×18 (border rgba(separator,120), hover → accent, checked → bg accent); barra de progresso (12px, chunk accent), contador "Concluído N de M", animação QProgressBar via QVariantAnimation (280ms); botão HoverButton("Redefinir").

  3. Entrada — campos:

    • Nome, E-mail — QLineEdit comum (34px, radius 8, hover borda rgba(sep,140), focus → borda accent + tint rgba(accent,8), placeholder text_secondary, selection accent+accent_fg).
    • Senha — _build_password_field: ação trailing "olho" — ícone muda eye ↔ eye_slash, clique alterna EchoMode.Password ↔ Normal.
    • Pesquisa — _build_search_field: pill-radius 17, ícone leading de lupa, ícone trailing de 'X', visível apenas quando o texto não está vazio (clique — limpa).
    • Multilinha — QTextEdit (radius 10, placeholder via palette.setColor(QPalette.ColorRole.PlaceholderText, ...) — seletor QSS QTextEdit[placeholderText] não é confiável) + contador de caracteres à direita.
    • Ícones dos campos desenhados em _field_icon (18px: lupa = círculo + alça; 'X' = círculo rgba(text_secondary,110) + "✕" cor theme.bg) ou renderizados a partir de SVG (_svg_icon): antes de renderizar, substitui-se no SVG currentColor por theme.text_secondary (veja armadilha).
  4. Chat — veja seção 8.

  5. Arquivos — veja seção 9.

  6. Configurações — cartão "Aparência" (toggle "Tema Escuro" → _on_theme_toggled, segmento "Tamanho da Interface"), "Comportamento" (toggles Atualizações Automáticas ✓, Animações ✓), combobox "Idioma da Interface" (QSS: campo surface+borda, QAbstractItemView — selection-background-color: accent; selection-color: accent_fg), slider "Velocidade das Animações"=50.


8. Chat (página "Chat")

  • Container — QScrollArea transparente (sem moldura; para evitar fundo branco: QSS background: transparent + viewport().setStyleSheet(...transparent) + widget interno QWidget transparente).
  • Mensagens armazenadas em `self._chat_messages: list[(text, m) ... [truncado para tradução]

Individual skills in this repo

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

Habilidades Relacionadas