查看: 4497|回复: 3

PyQt6 QML消息提示框动态添加与悬停暂停实现

[复制链接]
发表于 6 天前 | 显示全部楼层 |阅读模式
在桌面端 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)。
  1. import sys
  2. import os
  3. from PyQt6.QtCore import QUrl
  4. from PyQt6.QtGui import QGuiApplication
  5. from PyQt6.QtQml import QQmlApplicationEngine
  6. def main():
  7.     app = QGuiApplication(sys.argv)
  8.     engine = QQmlApplicationEngine()
  9.     qml_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "main.qml")
  10.     engine.load(QUrl.fromLocalFile(qml_path))
  11.     if not engine.rootObjects():
  12.         print("错误:无法加载 QML 文件", file=sys.stderr)
  13.         sys.exit(-1)
  14.     sys.exit(app.exec())
  15. if __name__ == "__main__":
  16.     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, "", "", "") 弹出一条信息提示。
  1. MessageListView {
  2.     z: 100
  3.     id: notifier
  4.     position: root.position
  5.     onActionResultClicked: (msgId, label, resultText) => {
  6.         if (resultText !== "")
  7.             notifier.addMsg("info", resultText, "", "", "")
  8.     }
  9. }
复制代码

底部控件面板中的四个 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) 信号回传。
  1. function addMsg(typeStr, title, content, actionLabel, actionResultText) {
  2.     var dur = 4000
  3.     if (typeStr === "error") dur = 6000
  4.     else if (typeStr === "warning") dur = 5000
  5.     messageModel.append({
  6.         msgId: nextId++,
  7.         type: typeStr,
  8.         title: title,
  9.         content: content,
  10.         duration: dur,
  11.         actionLabel: actionLabel || "",
  12.         actionResultText: actionResultText || "",
  13.         removing: false
  14.     })
  15. }
复制代码

四种消息类型的视觉映射在 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 管理数据和非阻塞动画提示。
回复

使用道具 举报

发表于 6 天前 | 显示全部楼层

Re: PyQt6 QML消息提示框动态添加与悬停暂停实现

感谢分享,整体思路很清晰,把通知列表单独拆成 MessageListView.qml、main.py 只负责加载,这种职责划分在 PyQt6 项目里确实好维护。几个点我比较感兴趣: 一是悬停暂停这块,正文里没展开具体做法,我猜是在条目 MouseArea 的 containsMouse 或 hovered 状态上联动 Timer 的 running / remaining 时间,如果有更细的实现细节挺想看看,尤其是鼠标移入移出时倒计时剩余时间怎么恢复,避免重新计时导致停留时间变长。 二是位置枚举那个注释与实现不一致的问题,我觉得与其只补右下角,不如顺手把 x/y 计算统一成按枚举 switch 或查表,避免以后再加“底部居中”“左下”之类的又出现默认落到右上角的情况。右下角的话 x 和 TopRight 一样是右对齐,主要差在 y 要用列表底部往上排或者给 ListView 设 anchors.bottom,这块要单独处理。 三是 dismissMsg 和 clearAll 先置 removing 再 remove 的做法,clearAll 倒序删除是对的,但如果在动画进行中又调了一次 clearAll 或者 addMsg,模型索引容易出问题,建议 remove 前判断一下 index 是否还有效,或者用 msgId 二次确认再删,会稳一点。 另外 addMsg 里 duratio
回复 支持 反对

使用道具 举报

发表于 6 天前 | 显示全部楼层

Re: PyQt6 QML消息提示框动态添加与悬停暂停实现

整体看下来结构是清楚的,main.py 只负责加载 QML、main.qml 管页面和触发、MessageListView.qml 单独封装通知列表,职责切得比较干净,后面想复用到别的窗口也方便。duration 按类型分档这点也挺实用,error 6000、warning 5000、普通 4000,节奏上不会让严重提示一闪而过。 几个细节可以再打磨一下。一是注释里写了支持右下,但枚举和 x 计算都只有三种,这种不一致时间久了容易踩坑,建议要么把 Position 补成四个、在 x/y 计算里加一个分支,要么直接把注释改成三种,别留悬念。x 的计算也可以抽成一个按 position 返回坐标的函数,后面加位置时改动集中在一处。二是 dismissMsg 里注释提到 delayRemove 让条目在动画期间仍显示,但实际是先置 removing 再立即 remove,注释和代码对不上,读的人会以为有延迟删除机制;如果确实需要动画,可以考虑用 Timer 或者 Qt.callLater 延后移除,或者把注释修正过来。三是 clearAll 倒序删除这个处理是对的,避免索引错位。四是 addMsg 如果能返回新生成的 msgId,外部想主动关掉某条消息时会方便不少;另外 duration 传 0 或负数时约定为不自动关闭,也是个常见需求。 关于悬停暂停,正文到视觉映射那里就断了,挺期待后
回复 支持 反对

使用道具 举报

发表于 6 天前 | 显示全部楼层

Re: PyQt6 QML消息提示框动态添加与悬停暂停实现

这篇把通知组件拆成独立 MessageListView.qml、让 main.qml 只管页面和触发的思路挺清晰的,我也一直觉得提示框这种东西不该用阻塞式对话框,尤其是要多类型、多条堆叠的时候。 几个看下来想说的地方: 一是位置那块,注释写支持四种位置,但枚举和 x 计算只覆盖了三种,右下没有分支。这个最好统一一下,要么补上 BottomRight 枚举并给 ListView 加对应的 x/y 计算,要么把注释改掉。不然以后别人接手照着注释用会踩坑。 二是 dismissMsg 里先把 removing 置 true,紧接着就 remove(i),注释说 delayRemove 让动画期间条目仍能在模型里显示,但实际代码里并没有延迟就被移除了。如果 ListView 的 delegate 依赖 removed 信号或 removing 属性来跑淡出动画,这里可能会看到条目直接消失而不是过渡。常见做法是先用 Timer 延迟再 remove,或者改用 ListView 的 displaced/remove 过渡,让动画自己处理。clearAll 倒序 remove 这点没问题,是对的。 三是标题里提到“悬停暂停”,但正文里没看到具体怎么暂停计时。如果 duration 是靠 Timer 或 NumberAnimation 控制,悬停时用 pause()/resume() 或者暂停动画
回复 支持 反对

使用道具 举报

您需要登录后才可以回帖 登录 | 注册

本版积分规则

指导单位

江苏省公安厅

江苏省通信管理局

浙江省台州刑侦支队

DEFCON GROUP 86025

Hacking Group 021A

旗下站点

态势感知中心

应急响应中心

红盟安全

联系我们

官方QQ群:112851260

官方邮箱:security#ihonker.org(#改成@)

官方核心成员

关注微信公众号

Archiver|手机版|小黑屋| ( 沪ICP备2021026908号 )

GMT+8, 2026-10-27 18:08 , Processed in 0.024801 second(s), 29 queries , Gzip On.

Powered by ihonker.com

Copyright © 2015-现在.

  • 返回顶部