查看: 4482|回复: 3

PyQt6+QML消息通知栏Canvas箭头与倒计时移除

[复制链接]
发表于 6 天前 | 显示全部楼层 |阅读模式
PyQt6 负责应用生命周期和 QML 引擎加载,QML 负责界面渲染与交互。原文的入口文件 main.py 使用 QGuiApplication 和 QQmlApplicationEngine,通过 os.path 定位同目录 main.qml,再用 QUrl.fromLocalFile 加载;如果 engine.rootObjects() 为空,就向 stderr 输出“错误:无法加载 QML 文件”并 sys.exit(-1)。这种写法避免依赖当前工作目录,适合把 main.py、main.qml 放在同一工程目录。
  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()
复制代码

消息栏的数据源是 QML 内置 ListModel。每条消息有 id、text、type、remaining、duration 五个字段:id 用于按消息定位删除,type 取 info=0、warning=1、error=2,remaining 是剩余存活毫秒数,duration 是总存活时间。初始示例追加三条 5000ms 消息。addMessage(text, type, durationMs=5000) 会先自增 msgCounter,生成新 id,再把 remaining 和 duration 都设为 durationMs;closeMessage(msgId) 遍历模型,按 id 调用 messagesModel.remove(i) 删除。进度条不单独存储,而是由 remaining/duration 实时计算。

视觉映射通过 QtObject 定义只读常量,再用 getColor(type)、getProgressColor(type)、getIcon(type) 三个函数完成类型到样式的转换:info 主色 #3b82f6、进度色 #bfdbfe、图标 💡;warning 主色 #f59e0b、进度色 #fde68a、图标 ⚡;error 主色 #ef4444、进度色 #fecaca、图标 ❌。默认分支回落到 info 的配色和图标。增加新类型时,需要同步扩展常量和这几个映射函数,避免在 delegate 中散落 if/else。

每个消息项用 Canvas 绘制箭头丝带。画布宽度为 parent.width+12,高度与消息项一致,右侧多出的 12px 用于箭头尖角。绘制路径依次为:左上角 (12,0)、右上角 (width-12,0)、右侧中点 (width,height/2)、右下角 (width-12,height)、左下角 (12,height)、左侧中点 (24,height/2),最后 closePath 闭合;填充使用 getColor(msgType),描边使用半透明白色 #ffffff30,线宽 1。进度条是叠加在 Canvas 内的 Rectangle,x 为 24,y 为 parent.height - height - 3,宽度为 (parent.width - 48) * progress,高度 2,颜色取 getProgressColor(msgType)。左侧内凹、右侧外凸配合 ListView 的 spacing: -10,形成相邻消息箭头咬合的重叠效果。

ListView 使用 Horizontal 方向,delegate 宽 160,锚点边距 8,footer 留 12px 宽度补偿最后一个箭头的延伸。消息增加后监听 onCountChanged,用 Qt.callLater(function(){ positionViewAtEnd() }) 延迟滚动到最右侧,确保新消息出现后可见。delegate 内的 MouseArea 铺满消息项,cursorShape 设为 PointingHandCursor,onClicked 调用 closeMessage(msgId),实现点击关闭。

倒计时使用一个共享 Timer 统一驱动:interval 为 100,repeat 为 true,running 为 true,triggeredOnStart 为 true。每次触发遍历 messagesModel,用 newRemaining = msg.remaining - 100 计算新值;如果 newRemaining <= 0,就把当前下标放入 toRemove;否则调用 messagesModel.setProperty(i, "remaining", newRemaining)。遍历结束后,对 toRemove 从后往前删除,避免正序删除导致索引错位。
  1. Timer {
  2.     id: countdownTimer
  3.     interval: 100
  4.     repeat: true
  5.     running: true
  6.     triggeredOnStart: true
  7.     onTriggered: {
  8.         var toRemove = []
  9.         for (var i = 0; i < messagesModel.count; i++) {
  10.             var msg = messagesModel.get(i)
  11.             var newRemaining = msg.remaining - 100
  12.             if (newRemaining <= 0) {
  13.                 toRemove.push(i)
  14.             } else {
  15.                 messagesModel.setProperty(i, "remaining", newRemaining)
  16.             }
  17.         }
  18.         for (var j = toRemove.length - 1; j >= 0; j--) {
  19.             messagesModel.remove(toRemove[j])
  20.         }
  21.     }
  22. }
复制代码

这段逻辑的关键是“先收集后删除”和“倒序删除”:遍历时不能边删边改下标,否则后续元素位置会变化;倒序 remove 可以保证待删除位置之前的索引仍有效。由于所有消息共享一个 100ms 定时器,批量递减和批量清理的开销较小,也避免为每条消息单独创建定时器。

交互部分由三个按钮分别调用 addMessage(信息提示文本, messageType.info)、addMessage(警告提示文本, messageType.warning)、addMessage(故障提示文本, messageType.error),默认 durationMs 为 5000。整个实现可以归纳为:ListModel 作为唯一数据源,delegate 只负责渲染;Canvas 定制普通控件不易实现的箭头丝带;Timer 统一驱动倒计时和自动移除。原文依赖 PyQt6 的 QtQml 与 QML 的 QtQuick、QtQuick.Window、QtQuick.Controls、QtQuick.Layouts 模块,未额外给出安装命令,实际运行时应以原文工程所需的 PyQt6/QML 环境为准。
回复

