太棒了!下面给你两种 可直接复制运行 的 Python 实现:一个是功能更强、外观更现代的 PyQt5(推荐) 版本;另一个是更轻量、仅依赖标准库的 Tkinter 版本,适合不想装额外库的场景。两个都做了自定义样式、自动消失与交互按钮,方便你直接拿来当桌面“温馨提醒”弹窗。


方案 A — PyQt5(更美观、动画流畅,推荐)

特点:圆角、半透明、阴影、滑入/淡出动画、可自定义图标与按钮
先安装依赖:

pip install PyQt5 pillow

把下面代码保存为 toast_pyqt.py,运行 python toast_pyqt.py

# toast_pyqt.py
"""
美观桌面提醒(PyQt5)
- 圆角、半透明、阴影
- 滑入与淡出动画
- 支持图标、标题、内容、持续时间
"""

import sys
from PyQt5 import QtCore, QtGui, QtWidgets
from PIL import Image, ImageQt
import os

class Toast(QtWidgets.QWidget):
    def __init__(self, title, message, duration=4000, icon_path=None, parent=None):
        super().__init__(parent, flags=QtCore.Qt.FramelessWindowHint | QtCore.Qt.WindowStaysOnTopHint)
        self.duration = duration
        self.opacity_effect = QtWidgets.QGraphicsOpacityEffect(self)
        self.setGraphicsEffect(self.opacity_effect)
        self.opacity_effect.setOpacity(0.0)

        # Window flags for click-through? (we keep it clickable)
        self.setAttribute(QtCore.Qt.WA_TranslucentBackground)
        self.setWindowFlag(QtCore.Qt.Tool)  # don't show in taskbar

        # Layout
        layout = QtWidgets.QHBoxLayout()
        layout.setContentsMargins(14, 12, 14, 12)
        layout.setSpacing(12)

        # Icon
        if icon_path and os.path.exists(icon_path):
            pix = QtGui.QPixmap(icon_path).scaled(48, 48, QtCore.Qt.KeepAspectRatio, QtCore.Qt.SmoothTransformation)
            icon_label = QtWidgets.QLabel()
            icon_label.setPixmap(pix)
            layout.addWidget(icon_label)
        else:
            layout.addSpacing(0)

        # Texts
        text_layout = QtWidgets.QVBoxLayout()
        self.title_label = QtWidgets.QLabel(title)
        self.title_label.setStyleSheet("font-weight:600; font-size:14px; color: #222;")
        self.message_label = QtWidgets.QLabel(message)
        self.message_label.setStyleSheet("font-size:12px; color: #333;")
        self.message_label.setWordWrap(True)
        text_layout.addWidget(self.title_label)
        text_layout.addWidget(self.message_label)
        layout.addLayout(text_layout)

        # Close button
        close_btn = QtWidgets.QPushButton("✕")
        close_btn.setFixedSize(24, 24)
        close_btn.setCursor(QtGui.QCursor(QtCore.Qt.PointingHandCursor))
        close_btn.setStyleSheet("""
            QPushButton{
                border: none;
                background: transparent;
                font-size:12px;
            }
            QPushButton:hover{ color: #f00; }
        """)
        close_btn.clicked.connect(self.close_with_animation)
        layout.addWidget(close_btn, alignment=QtCore.Qt.AlignTop)

        # Container frame to allow rounded corners + background
        frame = QtWidgets.QFrame()
        frame.setLayout(layout)
        frame.setObjectName("frame")
        frame.setStyleSheet("""
            QFrame#frame {
                background: qlineargradient(x1:0 y1:0, x2:1 y2:1,
                            stop:0 rgba(255,255,255,240), stop:1 rgba(245,245,245,230));
                border-radius: 12px;
            }
        """)
        main_layout = QtWidgets.QVBoxLayout(self)
        main_layout.setContentsMargins(8,8,8,8)
        main_layout.addWidget(frame)

        # Drop shadow
        shadow = QtWidgets.QGraphicsDropShadowEffect(self)
        shadow.setBlurRadius(18)
        shadow.setOffset(0, 6)
        shadow.setColor(QtGui.QColor(0,0,0,120))
        frame.setGraphicsEffect(shadow)

        # Size hint
        self.resize(360, 90)

        # Timers & animations
        self.show_anim = QtCore.QPropertyAnimation(self.opacity_effect, b"opacity")
        self.show_anim.setDuration(350)
        self.show_anim.setStartValue(0.0)
        self.show_anim.setEndValue(1.0)

        self.hide_anim = QtCore.QPropertyAnimation(self.opacity_effect, b"opacity")
        self.hide_anim.setDuration(350)
        self.hide_anim.setStartValue(1.0)
        self.hide_anim.setEndValue(0.0)
        self.hide_anim.finished.connect(super().close)

        self._auto_close_timer = QtCore.QTimer(self)
        self._auto_close_timer.setSingleShot(True)
        self._auto_close_timer.timeout.connect(self.close_with_animation)

    def show(self):
        super().show()
        self.raise_()
        self.show_anim.start()
        self._auto_close_timer.start(self.duration)

    def close_with_animation(self):
        if self.hide_anim.state() == QtCore.QAbstractAnimation.Running:
            return
        self.hide_anim.start()

