最近想用 Python 写一个比较有趣的桌面小动画,效果类似满屏弹窗表白。

程序运行后,会先按照一定的时间间隔逐步弹出多个小窗口,每个窗口中间显示一句情话,例如“顺顺利利”“好好爱自己”“天冷了多穿衣服”“多喝水哦”“别熬夜”等。第一阶段,这些小窗口会根据心形坐标逐个出现,并最终在屏幕中间组成一颗爱心。

爱心成型后,窗口会停留几秒,然后再逐步消失。随后进入第二个动画阶段,大量小窗口会一个接一个随机弹出,分布在屏幕的不同位置,最终形成随机铺满全屏的效果。

整个效果主要包括以下几个部分:

  1. 使用 Python 的 tkinter 创建弹窗窗口;
  2. 每个弹窗显示不同的文字内容;
  3. 弹窗背景颜色随机变化;
  4. 通过数学公式生成心形轨迹坐标;
  5. 让窗口按照心形轨迹逐个出现;
  6. 心形窗口停留一段时间后逐步关闭;
  7. 最后随机生成多个弹窗,铺满整个屏幕;
  8. 支持调整窗口数量、出现速度、停留时间、窗口大小、文字内容和颜色。

这个小案例比较适合用来练习 Python 桌面 GUI、tkinter 弹窗控制、定时器动画以及坐标计算等内容。整体实现难度不高,但视觉效果比较有趣,适合做成表白小程序或者桌面趣味动画。

# -*- coding: utf-8 -*-
import math
import random
import tkinter as tk
# =============================================================
#                       可自定义参数区
# =============================================================
# 情话文案(想加多少句加多少句)
PHRASES = [
    "顺顺利利", "好好爱自己", "天冷了多穿衣服", "多喝水哦", "别熬夜",
    "记得吃早餐", "今天也要开心", "我爱你", "想你了", "笑一个",
    "你最棒", "辛苦了", "抱抱你", "晚安好梦", "早安宝贝",
    "永远在一起", "5 2 0", "我喜欢你", "你是我的唯一", "宝贝加油",
    "心情要美美的", "保护好自己", "万事胜意", "陪你到老", "宠你一辈子",
]

# (背景色, 文字色) 配对,保证文字看得清楚
COLOR_PAIRS = [
    ("#FFB6C1", "#8B0000"),   # 浅粉  / 暗红
    ("#FFC0CB", "#C71585"),   # 粉    / 紫红
    ("#FF8FB1", "#FFFFFF"),   # 玫粉  / 白
    ("#FF69B4", "#FFFFFF"),   # 亮粉  / 白
    ("#ADD8E6", "#00008B"),   # 浅蓝  / 深蓝
    ("#87CEEB", "#000080"),   # 天蓝  / 海军蓝
    ("#B5EAD7", "#1B5E20"),   # 薄荷  / 深绿
    ("#98FB98", "#1B5E20"),   # 嫩绿  / 深绿
    ("#FFFACD", "#8B4513"),   # 浅黄  / 棕
    ("#FFE4B5", "#A0522D"),   # 米黄  / 赭石
    ("#FFA07A", "#8B0000"),   # 浅橙  / 暗红
    ("#E6E6FA", "#4B0082"),   # 薰衣草/ 靛紫
    ("#DDA0DD", "#800080"),   # 梅紫  / 紫
    ("#FFD1DC", "#D81B60"),   # 樱花粉/ 玫红
]


CONFIG = {
    # 小窗口尺寸
    "win_width":        160,
    "win_height":        65,

    # ---------- 阶段一:心形(沿轮廓"接龙"堆叠) ----------
    "heart_count":      110,    # 沿心形轮廓排列的窗口数量(越多越密、堆得越厚)
    "heart_size_ratio":  0.78,  # 爱心占屏幕的比例(0~1),越大爱心越大
    "heart_start_phase": 0.0,   # 起始角度(弧度)。0=从心形顶部凹口出发,math.pi=从心尖出发
    "phase1_interval":   35,    # 每个心形窗口出现的间隔(毫秒)
    "shuffle_heart":     False, # False=沿轮廓顺序"接龙";True=随机顺序出现

    # ---------- 阶段二:保持 + 消失 ----------
    "hold_duration":   3000,    # 爱心保持的时间(毫秒)
    "phase2_interval":   25,    # 每个窗口消失的间隔(毫秒)
    "fade_steps":        10,    # 每个窗口"渐隐"的过渡步数,越大越平滑
    "fade_step_delay":   22,    # 渐隐每步之间的间隔(毫秒)

    # ---------- 阶段三:满屏随机 ----------
    "gap_before_phase3": 600,   # 阶段二结束到阶段三开始之间的停顿(毫秒)
    "phase3_count":     140,    # 满屏随机弹窗的数量
    "phase3_interval":   35,    # 随机弹窗每个之间的间隔(毫秒)

    # ---------- 字体 ----------
    "font_family":      "Microsoft YaHei",
    "font_size":         12,
    "font_weight":      "bold",
}


# =============================================================
#                           主程序
# =============================================================

