Skip to content

PyQt5相关内容

修订记录
时间内容修订人
2026-04-11创建胡英万

一、PyQt5安装

shell
#模块安装
pip install PyQt5 #安装pyqt5

pip install pyqt5-tools

pip install pyqt5designer

二、Pycharm配置

shell
#1.设计UI文件-designer.exep配置exe和工作目录
$FileDir$
#2.配置将ui文件转换为pyqt5, 用pyhton.exe 参数为
-m PyQt5.uic.pyuic $FileName$ -o $FileNameWithoutExtension$.py
#3.配置将qrc文件转换为py文件,用pyqrcc5.exe 参数为, 
$FileName$ -o $FileNameWithoutExtension$_rc.py
#4.1-3的工作目录都是:$FileDir$

三、py文件打包成exe

shell
 pyinstaller -F -w -i logo.ico --add-data "logo.ico;." --add-data "logo.png;." --name "工具集合" .\logmqtt_mqtt.py

PyQt5 现代 UI 布局示例笔记

基于银河流水线工具(pipeline.py + pipeline.ui)的 UI 方案整理,含可运行的简化示例代码。
技术栈:PyQt5 + Qt Designer(.ui)+ QSS 主题


目录

  1. 整体布局:左菜单 + 右主区 + 底日志
  2. 无边框窗口 + 自定义标题栏
  3. 主区域卡片设计(QGroupBox)
  4. 自定义弹窗 ModernMessageBox
  5. 最小可运行完整示例
  6. 与 pipeline.py 对照表

1. 整体布局:左菜单 + 右主区 + 底日志

1.1 布局思路

text
┌────────────────────────────────────────┐
│  CustomTitleBar(自定义标题栏)          │
├─────────┬──────────────────────────────┤
│ sidebar │  stack_pages(主内容页)       │
│ nav     │  ┌──────┐ ┌──────┐           │
│ list    │  │卡片1 │ │卡片2 │           │
│         │  └──────┘ └──────┘           │
│         ├──────────────────────────────┤
│         │  log_text(日志,可折叠)      │
└─────────┴──────────────────────────────┘

核心控件:

控件作用
QListWidget左侧菜单
QStackedWidget右侧多页切换(不是 Tab)
QSplitter侧栏宽度、日志高度可拖、可折叠
QGroupBox主区「卡片」容器

1.2 示例:Splitter + 导航 + 堆叠页

python
# -*- coding: utf-8 -*-
"""示例 1:左菜单 + 右主区 + 底日志"""
import sys
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import (
    QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
    QSplitter, QListWidget, QListWidgetItem, QStackedWidget,
    QGroupBox, QTextEdit, QLabel, QPushButton,
)


