主题
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.pyPyQt5 现代 UI 布局示例笔记
基于银河流水线工具(
pipeline.py+pipeline.ui)的 UI 方案整理,含可运行的简化示例代码。
技术栈:PyQt5 + Qt Designer(.ui)+ QSS 主题
目录
- 整体布局:左菜单 + 右主区 + 底日志
- 无边框窗口 + 自定义标题栏
- 主区域卡片设计(QGroupBox)
- 自定义弹窗 ModernMessageBox
- 最小可运行完整示例
- 与 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里画sidebar、main_panel、stack_pages、groupBox_log - 动态组装:
MainWindow._init_shell_from_ui()用split_root(水平)+split_main(垂直)两个QSplitter - 页面切换:
app_nav_list.currentRowChanged→stack_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.Accepted4.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() |
| CustomTitleBar | CustomTitleBar |
| 圆角外壳 | RoundedShellWidget + _install_frameless_chrome() |
| 卡片 QSS | ThemeManager.build_card_style() |
| 主题切换 | ThemeManager + _apply_theme() |
| 自定义弹窗 | ModernMessageBox |
| Toast | ToastWidget + ToastManager |
| 上传确认 | _confirm_upload() |
| 一键发布确认 | _confirm_publish() |
| 打包确认 | _confirm_pack() |
快速复用 Checklist
[ ] Designer 画
sidebar、stack_pages、groupBox_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 通知]