Community코딩 & 개발github.com

mawwki/pyqt6-macos-ui

PyQt6/PySide6 데스크탑 GUI 앱을 macOS 스타일로 디자인, 재구축 또는 스타일링합니다 (프레임리스 창, 신호등, 사이드바, 부드러운 그림자가 있는 카드, 애니메이션, 라이트/다크 테마).

pyqt6-macos-ui란 무엇인가요?

이 스킬은 PyQt6 또는 PySide6로 데스크탑 애플리케이션을 제작할 때, 기본 Qt 위젯의 딱딱한 UI를 피하고 macOS와 같은 세련된 외관을 구현하고자 할 때 사용됩니다. 'frameless' 창, macOS 특유의 '신호등' 버튼, 사이드바 탐색, 부드러운 애니메이션, 라이트/다크 테마 전환 등을 지원합니다. `app_template.py` 파일을 참조 구현으로 하여, 디자인 토큰(색상, 간격, 그림자, 애니메이션 지속 시간)부터 위젯별 동작과 QSS 스타일시트까지 완전한 명세를 제공합니다. 특히 PyQt6에서 Windows 환경에서 발생할 수 있는 크래시 문제와 Qt.Edge 타입 관련 함정을 경고하며, macOS 스타일을 재현하기 위한 QPropertyAnimation 기반의 커스텀 위젯(ToggleSwitch, MacSlider, SegmentedControl 등) 구현법을 상세히 설명합니다. Cursor, Codex, Claude Code 등 모든 에이전트에서 사용할 수 있는 독립적인 SKILL.md 문서로 구성되어 있습니다.

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

Installed? Explore more 코딩 & 개발 skills: steipete/bluebubbles, steipete/eightctl, steipete/blucli · View all 6 →

즐겨 사용하는 AI에게 물어보기

이 에이전트 스킬이 미리 로드된 새 채팅을 엽니다.

문서

PyQt6 macOS 스타일 — 레퍼런스 앱 명세

아래는 assets/app_template.py를 동작별로 바이트 단위로 재현할 수 있는 완전한 명세입니다. macOS 스타일 데스크탑 애플리케이션을 만드는 스튜디오의 UI 디자이너 에이전트를 모델링하세요. 절제된 팔레트, 하나의 강조 색상, 부드러운 조명, 그리고 동작을 설명하는 데 필요한 정확한 애니메이션만 사용합니다.

핵심 규칙: Qt에서의 'macOS 스타일'은 네이티브 API가 아니라 수제로 재현한 미학입니다: 프레임리스 창 + 자체 QSS + 부드러움을 위한 QPropertyAnimation. 이렇게 하면 Windows, Linux, macOS에서 동일하게 보입니다. Native Qt 스타일은 라운드 처리, 반투명도, 애니메이션을 제공하지 않습니다.

참조 구현은 assets/app_template.py (단일 파일)입니다. 에이전트의 작업은 이 파일을 복사하여 적용하거나, 이 문서를 기반으로 처음부터 다시 조립하는 것입니다.


1. 빠른 시작

pip install PyQt6
python app_template.py

사용자용 빌드: pyinstaller --windowed --icon=app.icns app_template.py.

앱 실행:

def main() -> None:
    app = QApplication(sys.argv)
    app.setStyle("Fusion")                       # QSS를 모든 곳에서 존중하는 유일한 스타일
    app.setFont(system_font(10, QFont.Weight.Normal))
    window = MainWindow()
    window.show()
    sys.exit(app.exec())

if __name__ == "__main__":
    main()

2. app_template.py 파일 구조 (가독성을 위해 순서가 중요)

  1. 임포트: 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 (필요한 모든 위젯).
  2. 디자인 토큰: Theme (dataclass), LIGHT/DARK, 반경/지속 시간 상수, REPLIES, SVG_EYE/SVG_EYE_SLASH, PAGES.
  3. 헬퍼: system_font, rgba, accent_fg, shadow_color.
  4. 종속성 순서의 커스텀 위젯: HoverButton, ToggleSwitch, MacSlider, SegmentedControl, FileDropArea, TrafficLight, SidebarToggle, TitleBar, Sidebar.
  5. build_stylesheet(theme).
  6. MainWindow + main().

3. 디자인 토큰 (정확한 값)

