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 中,便于快速迭代和热调试。
2、数据模型设计
消息列表使用 QML 内置的ListModel作为数据源,每条消息包含 5 个字段:
| 字段 | 类型 | 作用 |
|---|---|---|
id | int | 消息唯一标识,用于定位删除 |
text | string | 消息显示文本 |
type | int | 消息类型(info/warning/error) |
remaining | int | 剩余存活时间(毫秒) |
duration | int | 总存活时间(毫秒) |
remaining与duration的比值即为进度条的实时进度,由Timer每 100ms 递减更新,实现平滑的倒计时效果。
3、消息类型与配色映射
通过QtObject定义三个只读常量作为枚举,配合三个映射函数实现「类型 → 视觉样式」的转换:
getColor(type):返回主色(丝带填充色),info 蓝色、warning 橙色、error 红色;getProgressColor(type):返回进度条浅色,与主色同色系但更淡;getIcon(type):返回对应 emoji 图标(💡 / ⚡ / ❌)。
这种「常量 + 映射函数」的设计避免了在 delegate 中写大量if/else,新增类型时只需扩展QtObject和三个函数即可。
4、Canvas 绘制箭头丝带
每个消息项的核心视觉是「箭头丝带」,使用 QMLCanvas2D 上下文绘制:
- 画布宽度比消息项宽 12px,为右侧箭头尖角留出延伸空间;
beginPath()后依次连线:左上角 → 右上角 → 右侧中点(箭头尖)→ 右下角 → 左下角 → 左侧中点(内凹尖角)→ 闭合;- 用
getColor(msgType)填充,再用半透明白色描边,形成立体感; - 进度条作为
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、小结
整个通知栏的核心设计可以概括为三点:
- 数据驱动视图:
ListModel作为唯一数据源,delegate 只负责渲染,增删改都通过模型操作; - Canvas 定制视觉:用 2D 绘制实现普通控件难以做到的箭头丝带造型;
- Timer 统一驱动:所有消息共享一个 100ms 定时器,批量递减、批量清理,性能开销极小。
r)