def show_toast(title, message, duration=4000, icon_path=None):
    app = QtWidgets.QApplication.instance() or QtWidgets.QApplication(sys.argv)
    toast = Toast(title, message, duration=duration, icon_path=icon_path)
    # Place at bottom-right of primary screen (or top-right if preferred)
    screen_geo = QtWidgets.QApplication.primaryScreen().availableGeometry()
    x = screen_geo.x() + screen_geo.width() - toast.width() - 20
    y = screen_geo.y() + screen_geo.height() - toast.height() - 40
    toast.move(x, y)
    toast.show()
    # If we created the app, exec
    if not QtWidgets.QApplication.instance():
        sys.exit(app.exec_())

if __name__ == "__main__":
    # 示例调用
    # 准备一个小图标(可选)
    icon_file = None
    # icon_file = "reminder_icon.png"  # 放一个 64x64 的 png 在同目录下试试

    show_toast("温馨提醒", "今天下午 3:30 有团队例会,别忘了准备PPT哦~", duration=6000, icon_path=icon_file)

可自定义项(在调用 show_toast(...) 时传入)

  • title(标题)、message(主文案)、duration(毫秒)、icon_path(图标路径)。
  • 可改位置:把 y 改小为 20 就会变成右上角弹出效果。

方案 B — Tkinter(轻量、跨平台但样式较基础)

不需要额外依赖,直接运行即可。保存为 toast_tkinter.py

# toast_tkinter.py
"""
简单桌面提醒(Tkinter)
- 纯 Python 标准库
- 圆角/阴影受限,但跨平台且无需安装第三方包
"""

import tkinter as tk
import threading
import time

class Toast(tk.Toplevel):
    def __init__(self, title, message, duration=4, **kwargs):
        super().__init__(**kwargs)
        self.overrideredirect(True)  # 去掉窗口边框
        self.attributes("-topmost", True)
        self.config(bg="#333333")
        self.duration = duration

        frm = tk.Frame(self, bg="#ffffff", bd=0)
        frm.pack(padx=6, pady=6)

        title_lbl = tk.Label(frm, text=title, font=("Helvetica", 12, "bold"), bg="#ffffff")
        title_lbl.pack(anchor="w")
        msg_lbl = tk.Label(frm, text=message, font=("Helvetica", 10), bg="#ffffff", wraplength=320, justify="left")
        msg_lbl.pack(anchor="w", pady=(4,0))

        btn = tk.Button(frm, text="关闭", command=self.close)
        btn.pack(anchor="e", pady=(8,0))

        self.update_idletasks()
        w = self.winfo_width()
        h = self.winfo_height()

        # 右下角
        screen_w = self.winfo_screenwidth()
        screen_h = self.winfo_screenheight()
        x = screen_w - w - 20
        y = screen_h - h - 50
        self.geometry(f"{w}x{h}+{x}+{y}")

        # 自动关闭线程
        threading.Thread(target=self._auto_close, daemon=True).start()

    def _auto_close(self):
        time.sleep(self.duration)
        try:
            self.close()
        except:
            pass

    def close(self):
        self.destroy()

if __name__ == "__main__":
    root = tk.Tk()
    root.withdraw()
    Toast("温馨提醒", "别忘了 18:00 去健身房运动~", duration=5)
    root.mainloop()


使用建议 & 小贴士

  • 如果追求“原生感”与更多动画,优先用 PyQt5 / PySide6。两者 API 类似,若用 PySide6,只需把 PyQt5 的 import 换成 PySide6 相应模块(注意小差异)。
  • 想要系统通知(不保证可自定义样式)可以考虑 plyer / win10toast(Windows)/ notify-send(Linux)/ osascript(macOS)。但这些通常样式受限,不如自定义窗口灵活。
  • 可扩展功能:支持「点击跳转打开某个 URL / 程序」、「带进度条的倒计时」、「多个同时显示(管理堆叠)」、「调度定时提醒(结合 schedule 或系统定时任务)」。
  • 打包成单个可执行文件:用 pyinstaller --noconfirm --onefile toast_pyqt.py(注意 PyQt 打包需要额外处理资源)。

如果你想,我可以:

  • 把 PyQt 版本改成 右上角横幅、或支持多个同时排队显示;
  • 加入“稍后提醒 10 分钟”的按钮并演示完整实现;
  • 或把样式改成你喜欢的配色 / 动画风格(举例:模糊背景、渐变色、可拖动窗口)。

想怎么定制就告诉我(比如配色、字体、按钮样式或需要的交互),我直接把代码改好给你。