class DemoShell(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("布局示例")
        self.resize(1000, 640)

        root = QWidget()
        self.setCentralWidget(root)
        outer = QHBoxLayout(root)
        outer.setContentsMargins(0, 0, 0, 0)

        # 水平 Splitter:左 sidebar | 右 main
        split_h = QSplitter(Qt.Horizontal)
        outer.addWidget(split_h)

        # 左侧菜单
        sidebar = QWidget()
        sidebar.setFixedWidth(220)
        sidebar_layout = QVBoxLayout(sidebar)
        self.nav = QListWidget()
        self.nav.addItem(QListWidgetItem("Windows 流水线"))
        self.nav.addItem(QListWidgetItem("Linux 流水线"))
        sidebar_layout.addWidget(self.nav)
        split_h.addWidget(sidebar)

        # 右侧:垂直 Splitter = 主区 + 日志
        main_panel = QWidget()
        main_layout = QVBoxLayout(main_panel)
        main_layout.setContentsMargins(0, 0, 0, 0)

        split_v = QSplitter(Qt.Vertical)
        main_layout.addWidget(split_v)

        # 主内容:QStackedWidget
        self.pages = QStackedWidget()
        self.pages.addWidget(self._make_page("Windows 页面"))
        self.pages.addWidget(self._make_page("Linux 页面"))
        split_v.addWidget(self.pages)

        # 底部日志
        log_box = QGroupBox("日志")
        log_layout = QVBoxLayout(log_box)
        self.log_text = QTextEdit()
        self.log_text.setReadOnly(True)
        self.log_text.setStyleSheet(
            "QTextEdit { background:#2B2B2B; color:#BBBBBB; border:none; }"
        )
        log_layout.addWidget(self.log_text)
        split_v.addWidget(log_box)

        split_v.setStretchFactor(0, 1)
        split_v.setStretchFactor(1, 0)
        split_v.setSizes([500, 160])

        split_h.addWidget(main_panel)
        split_h.setStretchFactor(0, 0)
        split_h.setStretchFactor(1, 1)
        split_h.setSizes([220, 780])

        self.nav.currentRowChanged.connect(self.pages.setCurrentIndex)
        self.nav.setCurrentRow(0)

    def _make_page(self, title):
        page = QWidget()
        layout = QVBoxLayout(page)
        layout.addWidget(QLabel(title))
        layout.addWidget(QPushButton("示例按钮"))
        layout.addStretch()
        return page


if __name__ == "__main__":
    app = QApplication(sys.argv)
    w = DemoShell()
    w.show()
    sys.exit(app.exec_())

1.3 pipeline.py 中的对应实现

  • 静态布局pipeline.ui 里画 sidebarmain_panelstack_pagesgroupBox_log
  • 动态组装MainWindow._init_shell_from_ui()split_root(水平)+ split_main(垂直)两个 QSplitter
  • 页面切换app_nav_list.currentRowChangedstack_pages.setCurrentIndex

入门记忆:

  • QStackedWidget 像一叠纸,只显示当前页
  • QSplitter 让用户拖分隔条改大小、折叠侧栏/日志
  • 不是 QTabWidget,是 QListWidget 导航 + QStackedWidget

2. 无边框窗口 + 自定义标题栏

2.1 三步走

python
self.setWindowFlags(Qt.Window | Qt.FramelessWindowHint)  # 1. 去系统边框
self.setAttribute(Qt.WA_TranslucentBackground, True)       # 2. 背景透明(圆角用)
# 3. 顶部放 CustomTitleBar,自己实现拖动和 − □ ×

2.2 层级结构

text
QMainWindow(透明)
└── centralwidget(留边距给阴影)
    └── window_shell(圆角 10px + 阴影 + setMask 裁剪四角)
        ├── CustomTitleBar(40px 高)
        └── window_body(侧栏 + 主区 + 日志)

2.3 示例:自定义标题栏

python
# -*- coding: utf-8 -*-
"""示例 2:无边框 + 自定义标题栏"""
import sys
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import (
    QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
    QLabel, QPushButton,
)


class CustomTitleBar(QWidget):
    """拖动、双击最大化、最小化/关闭。"""

    def __init__(self, window):
        super().__init__(window)
        self._window = window
        self._drag_pos = None
        self.setFixedHeight(40)
        self.setStyleSheet(
            "background:#E0F2FE; border-bottom:1px solid #CBD5E1;"
        )

        layout = QHBoxLayout(self)
        layout.setContentsMargins(12, 0, 4, 0)

        self.title = QLabel(window.windowTitle())
        self.title.setStyleSheet("font-weight:600; color:#0369A1;")
        layout.addWidget(self.title, 1)

        for text, slot in [
            ("−", window.showMinimized),
            ("□", self._toggle_max),
            ("×", window.close),
        ]:
            btn = QPushButton(text)
            btn.setFixedSize(40, 28)
            btn.setFlat(True)
            btn.clicked.connect(slot)
            layout.addWidget(btn)

    def _toggle_max(self):
        if self._window.isMaximized():
            self._window.showNormal()
        else:
            self._window.showMaximized()

    def mousePressEvent(self, event):
        if event.button() == Qt.LeftButton:
            self._drag_pos = event.globalPos() - self._window.frameGeometry().topLeft()

    def mouseMoveEvent(self, event):
        if self._drag_pos and event.buttons() & Qt.LeftButton:
            if not self._window.isMaximized():
                self._window.move(event.globalPos() - self._drag_pos)

    def mouseReleaseEvent(self, event):
        self._drag_pos = None

    def mouseDoubleClickEvent(self, event):
        if event.button() == Qt.LeftButton:
            self._toggle_max()


class FramelessDemo(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowFlags(Qt.Window | Qt.FramelessWindowHint)
        self.setAttribute(Qt.WA_TranslucentBackground, True)
        self.resize(800, 500)

        shell = QWidget()
        shell.setObjectName("shell")
        shell.setStyleSheet(
            "#shell { background:#F0F9FF; border:1px solid #E8EAED; border-radius:10px; }"
        )

        container = QWidget()
        self.setCentralWidget(container)
        outer = QVBoxLayout(container)
        outer.setContentsMargins(10, 10, 10, 10)

        shell_layout = QVBoxLayout(shell)
        shell_layout.setContentsMargins(0, 0, 0, 0)

        self.title_bar = CustomTitleBar(self)
        shell_layout.addWidget(self.title_bar)
        shell_layout.addWidget(QLabel("这里放 sidebar + 主区 + 日志", alignment=Qt.AlignCenter))

        outer.addWidget(shell)


if __name__ == "__main__":
    app = QApplication(sys.argv)
    w = FramelessDemo()
    w.show()
    sys.exit(app.exec_())

2.4 圆角裁剪(RoundedShellWidget)

子控件是矩形,会溢出圆角,用 setMask 裁剪:

python
from PyQt5.QtCore import QRectF
from PyQt5.QtGui import QPainterPath, QRegion

def apply_round_mask(widget, radius=10):
    path = QPainterPath()
    path.addRoundedRect(QRectF(widget.rect()), radius, radius)
    widget.setMask(QRegion(path.toFillPolygon().toPolygon()))

resizeEvent 里调用。最大化时 radius=0,去掉 mask 和阴影边距。

2.5 要点

项目说明
.ui 还是代码标题栏、圆角、拖动 必须代码_install_frameless_chrome
阴影QGraphicsDropShadowEffect 加在 window_shell
注意setGraphicsEffect(None) 会销毁 effect,还原时要 新建_apply_window_shadow
边缘缩放主窗口距边缘 6px 内可拖拽改大小

3. 主区域卡片设计(QGroupBox)

3.1 卡片 = QGroupBox + QSS 渐变

Qt 没有 Card 控件,用 QGroupBox 模拟卡片:

python
CARD_STYLE = """
QGroupBox {
    background: qlineargradient(x1:0, y1:0, x2:0, y2:1,
        stop:0 #FFFFFF, stop:0.55 #F0F9FF, stop:1 #E0F2FE);
    border: 1px solid #E8EAED;
    border-radius: 10px;
    margin-top: 14px;
    padding: 12px 10px 10px 10px;
    font-size: 13px;
    font-weight: 600;
    color: #374151;
}
QGroupBox::title {
    subcontrol-origin: margin;
    subcontrol-position: top left;
    left: 12px;
    padding: 0 6px;
}
"""

BTN_PRIMARY = """
QPushButton {
    background-color: #0284C7; color: white; font-weight: 600;
    border: none; border-radius: 8px; padding: 6px 16px; min-height: 28px;
}
QPushButton:hover { background-color: #0369A1; }
QPushButton:disabled { background-color: #E5E7EB; color: #9CA3AF; }
"""

BTN_GHOST = """
QPushButton {
    background-color: #F0F9FF; color: #0369A1;
    border: 1px solid #7DD3FC; border-radius: 8px; padding: 6px 16px;
}
QPushButton:hover { background-color: #E0F2FE; }
"""

BTN_DANGER = """
QPushButton {
    background-color: #FEF2F2; color: #DC2626;
    border: 1px solid #FECACA; border-radius: 8px; padding: 6px 16px;
}
"""

3.2 示例:阶段卡片横排

python
# -*- coding: utf-8 -*-
"""示例 3:卡片式主区域"""
import sys
from PyQt5.QtWidgets import (
    QApplication, QWidget, QVBoxLayout, QHBoxLayout,
    QGroupBox, QPushButton, QFormLayout, QLineEdit,
)

CARD_STYLE = """
QGroupBox {
    background: qlineargradient(x1:0,y1:0,x2:0,y2:1, stop:0 #FFF, stop:1 #E0F2FE);
    border: 1px solid #E8EAED; border-radius: 10px;
    margin-top: 14px; padding: 12px; font-weight: 600;
}
"""
BTN_PRIMARY = "QPushButton{background:#0284C7;color:#fff;border:none;border-radius:8px;padding:6px 16px;}"


class CardPage(QWidget):
    def __init__(self):
        super().__init__()
        layout = QVBoxLayout(self)
        row = QHBoxLayout()
        row.addWidget(self._card_phase2())
        row.addWidget(self._card_phase3())
        layout.addLayout(row)
        layout.addStretch()

    def _card_phase2(self):
        box = QGroupBox("阶段二:构建并上传")
        box.setStyleSheet(CARD_STYLE)
        form = QFormLayout(box)
        form.addRow("目标目录:", QLineEdit("DEMO"))
        btn = QPushButton("构建并上传")
        btn.setStyleSheet(BTN_PRIMARY)
        form.addRow(btn)
        return box

    def _card_phase3(self):
        box = QGroupBox("阶段三:构建安装包")
        box.setStyleSheet(CARD_STYLE)
        v = QVBoxLayout(box)
        btn = QPushButton("构建安装包")
        btn.setStyleSheet(BTN_PRIMARY)
        v.addWidget(btn)
        return box


if __name__ == "__main__":
    app = QApplication(sys.argv)
    w = CardPage()
    w.resize(900, 300)
    w.show()
    sys.exit(app.exec_())

3.3 主题换肤 ThemeManager

python
THEMES = {
    "ocean": {
        "label": "青蓝",
        "accent": "#0284C7",
        "card_grad": ("#FFFFFF", "#F0F9FF", "#E0F2FE"),
        "border": "#E8EAED",
    },
    "forest": {
        "label": "碧 green",
        "accent": "#16A34A",
        "card_grad": ("#FFFFFF", "#F0FDF4", "#DCFCE7"),
        "border": "#D1FAE5",
    },
}

def build_card_style(theme_id):
    t = THEMES[theme_id]
    g0, g1, g2 = t["card_grad"]
    return f"""
    QGroupBox {{
        background: qlineargradient(x1:0,y1:0,x2:0,y2:1,
            stop:0 {g0}, stop:0.55 {g1}, stop:1 {g2});
        border: 1px solid {t['border']}; border-radius: 10px;
        margin-top: 14px; padding: 12px;
    }}
    """

def build_primary_btn(theme_id):
    accent = THEMES[theme_id]["accent"]
    return f"QPushButton {{ background:{accent}; color:white; border-radius:8px; padding:6px 16px; }}"

pipeline 中 _apply_theme()_apply_modern_styles() 批量给 groupBox_*、按钮 setStyleSheet

3.4 按钮角色

类型用途pipeline 中的按钮
Primary主操作构建、上传、发布、打包
Ghost次要保存配置、选择目录、主题
Danger停止取消构建/上传/打包

4. 自定义弹窗 ModernMessageBox

4.1 结构

text
ModernMessageBox(无边框 + 透明)
└── 外层 layout(留 12px 给阴影)
    └── #dialogCard(圆角渐变 + DropShadow)
        ├── 标题行(标题 + × 关闭)
        ├── 内容行(DialogStatusIcon + 正文 QLabel)
        └── 按钮行(取消左、确定右)

4.2 示例:简化版弹窗

python
# -*- coding: utf-8 -*-
"""示例 4:自定义弹窗"""
import sys
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import (
    QApplication, QDialog, QWidget, QVBoxLayout, QHBoxLayout,
    QLabel, QPushButton, QGraphicsDropShadowEffect, QMainWindow,
)
from PyQt5.QtGui import QColor


class SimpleMessageBox(QDialog):
    def __init__(self, parent, title, message, yes_no=False):
        super().__init__(parent, Qt.Dialog | Qt.FramelessWindowHint)
        self.setAttribute(Qt.WA_TranslucentBackground)
        self.setModal(True)

        outer = QVBoxLayout(self)
        outer.setContentsMargins(12, 12, 12, 12)

        card = QWidget()
        card.setObjectName("dialogCard")
        card.setStyleSheet("""
            #dialogCard {
                background: qlineargradient(x1:0,y1:0,x2:0,y2:1,
                    stop:0 #FFFFFF, stop:1 #E0F2FE);
                border: 1px solid #E8EAED;
                border-radius: 12px;
            }
        """)
        shadow = QGraphicsDropShadowEffect(card)
        shadow.setBlurRadius(28)
        shadow.setOffset(0, 6)
        shadow.setColor(QColor(0, 0, 0, 90))
        card.setGraphicsEffect(shadow)
        outer.addWidget(card)

        layout = QVBoxLayout(card)
        layout.setContentsMargins(18, 16, 18, 18)

        layout.addWidget(QLabel(f"<b>{title}</b>"))
        layout.addWidget(QLabel(message))

        btns = QHBoxLayout()
        btns.addStretch()
        if yes_no:
            cancel = QPushButton("取消")
            cancel.clicked.connect(self.reject)
            btns.addWidget(cancel)
        ok = QPushButton("确定")
        ok.setStyleSheet("background:#0284C7;color:white;border-radius:8px;padding:6px 20px;")
        ok.clicked.connect(self.accept)
        btns.addWidget(ok)
        layout.addLayout(btns)

        self.setFixedWidth(420)

    @classmethod
    def warning(cls, parent, title, message):
        dlg = cls(parent, title, message, yes_no=True)
        return dlg.exec_() == QDialog.Accepted

4.3 业务确认框规范(【】标注动态信息)

python
def confirm_publish(parent, target, build_type, soft_type, tag_version, app_name):
    bat_rel = f"{target}\\RunBuild - C#采集.bat"
    release_rel = f"{target}\\Release"
    msg = (
        "即将执行【一键发布】,请确认:\n\n"
        f"1. 订单目录【{target}\n"
        f"2. 构建前端包【{build_type}\n"
        f"3. 上传覆盖【{target}】前端目录下的【dist/】、【dist-electron/】\n"
        f"4. 软件类型【{soft_type}\n"
        f"5. 发包日期【{tag_version}\n"
        f"6. 软件名称【{app_name}\n"
        f"7. 执行脚本【{bat_rel}\n"
        f"8. 输出目录【{release_rel}\n\n"
        "将依次构建、上传并打包,远程文件及 Release 下安装包可能被覆盖。是否继续?"
    )
    return SimpleMessageBox.warning(parent, "确认一键发布", msg)

【】内全部是动态的:选 DEMO 目录 → 订单目录【DEMO】;HMI/PC 随单选变化。

pipeline 中对应:

  • _confirm_upload() — 阶段二上传
  • _confirm_publish() — 一键发布
  • _confirm_pack() — 阶段三打包

4.4 Toast 通知(右下角)

ToastWidget + ToastManager:同样无边框 + 深色圆角卡片 + 滑入动画,用于构建完成等提示,不阻塞操作

4.5 弹窗放 .ui 还是代码?

适合 .ui适合代码
固定表单、按钮位置无边框、阴影、动画
简单静态对话框ModernMessageBox、Toast、主题联动

5. 最小可运行完整示例

保存为 modern_ui_template.py,整合布局 + 无边框 + 卡片 + 弹窗:

python
# -*- coding: utf-8 -*-
"""
modern_ui_template.py — PyQt5 现代 UI 最小模板
运行: python modern_ui_template.py
"""
import sys
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QColor
from PyQt5.QtWidgets import (
    QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
    QSplitter, QListWidget, QStackedWidget,
    QGroupBox, QTextEdit, QLabel, QPushButton, QDialog,
    QGraphicsDropShadowEffect,
)

CARD_STYLE = """
QGroupBox {
    background: qlineargradient(x1:0,y1:0,x2:0,y2:1, stop:0 #FFF, stop:1 #E0F2FE);
    border: 1px solid #E8EAED; border-radius: 10px;
    margin-top: 14px; padding: 12px; font-weight: 600;
}
"""
BTN_PRIMARY = "QPushButton{background:#0284C7;color:#fff;border:none;border-radius:8px;padding:6px 16px;}"


class TitleBar(QWidget):
    def __init__(self, win):
        super().__init__()
        self.win = win
        self._pos = None
        self.setFixedHeight(40)
        self.setStyleSheet("background:#E0F2FE;border-bottom:1px solid #CBD5E1;")
        lay = QHBoxLayout(self)
        lay.addWidget(QLabel("现代 UI 模板"), 1)
        for t, fn in [("−", win.showMinimized), ("×", win.close)]:
            b = QPushButton(t)
            b.setFlat(True)
            b.clicked.connect(fn)
            lay.addWidget(b)

    def mousePressEvent(self, e):
        if e.button() == Qt.LeftButton:
            self._pos = e.globalPos() - self.win.frameGeometry().topLeft()

    def mouseMoveEvent(self, e):
        if self._pos and e.buttons() & Qt.LeftButton:
            self.win.move(e.globalPos() - self._pos)


class ConfirmDialog(QDialog):
    def __init__(self, parent, title, text):
        super().__init__(parent, Qt.Dialog | Qt.FramelessWindowHint)
        self.setAttribute(Qt.WA_TranslucentBackground)
        outer = QVBoxLayout(self)
        outer.setContentsMargins(12, 12, 12, 12)
        card = QWidget()
        card.setStyleSheet("background:#fff;border:1px solid #E8EAED;border-radius:12px;")
        eff = QGraphicsDropShadowEffect(card)
        eff.setBlurRadius(24)
        eff.setOffset(0, 4)
        eff.setColor(QColor(0, 0, 0, 80))
        card.setGraphicsEffect(eff)
        outer.addWidget(card)
        v = QVBoxLayout(card)
        v.setContentsMargins(16, 16, 16, 16)
        v.addWidget(QLabel(f"<b>{title}</b>"))
        v.addWidget(QLabel(text))
        h = QHBoxLayout()
        h.addStretch()
        no = QPushButton("取消")
        no.clicked.connect(self.reject)
        yes = QPushButton("确定")
        yes.setStyleSheet(BTN_PRIMARY)
        yes.clicked.connect(self.accept)
        h.addWidget(no)
        h.addWidget(yes)
        v.addLayout(h)
        self.setFixedWidth(400)


class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowFlags(Qt.Window | Qt.FramelessWindowHint)
        self.setAttribute(Qt.WA_TranslucentBackground, True)
        self.resize(960, 620)

        shell = QWidget()
        shell.setStyleSheet("background:#F0F9FF;border:1px solid #E8EAED;border-radius:10px;")
        cw = QWidget()
        self.setCentralWidget(cw)
        root = QVBoxLayout(cw)
        root.setContentsMargins(8, 8, 8, 8)
        root.addWidget(shell)

        sl = QVBoxLayout(shell)
        sl.setContentsMargins(0, 0, 0, 0)
        sl.addWidget(TitleBar(self))

        body = QWidget()
        sl.addWidget(body, 1)
        bl = QHBoxLayout(body)
        bl.setContentsMargins(0, 0, 0, 0)

        split_h = QSplitter(Qt.Horizontal)
        bl.addWidget(split_h)

        nav = QListWidget()
        nav.setFixedWidth(200)
        nav.addItems(["页面 A", "页面 B"])
        split_h.addWidget(nav)

        right = QWidget()
        split_h.addWidget(right)
        rl = QVBoxLayout(right)
        split_v = QSplitter(Qt.Vertical)
        rl.addWidget(split_v)

        pages = QStackedWidget()
        self.log = QTextEdit()
        self.log.setStyleSheet("background:#2B2B2B;color:#BBB;border:none;")

        for name in ["A", "B"]:
            p = QWidget()
            pl = QVBoxLayout(p)
            box = QGroupBox(f"阶段 · {name}")
            box.setStyleSheet(CARD_STYLE)
            bv = QVBoxLayout(box)
            btn = QPushButton("执行操作")
            btn.setStyleSheet(BTN_PRIMARY)
            btn.clicked.connect(lambda _, n=name: self._on_action(n))
            bv.addWidget(btn)
            pl.addWidget(box)
            pl.addStretch()
            pages.addWidget(p)
        split_v.addWidget(pages)

        log_box = QGroupBox("日志")
        log_box.setStyleSheet(CARD_STYLE)
        lt = QVBoxLayout(log_box)
        lt.addWidget(self.log)
        split_v.addWidget(log_box)
        split_v.setSizes([450, 140])

        nav.currentRowChanged.connect(pages.setCurrentIndex)

    def _on_action(self, name):
        dlg = ConfirmDialog(self, "确认操作", f"即将对【{name}】执行操作,是否继续?")
        if dlg.exec_():
            self.log.append(f"已确认:{name}")


if __name__ == "__main__":
    app = QApplication(sys.argv)
    w = MainWindow()
    w.show()
    sys.exit(app.exec_())

6. 与 pipeline.py 对照表

笔记示例pipeline.py 中的类/方法
Splitter 布局_init_shell_from_ui()
CustomTitleBarCustomTitleBar
圆角外壳RoundedShellWidget + _install_frameless_chrome()
卡片 QSSThemeManager.build_card_style()
主题切换ThemeManager + _apply_theme()
自定义弹窗ModernMessageBox
ToastToastWidget + ToastManager
上传确认_confirm_upload()
一键发布确认_confirm_publish()
打包确认_confirm_pack()

快速复用 Checklist

  • [ ] Designer 画 sidebarstack_pagesgroupBox_log

  • [ ] 代码里用两个 QSplitter 组装(_init_shell_from_ui

  • [ ] FramelessWindowHint + CustomTitleBar + RoundedShellWidget

  • [ ] 主区用 QGroupBox + 渐变 QSS(ThemeManager

  • [ ] 用 ModernMessageBox 替代系统 QMessageBox

  • [ ] 危险操作用【】标注 + 二次确认

  • [ ] 四套主题颜色进 THEMES 字典,_apply_theme() 统一刷新


架构总览

mermaid
flowchart TB
    subgraph Window["无边框主窗口"]
        TB[CustomTitleBar]
        subgraph Shell["window_shell 圆角+阴影"]
            subgraph H["split_root 水平"]
                SB[sidebar + app_nav_list]
                subgraph MP["main_panel"]
                    subgraph V["split_main 垂直"]
                        ST[stack_pages 卡片页]
                        LOG[groupBox_log 日志]
                    end
                end
            end
        end
    end
    TB --> Shell
    NAV[点击菜单] --> ST
    DIA[ModernMessageBox 弹窗]
    TOA[ToastWidget 通知]

技术笔记文档