Python基于PyQt+QML实现个性化消息通知栏
作者:小灰灰搞电子
想用 Python 和 QML 做个带箭头丝带效果的消息通知栏吗,本文手把手教你用 PyQt6 和 QML 实现个性化通知栏,包括数据模型设计、Canvas 绘制箭头、倒计时自动移除等核心技巧,代码可直接复用,快来学习吧
一、效果展示


二、源码分享
1、工程结构

2、库安装

3、main.py
import sys
import os
from PyQt6.QtCore import QUrl
from PyQt6.QtGui import QGuiApplication
from PyQt6.QtQml import QQmlApplicationEngine
def main():
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))
if not engine.rootObjects():
print("错误:无法加载 QML 文件", file=sys.stderr)
sys.exit(-1)
sys.exit(app.exec())
if __name__ == "__main__":
main()
4、main.qml
import QtQuick
import QtQuick.Window
import QtQuick.Controls
import QtQuick.Layouts
ApplicationWindow {
id: root
visible: true
width: 800
height: 600
title: "消息提示栏"
color: "#f1f5f9"
QtObject {
id: messageType
readonly property int info: 0
readonly property int warning: 1
readonly property int error: 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 })
}
}
property int msgCounter: 3
function getColor(type) {
switch (type) {
case messageType.info: return "#3b82f6"
case messageType.warning: return "#f59e0b"
case messageType.error: return "#ef4444"
default: return "#3b82f6"
}
}
function getProgressColor(type) {
switch (type) {
case messageType.info: return "#bfdbfe"
case messageType.warning: return "#fde68a"
case messageType.error: return "#fecaca"
default: return "#bfdbfe"
}
}
function getIcon(type) {
switch (type) {
case messageType.info: return "💡"
case messageType.warning: return "⚡"
case messageType.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: 0
Item {
Layout.preferredHeight: 50
Layout.fillWidth: true
}
Rectangle {
Layout.preferredHeight: 60
Layout.fillWidth: true
color: "transparent"
ListView {
id: messagesListView
anchors.fill: parent
anchors.margins: 8
clip: true
model: messagesModel
orientation: ListView.Horizontal
spacing: -10 // 尖角重叠效果
layoutDirection: Qt.LeftToRight
// 添加消息后自动滚动到最右边
onCountChanged: {
Qt.callLater(function () {
positionViewAtEnd()
})
}
// 右侧留白,补偿最后一个箭头的12px延伸
footer: Item {
width: 12
height: messagesListView.height
}
delegate: Item {
id: messageItem
width: 160
height: messagesListView.height
property int msgId: model.id
property string msgText: model.text
property int msgType: model.type
property real progress: model.duration > 0 ? model.remaining / model.duration : 0
Canvas {
id: ribbonCanvas
width: parent.width + 12
height: 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 = 1
ctx.stroke()
}
Rectangle {
x: 24
y: parent.height - height - 3
width: (parent.width - 48) * messageItem.progress
height: 2
color: getProgressColor(msgType)
}
}
RowLayout {
anchors.fill: parent
anchors.leftMargin: 28
anchors.rightMargin: 28
spacing: 4
Text {
text: getIcon(msgType)
font.pixelSize: 14
Layout.alignment: Qt.AlignVCenter
}
Text {
text: msgText
color: "#ffffff"
font.pixelSize: 12
elide: 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: 20
Row {
anchors.horizontalCenter: parent.horizontalCenter
spacing: 16
Button {
width:150
height: 50
text: "💡 添加提示"
palette.button: "#3b82f6"
palette.buttonText: '#050505'
onClicked: {
addMessage("这是一条信息提示", messageType.info)
}
}
Button {
width:150
height: 50
text: "⚡ 添加警告"
palette.button: "#f59e0b"
palette.buttonText: "#050505"
onClicked: {
addMessage("这是一条警告提示", messageType.warning)
}
}
Button {
width:150
height: 50
text: "❌ 添加故障"
palette.button: "#ef4444"
palette.buttonText: "#050505"
onClicked: {
addMessage("这是一条故障提示", messageType.error)
}
}
}
}
}
}
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])
}
}
}
}
三、实现原理
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 绘制箭头丝带
每个消息项的核心视觉是「箭头丝带」,使用 QML Canvas 2D 上下文绘制:
- 画布宽度比消息项宽 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 定时器,批量递减、批量清理,性能开销极小。
以上就是Python基于PyQt+QML实现个性化消息通知栏的详细内容,更多关于Python PyQt+QML个性化消息通知栏的资料请关注脚本之家其它相关文章!
