python

关注公众号 jb51net

关闭
首页 > 脚本专栏 > python > Python PyQt+QML个性化消息通知栏

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 个字段:

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

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

3、消息类型与配色映射

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

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

4、Canvas 绘制箭头丝带

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

  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])
        }
    }
}

关键点:

6、交互逻辑

7、小结

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

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

以上就是Python基于PyQt+QML实现个性化消息通知栏的详细内容,更多关于Python PyQt+QML个性化消息通知栏的资料请关注脚本之家其它相关文章!

您可能感兴趣的文章:
阅读全文