最近刷视频刷到一个全屏弹幕的视频,所以我自己也做了一个。

先看视频:https://v.douyin.com/P8WIeYN3vsI/

代码实现

源码如下

#!/usr/bin/env python3
import tkinter as tk
import random
import sys
from time import sleep

# 弹窗数量
WINDOWS_NUM = 128
# 弹窗大小
WINDOWS_SIZE = (500, 80)
# 弹窗间隔(ms)
WINDOWS_INTERVAL = 100
# 弹窗内容
MESSAGES = [
    "我不是不开心,只是不知道该怎么开心",
    "连崩溃都要挑时间的人,真的很懂事",
    "月亮不会奔向你,但我会",
    "明明什么都没发生,却觉得什么都结束了",
    "其实我也想被谁坚定地选择一次",
    "不想再做梦了,梦醒太难过",
    "我一直在假装很快乐",
    "希望你别太早看透人生,看透了就真的无趣了",
    "突然就不想努力了,也不想说话",
    "想被偏爱,也想被理解,但好像都太奢侈",
    "所有的热情都会在失望中慢慢变冷",
    "夜晚的风吹不走压抑",
    "想念不一定要见面,有时候回忆就够了",
    "人这一生,最怕突然想通又突然心痛",
    "我笑着说没关系,其实已经碎了一地",
    "我也想被一个人坚定地喜欢一次",
    "那些没说出口的委屈,后来都变成了沉默",
    "如果有下次,我希望我不是那个主动的人",
    "没人能看见我凌晨三点的样子",
    "我什么都没说,但你什么都懂——那就算了吧",
    "你走之后,我连梦都变得小心翼翼",
    "我假装释怀,其实一想到你还是会红了眼",
    "失去你那天,我学会了笑着说没事",
    "我以为我们能久一点,结果只是以为",
    "你没有挽留,我也没再回头",
    "后来的我,连听情歌都小心翼翼",
    "你是我心口的那颗钉,拔不掉也忘不掉",
    "我删了聊天记录,却删不掉回忆",
    "我好像把所有的温柔都给错了人",
    "你教会了我什么叫“自作多情”",
    "我不怪你离开,只怪我没能让你留下",
    "你是我没说出口的心动,也是我没来得及的遗憾",
    "再见面时,我一定要笑得比现在更好看",
    "我装作无所谓,其实连朋友圈都不敢看",
    "最难过的不是分开,而是从此没资格关心",
    "你走的那天,连天气都没挽留我",
    "我终于学会不打扰,可你早已不在意",
    "爱的时候像救赎,散的时候像坠落",
    "后来我们都没再提起,但都记得",
    "我没放下你,只是学会了不提你"
]


# 基于操作系统的自动配置
# mac下圆角
RADIUS = 30 if sys.platform == "darwin" else 0
# 字体
FONT = "Arial" if sys.platform == "darwin" else "Microsoft Yahei"