使用道具 举报

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

Re: PyQt6+QML消息通知栏Canvas箭头与倒计时移除

楼主这个分享挺完整的,从入口加载、数据模型、样式映射到倒计时驱动,整体思路很清晰。几点感受和补充: 一是用 os.path 定位 main.qml 而不是依赖当前工作目录,这点很实用,打包或者从别的目录启动时少踩不少坑。二是把颜色和图标收敛到 QtObject 常量加三个映射函数里,delegate 里就不用散落 if/else,后面加新类型只要改一处,这个取舍很对。三是倒计时删除时先收集下标再从后往前删,避免正序删导致索引错位,这是很多人容易忽略的细节,楼主处理得很稳。 也提几个可以再打磨的地方,供参考: 1. 倒计时用 remaining 每次减 100 来推进,如果主线程被别的操作卡住,Timer 的触发会累计延迟,最后倒计时会比真实时间慢。如果想更准,可以改成记录每条消息的创建时间戳或者到期时间点,每次触发用“到期时间 - 当前时间”重新算剩余量,这样即使 Timer 偶尔跳帧也不影响总时长。 2. 100ms 的刷新频率对两三条消息没问题,但如果消息数量上去,每轮都 setProperty 会让对应 delegate 重新绑定进度条宽度和 Canvas 重绘。可以考虑消息少时用 100ms、多时降到 200~250ms,或者只更新 remaining 变化超过某个阈值的项。另外 Canvas 的 onPaint 里如果依赖 remaining,需要确认有没有在数值变化时调
回复 支持 反对

使用道具 举报

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

Re: PyQt6+QML消息通知栏Canvas箭头与倒计时移除

这个方案整体挺完整的,分层清晰:PyQt6 管生命周期,QML 管界面,路径用 __file__ 定位,不依赖启动目录,这点实际部署和打包时很省心。加载失败直接 stderr 报错并退出,排查也方便。 视觉和交互部分处理得比较细。Canvas 箭头用固定坐标画出左侧内凹、右侧外凸,再配合 spacing 负值和 footer 补偿,相邻消息的咬合效果应该不错。样式没有散落在 delegate 里,而是集中到 QtObject 常量和几个映射函数,后面加新类型不用到处改 if/else,这个习惯值得保持。 倒计时这块,共享一个 100ms Timer 统一驱动、遍历模型改 remaining,逻辑简单直接;删除时先收集下标再倒序处理,避免正序删除后的索引错位,是正确做法。有两点可以留意:一是 remaining 每次固定减 100,依赖 interval 也是 100,以后如果改间隔记得同步,或者干脆按实际时间差算更稳;二是消息数量多时,每 100ms 对每条消息 setProperty 会带来持续的绑定刷新开销,量大的话可以评估一下。 另外首帖末尾的代码好像被截断了,remaining 赋值那句只到 n 就结束了,可能只是粘贴问题。整体思路没问题,期待后续效果或更多细节。
回复 支持 反对

使用道具 举报

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

Re: PyQt6+QML消息通知栏Canvas箭头与倒计时移除

楼主这套方案整体很完整,思路也比较清晰。我比较欣赏用 QtObject 把类型到颜色、图标、进度色的映射集中起来,delegate 里只调用 getColor、getIcon 这些函数,没有散落一堆 if/else,后面加新类型时改动点很明确。Canvas 画箭头丝带那段的路径顺序也合理,左右两侧的凹进和外凸配合 spacing:-10,确实能做出消息之间咬合重叠的效果,footer 留 12px 补偿最后箭头尖角这个细节也考虑到了。 几个小地方可以再打磨一下: 一是倒计时用固定减 100 的方式,在系统繁忙或定时器延迟时会慢慢漂移,剩余时间和真实经过时间对不上。更稳的做法是记下每条消息的到期时间戳,每次触发用当前时间算剩余,或者至少用 Timer 的实际触发间隔来扣减,当然这样模型字段要稍作调整。 二是 100ms 的共享 Timer 遍历整个 model 并 setProperty,消息条数多时会有比较频繁的模型变更和重绘,如果只是进度条动画,可以考虑降到 50 到 100ms 之间的折中,或者让进度条宽度用 Behavior 做插值,减少视觉上的跳变。 三是 Canvas 在宽度或高度变化时会重绘,delegate 又是复用的,消息滚动时可能有点开销,如果后面发现卡顿,可以把箭头路径抽成 Shape 或者用缓存的 Canvas 图像来减轻负担。 四是倒序删除 toRemove
回复 支持 反对

使用道具 举报

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

本版积分规则

指导单位

江苏省公安厅

江苏省通信管理局

浙江省台州刑侦支队

DEFCON GROUP 86025

Hacking Group 021A

旗下站点

态势感知中心

应急响应中心

红盟安全

联系我们

官方QQ群:112851260

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

官方核心成员

关注微信公众号

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

GMT+8, 2026-10-27 17:23 , Processed in 0.031517 second(s), 29 queries , Gzip On.

Powered by ihonker.com

Copyright © 2015-现在.

  • 返回顶部