You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Flet框架问题:Timer类内无法播放音频,触发AttributeError

解决Flet计时器应用中Audio组件播放的AttributeError问题

问题分析

出现'NoneType' object has no attribute 'invoke_method'错误的核心原因有两个:

  1. 虽然将self.sound添加到了page.overlay,但未触发页面更新完成组件的初始化绑定,导致Audio组件的底层控制对象未创建。
  2. 在子线程的update_timer中直接调用self.sound.play(),违反了Flet UI操作必须在主线程执行的规则。

解决方案

方案1:修复Timer类内部的Audio组件初始化与线程安全调用

修改did_mount和timer_end方法,确保组件初始化完成且在主线程执行播放操作:

import flet as ft
import threading
import time
from datetime import timedelta


class Timer(ft.UserControl):
    def __init__(self, name, duration, remove_timer_clicked):
        super().__init__()
        self.name = name
        self.duration = duration
        self.delete_timer = remove_timer_clicked
        self.paused = False

    def did_mount(self):
        self.page.overlay.append(self.sound)
        # 触发页面更新,完成Audio组件初始化绑定
        self.page.update()
        
        self.running = True
        self.th = threading.Thread(
            target=self.update_timer,
            daemon=True)
        self.th.start()

    def will_unmount(self):
        self.running = False

    def update_timer(self):
        while self.running and self.duration:
            if self.paused:
                continue

            self.text_countdown.value = timedelta(seconds=self.duration)
            self.update()
            time.sleep(1)
            self.duration -= 1

        # 切换到主线程执行计时器结束逻辑
        self.page.run_task(self.timer_end)

    def timer_end(self):
        self.text_countdown.value = timedelta(seconds=self.duration)
        self.sound.play()
        self.update()

    def remove_timer_clicked(self, e):
        self.delete_timer(self)

    def toggle_resume_pause(self, e):
        self.paused = not self.paused
        self.button_resume.visible = not self.button_resume.visible
        self.button_pause.visible = not self.button_pause.visible
        self.update()

    def build(self):
        self.sound = ft.Audio(
            src="../Sounds/timer_done.mp3",
            autoplay=False
        )

        self.text_timer_name = ft.Text(
            value=self.name,
            text_align=ft.TextAlign.LEFT,
            size=18,
            width=335
        )

        self.text_countdown = ft.Text(
            text_align=ft.TextAlign.RIGHT,
            size=18,
            width=100
        )

        self.button_pause = ft.IconButton(
            icon=ft.icons.PAUSE_CIRCLE,
            tooltip="Pause this timer"
        )

        self.button_resume = ft.IconButton(
            icon=ft.icons.PLAY_CIRCLE,
            visible=False,
            tooltip="Resume this timer"
        )

        self.button_cancel = ft.IconButton(
            icon=ft.icons.CANCEL,
            icon_color=ft.colors.RED,
            tooltip="Remove this timer"
        )

        self.button_resume.on_click = self.toggle_resume_pause
        self.button_pause.on_click = self.toggle_resume_pause
        self.button_cancel.on_click = self.remove_timer_clicked

        return ft.Row(
            alignment=ft.MainAxisAlignment.CENTER,
            expand=True,
            controls=[
                self.text_timer_name,
                self.text_countdown,
                self.button_resume,
                self.button_pause,
                self.button_cancel,
            ]
        )

方案2:复用全局Audio组件(推荐)

如果多个计时器共用同一段提示音,更高效的方式是在main函数中创建Audio组件,通过构造函数传入Timer类,避免重复创建组件:

# main.py
import flet as ft
from timer import Timer

def main(page: ft.Page):
    # 全局创建Audio组件
    timer_sound = ft.Audio(src="../Sounds/timer_done.mp3", autoplay=False)
    page.overlay.append(timer_sound)
    page.update()

    def add_timer(e):
        # 将全局sound传入Timer实例
        new_timer = Timer("测试计时器", 10, remove_timer, timer_sound)
        page.add(new_timer)

    def remove_timer(timer: Timer):
        page.remove(timer)
        page.update()

    page.add(ft.ElevatedButton("添加计时器", on_click=add_timer))

if __name__ == "__main__":
    ft.app(target=main)

修改Timer类的构造函数,接收外部传入的sound参数:

# timer.py
import flet as ft
import threading
import time
from datetime import timedelta

class Timer(ft.UserControl):
    def __init__(self, name, duration, remove_timer_clicked, sound):
        super().__init__()
        self.name = name
        self.duration = duration
        self.delete_timer = remove_timer_clicked
        self.paused = False
        self.sound = sound  # 接收外部Audio组件

    def did_mount(self):
        self.running = True
        self.th = threading.Thread(
            target=self.update_timer,
            daemon=True)
        self.th.start()

    def update_timer(self):
        while self.running and self.duration:
            if self.paused:
                continue

            self.text_countdown.value = timedelta(seconds=self.duration)
            self.update()
            time.sleep(1)
            self.duration -= 1

        self.page.run_task(self.timer_end)

    def timer_end(self):
        self.text_countdown.value = timedelta(seconds=self.duration)
        self.sound.play()
        self.update()

    # 其余方法保持不变,build中不再创建self.sound
    def build(self):
        self.text_timer_name = ft.Text(
            value=self.name,
            text_align=ft.TextAlign.LEFT,
            size=18,
            width=335
        )

        self.text_countdown = ft.Text(
            text_align=ft.TextAlign.RIGHT,
            size=18,
            width=100
        )

        self.button_pause = ft.IconButton(
            icon=ft.icons.PAUSE_CIRCLE,
            tooltip="Pause this timer"
        )

        self.button_resume = ft.IconButton(
            icon=ft.icons.PLAY_CIRCLE,
            visible=False,
            tooltip="Resume this timer"
        )

        self.button_cancel = ft.IconButton(
            icon=ft.icons.CANCEL,
            icon_color=ft.colors.RED,
            tooltip="Remove this timer"
        )

        self.button_resume.on_click = self.toggle_resume_pause
        self.button_pause.on_click = self.toggle_resume_pause
        self.button_cancel.on_click = self.remove_timer_clicked

        return ft.Row(
            alignment=ft.MainAxisAlignment.CENTER,
            expand=True,
            controls=[
                self.text_timer_name,
                self.text_countdown,
                self.button_resume,
                self.button_pause,
                self.button_cancel,
            ]
        )

关键说明

  • page.run_task()用于将UI操作切换到主线程执行,避免子线程直接操作Flet组件引发的异常。
  • 添加组件到overlay后必须调用page.update(),确保组件完成初始化并绑定到底层控制对象。

内容的提问来源于stack exchange,提问作者user22547452

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.08 22:47:36