Theme — 다음 필드를 가진 frozen dataclass: bg, surface, sidebar, text, text_secondary, accent, separator_rgb (문자열 #000000/#FFFFFF — 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)

중요: 이 앱에서 강조 색상은 파란색이 아닙니다. '반전'되어 있습니다 — 라이트 테마에서는 검은색 #1D1D1F, 다크 테마에서는 밝은 색 #CFCFD4입니다 (시스템 블루와 동일한 대비를 제공하지만 흑백 계열입니다). 강조 색상 위에 텍스트를 배치하는 모든 코드는 하드코딩 대신 accent_fg(theme)를 사용해야 합니다.

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

상수:

  • RADIUS_WINDOW = 14, RADIUS_CARD = 12, RADIUS_CONTROL = 8, SIDEBAR_WIDTH = 196.
  • EDGE_MARGIN = 6, 가장자리 비트마스크 EDGE_TOP, EDGE_BOTTOM, EDGE_LEFT, EDGE_RIGHT = 1, 2, 4, 8.
  • DUR_FAST = 140 (호버/프레스), DUR_MED = 250 (페이지, 사이드바, 토글, 세그먼트), EASE = QEasingCurve.Type.OutCubic.
  • PAGES = ["Обзор", "Задачи", "Ввод", "Чат", "Файлы", "Настройки"].
  • REPLIES — 봇의 짧은 응답 5개.
  • SVG_EYE (Heroicons, viewBox 0 0 24 24, fill="currentColor") 및 SVG_EYE_SLASH (Bootstrap, viewBox 0 0 16 16) — 여러 줄 문자열.

글꼴: system_font()의 폴백 체인 — Inter → Segoe UI Variable Text → .AppleSystemUIFont → SF Pro Text → Helvetica Neue → Manrope → Segoe UI; 추가로 setStyleStrategy(QFont.StyleStrategy.PreferAntialias).

여백은 4/8/12/16/20/24/32 스케일을 따릅니다. 그림자는 부드럽습니다: 카드 — blurRadius 24, offset (0,5), 색상 shadow_color(theme) (검은색에 shadow_alpha 적용).


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) → panel (QFrame, objectName "Panel") → TitleBar (40px) + body (QHBoxLayout) → Sidebar (196px) + separator (1px) + QStackedWidget. 패널은 build_stylesheet(theme)을 적용받습니다 — QSS는 하위로 상속됩니다.

그림자와 라운드 처리는 창 자체가 아닌 자식 QFrame#Panel에 존재합니다: 창은 투명하며 그림자를 위한 여백을 제공합니다. QMainWindow에 효과를 적용하는 것은 근본적으로 옳지 않습니다.

드래그 및 리사이즈

창 이동 — Qt6의 네이티브 메커니즘, 수동 수학 없음:

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

가장자리 크기 변경 — 역시 네이티브 QWindow.startSystemResize. MainWindow에는 QApplication에 이벤트 필터가 설치되어 있으며:

  • 창 가장자리로부터 ≤ EDGE_MARGIN 영역에서 MouseButtonPress (Left) 시 _start_system_resize(edges)를 호출하고 True를 반환합니다 (이벤트 소비);
  • 버튼 없는 MouseMove 시 창 커서를 업데이트합니다;
  • Leave 시 커서를 재설정합니다.

_resize_edges_for(pos)는 전역 위치를 가장자리 비트마스크로 변환합니다; 창이 보이고 최대화되지 않은 경우에만 작동합니다. _cursor_for_edges(edges): 중앙 → Arrow; 왼쪽/오른쪽 → SizeHor; 위/아래 → SizeVer; 위+왼쪽 또는 아래+오른쪽 → SizeFDiag; 그 외 → SizeBDiag.

PyQt6 함정: Qt.Edge는 int로 변환할 수 없으며 int와 | 연산자로 혼합할 수 없습니다 (TypeError). 따라서 모든 가장자리 수학은 자체 비트 상수 EDGE_*를 사용하고, startSystemResize 플래그는 다음과 같이 구성됩니다:

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. 커스텀 위젯 (정확한 매개변수)

HoverButton (부드러운 호버/프레스가 있는 버튼)

  • QPushButton, setFlat(True), 높이 34, 글꼴 13/Medium, padding 0 18px, radius RADIUS_CONTROL.
  • QSS의 텍스트 색상: color: {accent_fg(theme)} (하드코딩 'white' 아님!).
  • 배경 — QSS :hover (즉각적)가 아닌 Q_PROPERTY alpha (float, 0..1): paintEvent에서 강조 QColor를 setAlphaF(alpha)로 칠하고 라운드 사각형을 그린 후 super().paintEvent()로 텍스트를 그립니다. 이벤트: enter → 0.85, leave → 1.0, press → 0.7, release → 0.85/1.0. 애니메이션 DUR_FAST, EASE.

