在桌面端 PyQt6 项目中,消息提示往往不能只弹一个阻塞式对话框,还需要处理多类型、自动关闭、悬停暂停、位置切换和操作按钮回调。原文的实现把通知列表封装成独立的 MessageListView.qml,由 main.qml 负责页面和触发按钮,main.py 只加载 QML。工程结构上,main.py 与 main.qml 同目录,MessageListView.qml 作为独立通知列表组件被引用。原文目录中列出了“库安装”,但正文未给出具体安装命令,本文不补写。
Python 入口使用 PyQt6.QtGui.QGuiApplication 和 PyQt6.QtQml.QQmlApplicationEngine,通过 QUrl.fromLocalFile 加载同目录 main.qml,并在 engine.rootObjects() 为空时打印错误后 sys.exit(-1)。
- import sys
- import os
- from PyQt6.QtCore import QUrl
- from PyQt6.QtGui import QGuiApplication
- from PyQt6.QtQml import QQmlApplicationEngine
- def main():
- app = QGuiApplication(sys.argv)
- engine = QQmlApplicationEngine()
- qml_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "main.qml")
- engine.load(QUrl.fromLocalFile(qml_path))
- if not engine.rootObjects():
- print("错误:无法加载 QML 文件", file=sys.stderr)
- sys.exit(-1)
- sys.exit(app.exec())
- if __name__ == "__main__":
- main()
复制代码
main.qml 中 ApplicationWindow 尺寸为 1000×700,最小 360×240,定义 property int position 保存通知位置,MessageListView 设置 z:100、id: notifier、position 绑定 root.position,并监听 onActionResultClicked。当操作按钮回传的 resultText 非空时,再调用 notifier.addMsg("info", resultText, "", "", "") 弹出一条信息提示。
- MessageListView {
- z: 100
- id: notifier
- position: root.position
- onActionResultClicked: (msgId, label, resultText) => {
- if (resultText !== "")
- notifier.addMsg("info", resultText, "", "", "")
- }
- }
复制代码
底部控件面板中的四个 MsgButton 分别调用 addMsg 添加 success、error、warning、info 消息,参数依次为类型、标题、正文、操作按钮文字和按钮点击后的提示文本。例如 error 调用传入“重试连接”和“正在重新连接...”,warning 传入“立即清理”和“正在清理缓存文件...”。PosButton 只覆盖左上、顶部居中、右上三个位置。
MessageListView.qml 根节点是 Item,枚举 Position 定义为 TopLeft、TopCenter、TopRight。文档注释写支持左上、顶部居中、右上、右下四种位置,但实际枚举和 main.qml 的位置按钮只有三种,ListView 的 x 计算也只处理 TopLeft、TopCenter 和默认右上;右下没有对应分支。这是原文注释与实现不一致的地方,若需要右下,必须扩展枚举和 x/y 计算。
核心数据用 ListModel 管理,字段包括 msgId、type、title、content、duration、actionLabel、actionResultText、removing。nextId 从 1 递增。addMsg 的 duration 默认 4000,error 为 6000,warning 为 5000;actionLabel 为空字符串时不显示操作按钮。对外方法还有 dismissMsg(msgId) 和 clearAll():dismissMsg 遍历模型找到 msgId,先把 removing 置 true,再 remove(i),代码注释说明 delayRemove 让条目在动画期间仍显示;clearAll 倒序把 removing 置 true 后逐条 remove。操作按钮通过 actionResultClicked(int msgId, string label, string resultText) 信号回传。
- function addMsg(typeStr, title, content, actionLabel, actionResultText) {
- var dur = 4000
- if (typeStr === "error") dur = 6000
- else if (typeStr === "warning") dur = 5000
- messageModel.append({
- msgId: nextId++,
- type: typeStr,
- title: title,
- content: content,
- duration: dur,
- actionLabel: actionLabel || "",
- actionResultText: actionResultText || "",
- removing: false
- })
- }
复制代码
四种消息类型的视觉映射在 colorForType、bgForType、iconForType 中完成:success 用 #10b981、背景 #ecfdf5、图标 ✓;error 用 #ef4444、背景 #fef2f2、图标 ✕;warning 用 #f59e0b、背景 #fffbeb、图标 !;info 用 #3b82f6、背景 #eff6ff、图标 i。默认分支按 info 处理。
自动关闭由 MessageCard 内的 progressTimer 完成,interval 为 100,running 条件为 autoDismissMs > 0 && !isRemoving && !paused,每次 progress += 100 / autoDismissMs,达到 1.0 后发出 dismissRequested()。paused 的悬停判断来自 hoverArea、actionArea、closeArea 的 containsMouse;同时组件维护 _hoverCount 和 anyCardHovered,delegate 把 paused 设为 notifierRoot.anyCardHovered,实现任一卡片悬停时全部暂停。从 QML 绑定关系看,delegate 显式传入 paused 后会覆盖 MessageCard 内部“property bool paused: isHovered”的初始绑定,实际生效的是全局悬停广播。
列表动画包括 add、remove、move、populate:添加时透明度 0→1、x 从 380 进入,时长 400ms;移除时透明度到 0、x 到 760,时长约 300–350ms;重排时对 y 和 x 做 400ms 动画;populate 让透明度 0→1、y 从 2000 进入。delayRemove 通过 _delayRemove 和 releaseDelayTimer 控制,Timer interval 380ms,略长于淡出动画 300ms,确保动画播完再释放。ListView 在 onCountChanged 中用 Qt.callLater(positionViewAtEnd()) 延迟滚动到底部,避免 contentHeight 未刷新。ListView 宽度 380,高度取 Math.min(contentHeight, 父高度-40),interactive 设为 false,spacing 为 12。
排查时可以按以下顺序看:QML 未加载时检查 main.py 的 rootObjects 判断和 main.qml 路径;通知不显示时检查 MessageListView 是否被 z:100 遮盖、parent 尺寸和 position 绑定;消息不自动关闭时检查 autoDismissMs、paused、isRemoving 和 progressTimer 的 running 条件;悬停暂停异常时检查 _hoverCount 增减以及 delegate 的 paused 绑定;位置切换异常时注意当前只实现三种位置;移除动画异常时检查 _delayRemove、releaseDelayTimer 和 ListView.delayRemove。该组件适合桌面应用中的操作结果、网络错误、空间警告、版本提示等通知场景,核心优势是 QML 组件化、ListModel 管理数据和非阻塞动画提示。 |