PyQt+QML 实现个性化消息通知栏源码分享

PyQt+QML 实现个性化消息通知栏源码分享

  • 一、效果展示
  • 二、源码分享
    • 1、工程结构
    • 2、库安装
    • 3、main.py
    • 4、main.qml
  • 三、实现原理
    • 1、整体架构
    • 2、数据模型设计
    • 3、消息类型与配色映射
    • 4、Canvas 绘制箭头丝带
    • 5、倒计时与自动移除机制
    • 6、交互逻辑
    • 7、小结

一、效果展示

二、源码分享

1、工程结构

2、库安装

3、main.py

importsysimportosfromPyQt6.QtCoreimportQUrlfromPyQt6.QtGuiimportQGuiApplicationfromPyQt6.QtQmlimportQQmlApplicationEnginedefmain():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))ifnotengine.rootObjects():print("错误:无法加载 QML 文件",file=sys.stderr)sys.exit(-1)sys.exit(app.exec())if__name__=="__main__":main()

4、main.qml

importQtQuickimportQtQuick.WindowimportQtQuick.ControlsimportQtQuick.Layouts ApplicationWindow{id:root visible:true width:800height:600title:"消息提示栏"color:"#f1f5f9"QtObject{id:messageType readonlypropertyintinfo:0readonlypropertyintwarning:1readonlypropertyinterror:2}ListModel{id:messagesModel Component.onCompleted:{append({id:1,text:"这是一条信息提示",type:messageType.info,remaining:5000,duration:5000})append({id:2,text:"这是一条警告提示",type:messageType.warning,remaining:5000,duration:5000})append({id:3,text:"这是一条故障提示",type:messageType.error,remaining:5000,duration:5000})}}propertyintmsgCounter:3function getColor(type){switch(type){casemessageType.info:return"#3b82f6"casemessageType.warning:return"#f59e0b"casemessageType.error:return"#ef4444"default:return"#3b82f6"}}function getProgressColor(type){switch(type){casemessageType.info:return"#bfdbfe"casemessageType.warning:return"#fde68a"casemessageType.error:return"#fecaca"default:return"#bfdbfe"}}function getIcon(type){switch(type){casemessageType.info:return"💡"casemessageType.warning:return"⚡"casemessageType.error:return"❌"default:return"💡"}}function addMessage(text,type,durationMs=5000){msgCounter++messagesModel.append({id:msgCounter,text:text,type:type,remaining:durationMs,duration:durationMs})}function closeMessage(msgId){for(var i=0;i<messagesModel.count;i++){if(messagesModel.get(i).id===msgId){messagesModel.remove(i)break}}}ColumnLayout{anchors.fill:parent spacing:0Item{Layout.preferredHeight:50Layout.fillWidth:true}Rectangle{Layout.preferredHeight:60Layout.fillWidth:true color:"transparent"ListView{id:messagesListView anchors.fill:parent anchors.margins:8clip:true model:messagesModel orientation:ListView.Horizontal spacing:-10//尖角重叠效果 layoutDirection:Qt.LeftToRight//添加消息后自动滚动到最右边 onCountChanged:{Qt.callLater(function(){positionViewAtEnd()})}//右侧留白,补偿最后一个箭头的12px延伸 footer:Item{width:12height:messagesListView.height}delegate:Item{id:messageItem width:160height:messagesListView.heightpropertyintmsgId:model.idpropertystring msgText:model.textpropertyintmsgType:model.typepropertyreal progress:model.duration>0? model.remaining/model.duration:0Canvas{id:ribbonCanvas width:parent.width+12height:parent.height onPaint:{var ctx=getContext("2d")ctx.clearRect(0,0,width,height)ctx.beginPath()ctx.moveTo(12,0)ctx.lineTo(width-12,0)ctx.lineTo(width,height/2)ctx.lineTo(width-12,height)ctx.lineTo(12,height)ctx.lineTo(24,height/2)ctx.closePath()ctx.fillStyle=getColor(msgType)ctx.fill()ctx.strokeStyle="#ffffff30"ctx.lineWidth=1ctx.stroke()}Rectangle{x:24y:parent.height-height-3width:(parent.width-48)*messageItem.progress height:2color:getProgressColor(msgType)}}RowLayout{anchors.fill:parent anchors.leftMargin:28anchors.rightMargin:28spacing:4Text{text:getIcon(msgType)font.pixelSize:14Layout.alignment:Qt.AlignVCenter}Text{text:msgText color:"#ffffff"font.pixelSize:12elide:Text.ElideRight Layout.fillWidth:true Layout.alignment:Qt.AlignVCenter wrapMode:Text.WordWrap}}MouseArea{anchors.fill:parent cursorShape:Qt.PointingHandCursor onClicked:{closeMessage(msgId)}}}}}Item{Layout.fillWidth:true Layout.fillHeight:true Column{anchors.centerIn:parent spacing:20Row{anchors.horizontalCenter:parent.horizontalCenter spacing:16Button{width:150height:50text:"💡 添加提示"palette.button:"#3b82f6"palette.buttonText:'#050505'onClicked:{addMessage("这是一条信息提示",messageType.info)}}Button{width:150height:50text:"⚡ 添加警告"palette.button:"#f59e0b"palette.buttonText:"#050505"onClicked:{addMessage("这是一条警告提示",messageType.warning)}}Button{width:150height:50text:"❌ 添加故障"palette.button:"#ef4444"palette.buttonText:"#050505"onClicked:{addMessage("这是一条故障提示",messageType.error)}}}}}}Timer{id:countdownTimer interval:100repeat:true running:true triggeredOnStart:true onTriggered:{var toRemove=[]for(var i=0;i<messagesModel.count;i++){var msg=messagesModel.get(i)var newRemaining=msg.remaining-100if(newRemaining<=0){toRemove.push(i)}else{messagesModel.setProperty(i,"remaining",newRemaining)}}for(var j=toRemove.length-1;j>=0;j--){messagesModel.remove(toRemove[j])}}}}

三、实现原理

1、整体架构

本项目采用PyQt6 + QML混合架构:Python 负责应用生命周期与 QML 引擎加载,QML 负责界面渲染与交互逻辑。两者通过QQmlApplicationEngine桥接,Python 侧只做最少的引导工作,所有界面逻辑都写在 QML 中,便于快速迭代和热调试。

main.py 入口

创建 QGuiApplication

创建 QQmlApplicationEngine

加载 main.qml

ApplicationWindow 初始化

ListModel 预置 3 条消息

Timer 驱动倒计时

Canvas 绘制箭头丝带

点击关闭 / 按钮添加

2、数据模型设计

消息列表使用 QML 内置的ListModel作为数据源,每条消息包含 5 个字段:

字段类型作用
idint消息唯一标识,用于定位删除
textstring消息显示文本
typeint消息类型(info/warning/error)
remainingint剩余存活时间(毫秒)
durationint总存活时间(毫秒)

remaining与duration的比值即为进度条的实时进度,由Timer每 100ms 递减更新,实现平滑的倒计时效果。

3、消息类型与配色映射

通过QtObject定义三个只读常量作为枚举,配合三个映射函数实现「类型 → 视觉样式」的转换:

  • getColor(type):返回主色(丝带填充色),info 蓝色、warning 橙色、error 红色;
  • getProgressColor(type):返回进度条浅色,与主色同色系但更淡;
  • getIcon(type):返回对应 emoji 图标(💡 / ⚡ / ❌)。

这种「常量 + 映射函数」的设计避免了在 delegate 中写大量if/else,新增类型时只需扩展QtObject和三个函数即可。

4、Canvas 绘制箭头丝带

每个消息项的核心视觉是「箭头丝带」,使用 QMLCanvas2D 上下文绘制:

  1. 画布宽度比消息项宽 12px,为右侧箭头尖角留出延伸空间;
  2. beginPath()后依次连线:左上角 → 右上角 → 右侧中点(箭头尖)→ 右下角 → 左下角 → 左侧中点(内凹尖角)→ 闭合;
  3. 用getColor(msgType)填充,再用半透明白色描边,形成立体感;
  4. 进度条作为Rectangle叠加在画布底部,宽度按progress比例动态计算。

左侧内凹 12px、右侧外凸 12px 的设计,配合ListView的spacing: -10,让相邻消息产生「箭头咬合」的重叠效果。

5、倒计时与自动移除机制

核心逻辑由Timer驱动:

Timer { id: countdownTimer interval: 100 repeat: true running: true triggeredOnStart: true onTriggered: { var toRemove = [] for (var i = 0; i < messagesModel.count; i++) { var msg = messagesModel.get(i) var newRemaining = msg.remaining - 100 if (newRemaining <= 0) { toRemove.push(i) } else { messagesModel.setProperty(i, "remaining", newRemaining) } } for (var j = toRemove.length - 1; j >= 0; j--) { messagesModel.remove(toRemove[j]) } } }

关键点:

  • 先收集后删除:遍历时先记录待删除下标,再倒序删除,避免正序删除导致索引错位;
  • 倒序删除:从后往前remove,保证前面的索引不受影响;
  • 进度条联动:remaining递减后,delegate 中progress = remaining / duration自动重算,进度条随之缩短。

6、交互逻辑

  • 点击消息关闭:delegate 内MouseArea捕获点击,调用closeMessage(msgId)按 id 从ListModel中移除;
  • 按钮添加消息:三个按钮分别调用addMessage(text, type),内部自增msgCounter生成新 id,并追加到ListModel;
  • 自动滚动到最新:ListView监听onCountChanged,用Qt.callLater延迟调用positionViewAtEnd(),确保新消息出现后自动滚到最右侧。

7、小结

整个通知栏的核心设计可以概括为三点:

  1. 数据驱动视图:ListModel作为唯一数据源,delegate 只负责渲染,增删改都通过模型操作;
  2. Canvas 定制视觉:用 2D 绘制实现普通控件难以做到的箭头丝带造型;
  3. Timer 统一驱动:所有消息共享一个 100ms 定时器,批量递减、批量清理,性能开销极小。


r)

相关新闻

MySQL 索引 B + 树机制深度剖析

MySQL 索引 B + 树机制深度剖析

目录 一、索引 为什么一定要用索引&#xff1f; 二、为什么 MySQL 偏偏选 B树 1.HASH&#xff08;哈希&#xff09; 2.二叉搜索树&#xff08;BST / AVL / 红黑树&#xff09; 3.N 叉树 4.B 树&#xff1a;最终答案 B 树 vs B 树&#xff0c;强在哪&#xff1f; 三、深…

2026/10/20 11:01:58 阅读更多 →
读懂 AS300 激光对中仪|偏移角(角偏差)到底代表什么?

读懂 AS300 激光对中仪|偏移角(角偏差)到底代表什么?

很多现场师傅做完激光对中&#xff0c;看得懂平行偏移&#xff0c;却看不懂偏移角&#xff08;角偏差&#xff09;。明明径向数值调得很小&#xff0c;设备依旧振动大、联轴器弹性体频繁开裂&#xff0c;多半就是偏移角超标埋下的隐患。AS300 激光对中仪完成盘车测量后&#xf…

2026/10/20 11:01:58 阅读更多 →
把Kronos-Tokenizer-base集成进量化流程:离散Token ID在K线时序策略中的落地指南

把Kronos-Tokenizer-base集成进量化流程:离散Token ID在K线时序策略中的落地指南

人工智能基础模型金融科技 【免费下载链接】kronos-tokenizer-base-npu 用户可在昇腾 NPU 上直接运行 Kronos-Tokenizer-base 金融 K 线时间序列模型&#xff0c;将 OHLCV 等连续数据转换为离散 Token ID&#xff0c;支持离线推理、CPU/NPU 一致性验证与性能基准测试&#xff0…

2026/10/20 11:01:58 阅读更多 →

最新新闻

多模态数据扩散模型做预测:原理、实操与工程落地全解析

多模态数据扩散模型做预测:原理、实操与工程落地全解析

打开扩散模型相关论文的时候&#xff0c;我第一反应也是“又一个做图像生成的”。直到我自己把扩散模型接到一个多模态预测项目上&#xff0c;才发现这条路的价值被严重低估了。传统的预测模型&#xff0c;不管是LSTM、Transformer还是各种树模型&#xff0c;本质上都在学“给定…

2026/10/20 14:18:12 阅读更多 →
AA-Scan:可编程扫描仪行为建模工具

AA-Scan:可编程扫描仪行为建模工具

简介&#xff1a;AA-Scan是一款基于Arduino与Android平台的开源全自动3D扫描仪软硬件方案&#xff0c;面向嵌入式开发初学者、创客及三维建模爱好者&#xff0c;解决低成本、易组装、可复现的简易3D扫描设备搭建问题。资源包为完整项目源码&#xff0c;共5个文件&#xff1a;含…

2026/10/20 14:18:12 阅读更多 →
强化学习数据集实战:环境下载、接口适配与数据处理全指南

强化学习数据集实战:环境下载、接口适配与数据处理全指南

强化学习这几个字一摆出来&#xff0c;很多人第一反应就是算法、策略梯度、PPO、SAC满天飞&#xff0c;但真正动手跑过实验的人心里都清楚&#xff1a;数据或者环境&#xff0c;才是整个项目的地基。算法再漂亮&#xff0c;丢到一个状态空间设计不合理、奖励信号稀疏到离谱的环…

2026/10/20 14:18:12 阅读更多 →
不用买开发板,纯仿真环境也能学STM32:GPIO到串口全流程实战

不用买开发板,纯仿真环境也能学STM32:GPIO到串口全流程实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 14:18:12 阅读更多 →
多模态扩散模型预测实战:从条件生成到未来分布

多模态扩散模型预测实战:从条件生成到未来分布

做生成模型这一块时间久了&#xff0c;其实会有个惯性思维&#xff1a;扩散模型约等于生成器。画图、配音、生成视频、做超分&#xff0c;全是拿它当生产工具使。但前一阵我接手了一个预测类项目&#xff0c;数据是多模态的&#xff0c;要求输出的又得是未来时刻的状态分布&…

2026/10/20 14:18:12 阅读更多 →
Qt5翻金币游戏:手写状态机与paintEvent动画实战

Qt5翻金币游戏:手写状态机与paintEvent动画实战

简介&#xff1a;这是一份基于Qt5框架开发的翻金币互动游戏完整源码项目&#xff0c;面向C与GUI编程初学者及Qt跨平台开发学习者&#xff0c;帮助掌握窗口管理、信号槽机制、动画实现与界面美化等核心技能。资源包含104个文件&#xff0c;涵盖21个.cpp源文件、18个.png资源图、…

2026/10/20 14:16:25 阅读更多 →

日新闻

CLRC663芯片Pin-to-Pin替代实战:从评估到量产全记录

CLRC663芯片Pin-to-Pin替代实战:从评估到量产全记录

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 1:01:30 阅读更多 →
ESP32舵机控制指南:原理、多路驱动与避坑实战

ESP32舵机控制指南:原理、多路驱动与避坑实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 1:01:30 阅读更多 →
位移法实现一阶低通滤波:单片机ADC采样降噪实战

位移法实现一阶低通滤波:单片机ADC采样降噪实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 1:01:30 阅读更多 →

周新闻

基于GMM的语音识别系统原理与MATLAB实现详解

基于GMM的语音识别系统原理与MATLAB实现详解

说到基于高斯混合模型&#xff08;GMM&#xff09;的语音识别系统&#xff0c;很多人第一反应就是一堆数学公式&#xff0c;觉得门槛很高。但如果你真的在MATLAB里把它落地跑通&#xff0c;会发现核心其实就是三件事&#xff1a;把声音变成特征向量&#xff0c;用GMM给每个类别…

2026/10/20 10:02:13 阅读更多 →
数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理这四个字听起来挺唬人&#xff0c;但落到具体操作上&#xff0c;无非就是让计算机按你的想法“摆弄”像素。我在一个模拟项目X里用了一套特别经典的基础处理流程&#xff1a;对图像做反白、改调色板、彩色转灰度&#xff0c;最后把像素数据导出成txt文件。单看每一…

2026/10/19 17:32:41 阅读更多 →
机器人大脑技术盘点:具身智能的感知、决策与数据闭环

机器人大脑技术盘点:具身智能的感知、决策与数据闭环

如果你的信息流最近也天天刷到“机器人大脑”这个词&#xff0c;你会发现一个很有意思的现象&#xff1a;每个人说的“大脑”根本不是同一个东西。有人指的是大语言模型&#xff0c;有人指的是运动控制系统&#xff0c;还有人指的是能理解物理世界的端到端模型。在2026年这个时…

2026/10/20 2:50:50 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 10:02:08 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 9:50:17 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 9:50:45 阅读更多 →