ToggleSwitch (macOS 토글 스위치)

  • QWidget 42×24, 시그널 toggled(bool), 처리기는 왼쪽 버튼만.
  • Q_PROPERTY knobPos (float 0..1)가 DUR_MED/EASE로 애니메이션.
  • 그리기: 트랙 라운드 (radius = h/2), 색상은 #B9B9BE (꺼짐)과 accent (켜짐) 사이; 손잡이는 원형 QColor(self._theme.bg), 지름 h-4, 위치 x_min = 2, x_max = width - knob_d - 2.
  • 함정: 손잡이는 흰색이 아닌 theme.bg로 칠해짐 — 다크 테마에서는 흰색 손잡이가 흰색 꺼짐 트랙과 혼동됨.
  • 프로그래밍 방식 setChecked는 애니메이션만 하고 시그널을 방출하지 않음 (mousePressEvent에서 방출).

MacSlider (macOS 슬라이더)

  • QWidget 높이 28, Q_PROPERTY knobScale (1.0 → 1.12 호버, 0.94 프레스), DUR_FAST.
  • x에서 값: ratio = (x - pad) / (width - 2*pad), pad = 9.5.
  • 그리기: 트랙 높이 6 (배경 separator alpha 26, 채움 — accent), 원 9.5px: 글로우 (accent, alpha 22 호버 / 34 프레스, radius +4), 그림자 (검은색 alpha 26), 원 자체 theme.bg에 테두리 separator alpha 90 (프레스 시 255), penWidth 1.2.

SegmentedControl ('컴팩트/중간/라지' 세그먼트)

  • QWidget 높이 30; 버튼 높이 26, 글꼴 12, padding 0 16px, box spacing 4, margins 4.
  • '알약' — QFrame#SegIndicator (bg accent, radius 7)가 동일한 레이아웃의 버튼 뒤에 위치, QPropertyAnimation(geometry) DUR_MED/EASE로 이동.
  • 텍스트 색상: 선택된 것 accent_fg(theme), 나머지 text_secondary.
  • showEvent에서 알약을 아래로 내리고 (lower()) 선택된 버튼의 지오메트리로 설정.

TrafficLight (신호등)

  • QPushButton 22×22, Q_PROPERTY hover (0..1) 애니메이션 DUR_MED.
  • 노란색/녹색 테두리 없음: 닫기 — 빨간색 #FF5F57 (alpha 200 + 35·hover), 최소화/줌 — 중성 rgba(text_secondary, 60 + 40·hover).
  • 글리프는 QPen으로 RoundCap, 너비 2로 그려지며, 정수 좌표만 사용 (함정 참조):
    • 닫기 — 두 선으로 만든 엑스;
    • 최소화 — 아래쪽 쉐브론: (cx-5,cy-3)-(cx,cy+3)-(cx+5,cy-3);
    • 최대화 (확장되지 않음) — 위쪽 쉐브론: (cx-5,cy+3)-(cx,cy-3)-(cx+5,cy+3);
    • 최대화 상태 (_maximized) — 4개의 모서리 괄호를 만드는 8개의 선.
  • 글리프 색상: 닫기용 — 흰색 alpha 205+30·hover; 나머지 — text_secondary → text를 hover로 보간.

SidebarToggle (사이드바 접기 버튼)

  • QPushButton 22×22, hover 0..1, hover-bg rgba(separator,26), 글리프는 text_secondary → text로 보간.
  • 열린 패널 글리프 (Lucide panel-left-close): 프레임 drawRoundedRect(QRectF(cx-7, cy-7, 14, 14), 2, 2), cx-2에 수직 구분선, 쉐브론 < (cx+4, cy-2)→(cx, cy)→(cx+4, cy+2).
  • 접힌 글리프 (Lucide panel-right): 프레임 + cx+2에 구분선, 쉐브론 없음.
  • set_collapsed(bool)이 글리프를 전환; 상태는 MainWindow.toggle_sidebar와 동기화.