class LovePopupShow:
    def __init__(self, config):
        self.cfg = config

        # 根窗口隐藏起来,仅用来调度
        self.root = tk.Tk()
        self.root.withdraw()
        self.root.title("520")

        # 屏幕尺寸
        self.screen_w = self.root.winfo_screenwidth()
        self.screen_h = self.root.winfo_screenheight()

        # 已创建的小窗口列表
        self.windows = []

        # 全局退出快捷键
        self.root.bind_all("<Escape>", lambda e: self.quit_all())

    # ---------- 工具方法 ----------

    def _new_popup(self, x, y, text=None, color_pair=None):
        """在 (x, y) 位置创建一个小弹窗。"""
        text = text if text is not None else random.choice(PHRASES)
        bg, fg = color_pair if color_pair is not None else random.choice(COLOR_PAIRS)

        w = tk.Toplevel(self.root)
        w.title("❤")
        w.geometry(
            f"{self.cfg['win_width']}x{self.cfg['win_height']}+{int(x)}+{int(y)}"
        )
        w.configure(bg=bg)
        w.resizable(False, False)

        label = tk.Label(
            w,
            text=text,
            bg=bg,
            fg=fg,
            font=(self.cfg["font_family"], self.cfg["font_size"], self.cfg["font_weight"]),
        )
        label.pack(expand=True, fill="both", padx=4, pady=4)

        # 单个窗口按 Esc 也能关闭整个程序
        w.bind("<Escape>", lambda e: self.quit_all())

        self.windows.append(w)
        return w

    def _heart_outline_points(self):
        """
        沿心形轮廓采样 n 个点,按参数 t 增大的方向连续排列,
        因此窗口会像一条"绳子"一样依次首尾相接地堆在轮廓上。
        参数方程:
            x = 16 sin^3(t)
            y = 13 cos(t) - 5 cos(2t) - 2 cos(3t) - cos(4t)
        在该方程下:x ∈ [-16, 16],y ∈ [-17, 5](凹口在上、心尖在下)。
        屏幕 y 轴向下,所以这里翻转 y。
        """
        n      = self.cfg["heart_count"]
        ratio  = self.cfg["heart_size_ratio"]
        t0     = self.cfg["heart_start_phase"]

        cx = self.screen_w / 2 - self.cfg["win_width"] / 2
        cy = self.screen_h / 2 - self.cfg["win_height"] / 2

        # 把数学坐标里 32 宽、22 高的心形放进屏幕的 ratio 区域
        max_w = self.screen_w * ratio
        max_h = self.screen_h * ratio
        scale = min(max_w / 32.0, max_h / 22.0)

        points = []
        for i in range(n):
            t = t0 + 2 * math.pi * i / n
            x = 16 * (math.sin(t) ** 3)
            y = 13 * math.cos(t) - 5 * math.cos(2 * t) \
                - 2 * math.cos(3 * t) - math.cos(4 * t)
            sx = cx + x * scale
            sy = cy - y * scale  # 翻转 y 轴
            points.append((sx, sy))
        return points

    def _fade_out(self, win, on_done=None):
        """让一个窗口逐步透明并销毁。"""
        steps = self.cfg["fade_steps"]
        delay = self.cfg["fade_step_delay"]

        def step(i):
            if not win.winfo_exists():
                if on_done:
                    on_done()
                return
            alpha = max(0.0, 1.0 - i / steps)
            try:
                win.attributes("-alpha", alpha)
            except tk.TclError:
                pass
            if i >= steps:
                try:
                    win.destroy()
                except tk.TclError:
                    pass
                if win in self.windows:
                    self.windows.remove(win)
                if on_done:
                    on_done()
            else:
                self.root.after(delay, lambda: step(i + 1))

        step(1)

    def quit_all(self):
        for w in list(self.windows):
            try:
                w.destroy()
            except tk.TclError:
                pass
        self.windows.clear()
        try:
            self.root.destroy()
        except tk.TclError:
            pass

    # ---------- 三个阶段 ----------

    def phase1_heart(self):
        """阶段一:沿心形轮廓依次弹出小窗口,像绳子一样首尾相接。"""
        points = self._heart_outline_points()
        if self.cfg["shuffle_heart"]:
            random.shuffle(points)

        n = len(points)
        for i, (x, y) in enumerate(points):
            self.root.after(
                i * self.cfg["phase1_interval"],
                lambda x=x, y=y: self._new_popup(x, y),
            )

        # 爱心组完 + 停留时间 之后,进入阶段二
        total = n * self.cfg["phase1_interval"] + self.cfg["hold_duration"]
        self.root.after(total, self.phase2_disappear)

    def phase2_disappear(self):
        """阶段二:爱心窗口逐个渐隐消失。"""
        wins = list(self.windows)
        random.shuffle(wins)  # 随机顺序消失,更自然

        for i, w in enumerate(wins):
            self.root.after(
                i * self.cfg["phase2_interval"],
                lambda w=w: self._fade_out(w),
            )

        # 估算最后一个窗口完全消失需要的时间
        last_start = (len(wins) - 1) * self.cfg["phase2_interval"]
        fade_time = self.cfg["fade_steps"] * self.cfg["fade_step_delay"]
        total = last_start + fade_time + self.cfg["gap_before_phase3"]
        self.root.after(total, self.phase3_random)

    def phase3_random(self):
        """阶段三:满屏随机弹窗。"""
        margin = 0
        max_x = max(margin, self.screen_w - self.cfg["win_width"] - margin)
        max_y = max(margin, self.screen_h - self.cfg["win_height"] - margin)

        for i in range(self.cfg["phase3_count"]):
            x = random.randint(margin, max_x)
            y = random.randint(margin, max_y)
            self.root.after(
                i * self.cfg["phase3_interval"],
                lambda x=x, y=y: self._new_popup(x, y),
            )

    # ---------- 启动 ----------

    def run(self):
        # 稍等一会儿再开始,给屏幕一个缓冲
        self.root.after(400, self.phase1_heart)
        self.root.mainloop()


if __name__ == "__main__":
    LovePopupShow(CONFIG).run()

 

更多推荐