查看: 188|回复: 0

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

[复制链接]
发表于 1 小时前 | 显示全部楼层 |阅读模式
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 环境为准。
回复

使用道具 举报

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

本版积分规则

指导单位

江苏省公安厅

江苏省通信管理局

浙江省台州刑侦支队

DEFCON GROUP 86025

Hacking Group 021A

旗下站点

态势感知中心

应急响应中心

红盟安全

联系我们

官方QQ群:112851260

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

官方核心成员

关注微信公众号

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

GMT+8, 2026-10-21 13:53 , Processed in 0.025863 second(s), 18 queries , Gzip On, Redis On.

Powered by ihonker.com

Copyright © 2015-现在.

  • 返回顶部