Sidebar (내비게이션)

  • QWidget setFixedWidth(SIDEBAR_WIDTH), margins 10,16,10,16, spacing 2.
  • 브랜드 'МойПроект' (13/Bold, padding: 0 8px 14px 8px).
  • 페이지 이름 — QPushButton#NavButton 높이 30, 글꼴 13, checkable, 비활성 — text_secondary, 활성 — text. 표시기 — QFrame radius 8, bg rgba(accent,40), geometry로 선택된 버튼으로 애니메이션 (DUR_MED).
  • 현재 페이지를 비활성 클릭 — 아무 작업 없음. showEvent에서 표시기는 애니메이션 없이 설정됨.

6. 공통 QSS (build_stylesheet)

  • #Panel — 테마 bg, 창 radius. #TitleBar — 투명. #Sidebar — 사이드바 bg.
  • #Indicator — rgba(accent, 40), radius 8.
  • QPushButton#NavButton — 왼쪽 정렬, radius 8, padding-left 10; :checked — text; :hover — rgba(separator,26).
  • QPushButton#ToolButton — 보조 색상, radius 6, hover 강조.
  • 스크롤바 (전역으로 추가되어 채팅 및 목록이 '두껍지' 않도록 함): 세로/가로 8px, handle radius 3 rgba(separator,60) (hover 120), min 30, 화살표 및 페이지 영역 투명.
  • QSS 배경이 있는 모든 위젯에 setAttribute(Qt.WidgetAttribute.WA_StyledBackground, True) 설정.

7. 페이지 (6개)

_page_shell(theme, title) (margins 28/24/28/24, spacing 16, 제목 22/Bold) 및 _add_card(theme, layout, title) (카드 + 제목 14/DemiBold + 그림자)를 통해 구성.

  1. Обзор — 카드 'Система' (토글: Автозапуск ✓, Уведомления ✓, Энергосбережение ✗) 및 'Отображение' (슬라이더 Яркость=70, Громкость=45). 슬라이더 행 — 왼쪽 레이블, 오른쪽 컨트롤 control_stretch=1 (stretch 함정 참조).

  2. Задачи — 4 QCheckBox (표시기 18×18, borderrgba(separator,120), hover → accent, checked → bg accent); 진행률 표시줄 (12px, chunk accent), 카운터 'Выполнено N из M', QProgressBar 애니메이션은 QVariantAnimation (280ms); HoverButton("Сбросить") 버튼.

  3. Ввод — 필드:

    • Имя, E-mail — 일반 QLineEdit (34px, radius 8, hover 테두리 rgba(sep,140), focus → 테두리 accent + tint rgba(accent,8), placeholder text_secondary, selection accent+accent_fg).
    • Пароль — _build_password_field: 후행 액션 'глаз' — 아이콘 변경 eye ↔ eye_slash, 클릭 시 EchoMode.Password ↔ Normal 전환.
    • Поиск — _build_search_field: pill-radius 17, 선행 돋보기 아이콘, 후행 X 아이콘, 비어 있지 않은 텍스트에서만 표시 (클릭 시 지우기).
    • Многострочный — QTextEdit (radius 10, placeholder는 palette.setColor(QPalette.ColorRole.PlaceholderText, ...)를 통해 — QSS 선택자 QTextEdit[placeholderText]는 신뢰할 수 없음) + 오른쪽 문자 카운터.
    • 필드 아이콘은 _field_icon (18px: 돋보기 = 원+f-핸들; X = 원 rgba(text_secondary,110) + '✕' 색상 theme.bg)으로 그리거나 SVG (_svg_icon)로 렌더링: 렌더링 전 SVG에서 currentColor를 theme.text_secondary로 대체 (함정 참조).
  4. Чат — 섹션 8 참조.

  5. Файлы — 섹션 9 참조.

  6. Настройки — 카드 'Внешний вид' (토글 'Тёмная тема' → _on_theme_toggled, 세그먼트 'Размер интерфейса'), 'Поведение' (토글 Автообновления ✓, Анимации ✓), 콤보박스 'Язык интерфейса' (QSS: 필드 surface+테두리, QAbstractItemView — selection-background-color: accent; selection-color: accent_fg), 슬라이더 'Скорость анимаций'=50.


8. Чат (페이지 'Чат')

  • 컨테이너 — 투명한 QScrollArea (프레임리스; 흰 배경 방지: QSS background: transparent + viewport().setStyleSheet(...transparent) + 내부 QWidget 투명).
  • 메시지는 `self._chat_messages: list[(text, m

[발췌됨]

Individual skills in this repo

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

관련 스킬