PyQt6 マテリアルデザイン 3 スタイル — リファレンスアプリケーション仕様
以下は、assets/app_template.py の動作をピクセル単位で再現するための完全な仕様です。MD3 デスクトップアプリケーションを制作する UI デザインスタジオのエージェントのように振る舞ってください。役割パレット(primary / on-primary / containers / surface / outline)、ステートレイヤー、リップル、フォームスケールによる角丸、アクションの発生をユーザーに伝えるための必要最小限のアニメーションを備えています。
最も重要なルール: Qt における「MD3 スタイル」とは、ネイティブ API ではなく、自力で再現した美学です。フレームレスウィンドウ + カスタム QSS + 滑らかさのための QPropertyAnimation で構成されます。これにより、アプリケーションは Windows、Linux、macOS で同じように見えます。ネイティブの Qt スタイルでは、角丸、半透明、ステートレイヤー、アニメーションを実現できません。
リファレンス実装は assets/app_template.py (単一ファイル)です。エージェントのタスクは、このファイルをコピーしてアダプトするか、このドキュメントに従ってゼロから再構築することです。
1. クイックスタート
pip install PyQt6
python app_template.py
ユーザー向けビルド: pyinstaller --windowed --icon=app.ico app_template.py。
アプリケーションは次のように起動します:
def main() -> None:
app = QApplication(sys.argv)
app.setStyle("Fusion") # QSS を全体で尊重する唯一のスタイル
app.setFont(system_font(14, QFont.Weight.Normal))
window = MainWindow()
window.show()
sys.exit(app.exec())
if __name__ == "__main__":
main()
2. app_template.py のファイル構成(読みやすさのための順序)
- インポート:
math,sys,time,dataclasses,pathlib.Path; QtCore (Qt,QEvent,QPropertyAnimation,QEasingCurve,QRect(F),QPointF,QParallelAnimationGroup,QVariantAnimation,QTimer,pyqtProperty,pyqtSignal); QtGui (QColor,QFont,QPainter,QPainterPath,QFontDatabase,QFontMetricsF,QPen,QPixmap,QIcon,QPalette); QtSvg (QSvgRenderer); QtWidgets(必要なすべてのウィジェット)。 - デザイントークン:
Theme(dataclass),LIGHT/DARK, 半径/持続時間の定数,REPLIES,SVG_EYE/SVG_EYE_SLASH,PAGES。 - ヘルパー:
system_font,rgba,accent_fg,shadow_color,lerp_color,animate_property,draw_checkmark。 - 依存関係の順序に従ったカスタムウィジェット:
MdButton,MdSwitch,MdSlider,MdCheckBox,MdSegmentButton/MdSegmentedControl,DropGlyph/FileDropArea,WindowButton,SidebarToggle,TopAppBar,NavButton/NavigationDrawer。 build_stylesheet(theme)。MainWindow+main()。
3. デザイントークン(正確な値、MD3 ロール)
Theme — frozen dataclass、すべての色は hex 文字列、MD3 ロール:
primary, on_primary, primary_container, on_primary_container、
secondary_container, on_secondary_container, surface, surface_container_low、
surface_container, surface_container_high, surface_container_highest, on_surface、
on_surface_variant, outline, outline_variant, error, on_error, error_container、
on_error_container, shadow_alpha (int 0..255、影は常に黒)。
LIGHT = Theme(primary="#6750A4", on_primary="#FFFFFF",
primary_container="#EADDFF", on_primary_container="#21005D",
secondary_container="#E8DEF8", on_secondary_container="#1D192B",
surface="#FEF7FF", surface_container_low="#F7F2FA",
surface_container="#F3EDF7", surface_container_high="#ECE6F0",
surface_container_highest="#E6E0E9", on_surface="#1D1B20",
on_surface_variant="#49454F", outline="#79747E", outline_variant="#CAC4D0",
error="#B3261E", on_error="#FFFFFF",
error_container="#F9DEDC", on_error_container="#410E0B", shadow_alpha=45)
DARK = Theme(primary="#D0BCFF", on_primary="#381E72",
primary_container="#4F378B", on_primary_container="#EADDFF",
secondary_container="#4A4458", on_secondary_container="#E8DEF8",
surface="#141218", surface_container_low="#1D1B20",
surface_container="#211F26", surface_container_high="#2B2930",
surface_container_highest="#36343B", on_surface="#E6E0E9",
on_surface_variant="#CAC4D0", outline="#938F99", outline_variant="#49454F",
error="#F2B8B5", on_error="#601410",
error_container="#8C1D18", on_error_container="#F9DEDC", shadow_alpha=120)
「ロール / on-ロール」のペアは必須です: primary 上のテキストは常に on_primary、
secondary_container 上のテキストは on_secondary_container、primary_container 上のテキストは
on_primary_container。"white"/"black" のハードコードは禁止です。accent_fg(theme) は
compat ヘルパーとして残され、theme.on_primary を返します。
def rgba(hex_color: str, alpha_0_255: int) -> str: # QSS は 0..255 のアルファ値を期待、0..1 ではない
c = QColor(hex_color)
return f"rgba({c.red()}, {c.green()}, {c.blue()}, {alpha_0_255})"
def shadow_color(theme: Theme) -> QColor:
c = QColor(0, 0, 0); c.setAlpha(theme.shadow_alpha); return c
定数:
- フォーム:
RADIUS_WINDOW = 16,RADIUS_CARD = 12,RADIUS_MENU = 8,RADIUS_FIELD = 4、SIDEBAR_WIDTH = 220。ボタン/ピル — radius = height/2; チップ/セグメント — full。 EDGE_MARGIN = 12(ルートのシャドウパディングと一致)、エッジのビットマスクEDGE_TOP, EDGE_BOTTOM, EDGE_LEFT, EDGE_RIGHT = 1, 2, 4, 8。DUR_FAST = 140(hover),DUR_PRESS = 90,DUR_RELEASE = 200,DUR_MED = 250(ページ、ドロワー、トグル、セグメント),DUR_RIPPLE = 450;EASE = OutCubic,EASE_PRESS = OutQuad。PAGES = ["Обзор", "Задачи", "Ввод", "Чат", "Файлы", "Настройки"]。REPLIES— 5 つの短いボットの応答。SVG_EYE/SVG_EYE_SLASH— Material Symbolsvisibility/visibility_off(viewBox 0 0 24 24,fill="currentColor")。
タイポグラフィ: system_font(point_size=14, weight) — チェーン Roboto → Google Sans →
Noto Sans → Segoe UI, setStyleStrategy(PreferAntialias)。サイズ: ページタイトル 24、
カードタイトル 16/Medium、ボタンテキスト 14/Medium、本文 14、セカンダリ 12、時刻 11。
余白はスケール 4/8/12/16/24/32 から。
影: ウィンドウ/パネル — blurRadius 24, offset (0,4); カード (elevation 1) —
blurRadius 8, offset (0,1); 色 shadow_color(theme)。
ステートレイヤー (コンテナ上、「on-ロール」の色で): hover 0.08, press 0.12 (つまり
0.08*hover + 0.04*press); スライダーは hover 0.10 / press 0.16; チェックボックスは 0.06 (unchecked)
/ 0.10 (checked)。
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, margins 12) →
panel (QFrame#Panel, drop-shadow) → TopAppBar (56px) + body (QHBoxLayout) →
NavigationDrawer (220px) + separator (1px, outline_variant) + QStackedWidget。
パネルは build_stylesheet(theme) を受け取ります — QSS は子に継承されます。さらに
_apply_palette(theme) は、ネイティブに描画される部分のために QPalette ロール
(Window/Base=surface, Text=on_surface, Highlight=primary, HighlightedText=on_primary,
PlaceholderText=on_surface_variant, ToolTipBase=surface_container_high…) を設定します。
影と角丸は子の QFrame#Panel に適用され、ウィンドウ自体には適用されません: ウィンドウは透明で、
影のために 12px のパディングを提供します。QMainWindow にエフェクトを適用することはできません。
ドラッグとリサイズ
ウィンドウの移動 — Qt6 のネイティブメカニズム (TopAppBar.mousePressEvent 内):
handle = self._window.windowHandle()
if handle is not None:
handle.startSystemMove()
TopAppBar のダブルクリック — 最大化/通常。
エッジでのリサイズ — ネイティブ QWindow.startSystemResize。MainWindow には
QApplication 上のイベントフィルター が設定されています: MouseButtonPress(Left) が
≤ EDGE_MARGIN のゾーンで発生した場合、_start_system_resize(edges) を呼び出し True を返します;
MouseMove でボタンが押されていない場合はカーソルを更新します; Leave でカーソルをリセットします。
_resize_edges_for(pos) → エッジのビットマスク (ウィンドウが表示されていて最大化されていない場合のみ)。
_cursor_for_edges: 中央 → Arrow; 左/右 → SizeHor; 上/下 → SizeVer;
上+左 または 下+右 → SizeFDiag; それ以外 → SizeBDiag。
PyQt6 の落とし穴: Qt.Edge は int に変換できず、| で int と混在できません。
エッジの計算は独自のビット定数 EDGE_* で行われ、フラグは次のように組み立てられます:
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. カスタムウィジェット(正確なパラメータ)
MdButton (filled / tonal, state layer + ripple)
QPushButton, 高さ 40, ピル半径 20, フォント 14/Medium,padding 0 24px, minWidth = text + 48。kind="filled"→ bgprimary/fgon_primary;kind="tonal"→ bgsecondary_container/fgon_secondary_container。- QSS:
background: transparent; color: {fg}— 背景は手動で描画、テキストはsuper().paintEvent()経由。 Q_PROPERTY hover/press/ripple(float)。fg の色のステートレイヤー:alphaF = 0.08*hover + 0.04*press、角丸矩形でクリップ。- Ripple:
mousePressEventでポイントを記憶 (_ripple_origin)、pressedで_ripple_animを 0→1 (DUR_RIPPLE) で起動; paint ではポイントからの楕円を 半径max_r*ripple(max_r = 最も遠いコーナーまでの距離 + 4)、alphaF = 0.18*(1-ripple)、 ピル形状でクリップ。アニメーションへの参照は属性に保持します。
MdSwitch (MD3 トグル)
QWidget52×32、シグナルtoggled(bool)はクリック時のみ;setCheckedは静かにアニメーションします。Q_PROPERTY knobPos(0..1)DUR_MED/EASE、さらに hover/press。- Off: トラック
surface_container_highest+ 枠線 2pxoutline; つまみ 16px 色outline、中心 x=16。On: トラックprimary枠線なし; つまみ 24pxon_primary、 中心 x=36、内側にチェックマークprimary色 (draw_checkmark, pen 2px)。Press: つまみ 28px。 - トラック/つまみは knobPos に応じて
lerp_colorで補間; 枠線は t→1 で消える (alpha = 255*(1-t)); つまみの周りに 40px のステートレイヤー円。
MdSlider
QWidget高さ 28、valueChanged(int)。トラック 4px radius 2: 非アクティブ部surface_container_highest、アクティブ部primary。つまみ — 20px の円primary+ 薄い影 (黒 alpha 30, offset y+1)。ステートレイヤー 40pxprimaryalphaF = 0.10*hover + 0.06*press。- x からの値:
pad = 10(つまみの半径)、ratio = (x-pad)/(width-2*pad); ドラッグ時 — 慣性なし、それ以外はfillがDUR_FASTでアニメーション。
MdCheckBox
QWidget高さ 24、フォント 14、minWidth = 34 + text。ボックス 18×18 radius 2 at x=3。- Unchecked: 枠線 2px
on_surface_variant。Checked: 塗りつぶしprimary+ チェックマークon_primary(pen 2px RoundCap/RoundJoin)、チェックマークは段階的に「描画」されます (check0..1、drawPathによる 2 つのセグメント線)。 - ステートレイヤー 36px:
on_surface0.06 (unchecked) /primary0.10 (checked) * hover。 boxScale(OutBack) は press 時にボックスを縮小;setChecked(checked, animate)はtoggledを発行;mouseReleaseEventでのクリックは、カーソルが rect 内にある場合のみ。
MdSegmentedControl / MdSegmentButton
- コンテナ高さ 40、ピル半径 20、枠線 1px
outline(paintEvent で描画)。 - セグメントは等幅 (
addWidget(btn, 1))、高さ 38、フォント 14/Medium; margins (1,1,1,1), spacing 0。選択されていない隣接セグメント間の区切り線 1pxoutline(paintEvent で int 座標で描画)。 - インジケーター —
QFrame#SegIndicator(bgsecondary_container, radius 19) ボタンの後ろ、QPropertyAnimation(geometry)DUR_MED;showEventでは —lower()+ アニメーションなしのジオメトリ。 - 選択されたセグメント: テキスト
on_secondary_container+ テキストの左側にチェックマーク (draw_checkmark); 非選択:on_surface。テキスト色はアニメーション (colorQColor property)。セグメントのステートレイヤー — on-ロールの色のピル。
DropGlyph / FileDropArea
- Glyph 38×38: 下矢印 + トレイ、色
lerp(on_surface_variant, primary, level)、 haloprimaryalpha 0.10*level。 FileDropArea(QFrame): minHeight 150、setAcceptDrops(True)。背景surface_container_lowradius 16; 点線 2pxoutline_variant(dashPattern [4,3.5]); hover/drag 時 — 点線primary+ オーバーレイprimary_containeralpha ≈ 60glow+20press。dragEnterEvent(hasUrls)/dragLeaveEvent/dropEvent(isLocalFile()のみ) →dropped(list[str]); クリック →QFileDialog.getOpenFileNames。すべてのシグナルロジックは不変。
WindowButton (ウィンドウボタン)
QPushButton40×40、radius 20、Q_PROPERTY hover/press、set_maximized(bool)。- グリフ
QPen1.8 RoundCap/RoundJoin、int 座標: 最小化 — 水平線; 最大化 — 12×12 の正方形 (outline)、最大化時 — 2 つの重なった正方形; 閉じる — X 印。 - グリフの色
lerp(on_surface_variant, on_surface, hover); 閉じるは hover 時に背景error(40px の円) + グリフon_error。その他は hover 時にステートレイヤーon_surface0.08。 - 右側の順序: 最小化、最大化、閉じる。
SidebarToggle
QPushButton40×40、ステートレイヤーon_surface0.08、グリフlerp(on_surface_variant, on_surface, hover)。- グリフ: 16×16 の枠線 radius 2 + 区切り線; 開いたパネル — 左側の区切り線 + シェブロン
<(panel-left-close); 折りたたまれた状態 — 右側の区切り線 (panel-left-open)。 set_collapsed(bool);MainWindow.toggle_sidebarと同期。
TopAppBar
QWidget#TopAppBar高さ 56、Panel の上に透明; margins (8,0,8,0), spacing 4。- 左に
SidebarToggle、次にタイトル「МойПроект」18/Mediumon_surface、stretch、右にWindowButton最小化/最大化/閉じる。_max_btn+_sync_max_stateは eventFilter のWindowStateChangeを介して。startSystemMove+ ダブルクリックで最大化。
NavButton / NavigationDrawer
NavButton(QPushButton#NavButton)高さ 48、フォント 14、checkable; アイコン 20×20 at x=16 はQPainterで描画 (Material Symbols outlined): dashboard (4 つの四角)、tasks (円+チェック)、 edit (鉛筆)、chat (吹き出し)、folder、tune (3 本の線 + ノブ)。テキストは QSS 経由padding-left: 48px。nextCheckState