class FancyPopup:
    def __init__(self, root, text, size=WINDOWS_SIZE, duration=4000):
        self.root = root
        self.text = text
        self.duration = duration
        self.win = tk.Toplevel(root)
        self.win.overrideredirect(True)
        self.win.attributes("-topmost", True)
        self.win.attributes("-alpha", 0.0)  # 初始透明
        self.bg_color = self.random_color()
        # window下圆角背景无法透明,取消圆角
        self.radius = RADIUS

        # 随机位置
        sw, sh = self.win.winfo_screenwidth(), self.win.winfo_screenheight()
        w, h = size
        self.x = random.randint(-50, sw - w + 50)
        self.y = random.randint(0, sh - h - 90) # -90 为mac下方程序坞的高度,win下可以删去
        self.win.geometry(f"{w}x{h}+{self.x}+{self.y}")

        # 创建圆角画布
        self.canvas = tk.Canvas(self.win, width=w, height=h, highlightthickness=0)
        self.canvas.pack(fill="both", expand=True)
        self.draw_rounded_rect(0, 0, w, h, self.radius, self.bg_color)

        # 添加文本, 自动计算字体大小(单行适配)
        font_size = self.auto_fit_text_size(self.text, w - 60)
        self.text_id = self.canvas.create_text(w / 2, h / 2, text=self.text, fill="white",
            font=(FONT, font_size, "bold"), justify="center", width=w - 60, )

        self.fade_in()

    def auto_fit_text_size(self, text, max_width):
        """根据窗口宽度自动调整字体大小,让文字恰好一行"""
        test_font_size = 32
        test_font = (FONT, test_font_size, "bold")

        temp_id = self.canvas.create_text(0, 0, text=text, font=test_font, anchor="nw")
        bbox = self.canvas.bbox(temp_id)
        text_width = bbox[2] - bbox[0]
        self.canvas.delete(temp_id)

        scale = max_width / text_width
        new_size = max(10, int(test_font_size * scale))
        return min(new_size, 32)  # 限制最大字号,防止太大

    def draw_rounded_rect(self, x1, y1, x2, y2, r, color):
        points = [x1 + r, y1, x1 + r, y1, x2 - r, y1, x2 - r, y1, x2, y1, x2, y1 + r, x2, y1 + r, x2, y2 - r, x2,
                  y2 - r, x2, y2, x2 - r, y2, x2 - r, y2, x1 + r, y2, x1 + r, y2, x1, y2, x1, y2 - r, x1, y2 - r, x1,
                  y1 + r, x1, y1 + r, x1, y1, ]
        self.canvas.create_polygon(points, smooth=True, fill=color, outline=color)

    def random_color(self):
        return f"#{random.randint(30, 220):02x}{random.randint(30, 220):02x}{random.randint(30, 220):02x}"

    def fade_in(self, step=0):
        alpha = step / 20
        if alpha <= 1:
            self.win.attributes("-alpha", alpha)
            self.win.after(30, lambda: self.fade_in(step + 1))
        else:
            self.win.attributes("-alpha", 1.0)

    def fade_out(self, step=0):
        alpha = 1 - step / 20
        if alpha > 0:
            self.win.attributes("-alpha", alpha)
            self.win.after(20, lambda: self.fade_out(step + 1))
        else:
            self.destroy()

    def destroy(self):
        try:
            self.win.destroy()
        except:
            pass


class PopupController:
    def __init__(self, messages, interval):
        self.root = tk.Tk()
        self.root.withdraw()
        self.popups = []
        self.messages = messages
        self.interval = interval
        self.spawn_num = WINDOWS_NUM
        self.finished = False

    def start(self):
        self.spawn_next()
        self.root.mainloop()

    def spawn_next(self):
        if len(self.popups) >= self.spawn_num:
            # 所有弹窗出来后开始消失
            self.finished = True
            self.popups = self.popups[::-1]
            self.root.after(500, self.start_fading, 1)
            return

        msg = self.messages[random.randint(0, len(self.messages) - 1)]
        popup = FancyPopup(self.root, msg)
        print(f"位置:{popup.x:>4}, {popup.y:<4} , 弹窗: {msg}")
        self.popups.append(popup)
        self.root.after(self.interval, self.spawn_next)

    def start_fading(self, count):
        if not self.popups:
            self.root.after(1000, lambda: sleep(1))
            self.root.quit()
            return

        remove_n = min(count, len(self.popups))
        to_remove = self.popups[:remove_n]
        for p in to_remove:
            p.fade_out()

        self.popups = self.popups[remove_n:]
        next_count = min(15, count + random.randint(0, 1))
        self.root.after(150, self.start_fading, next_count)


if __name__ == "__main__":
    try:
        app = PopupController(MESSAGES.copy(), WINDOWS_INTERVAL)
        app.start()
    except KeyboardInterrupt:
        sys.exit(0)

预览图

整体脚本还是比较简单,我对其进行了封装,方便调用和二次开发拓展。核心参数就可以在代码的头部继续修改。