Flet框架问题:Timer类内无法播放音频,触发AttributeError
解决Flet计时器应用中Audio组件播放的AttributeError问题
问题分析
出现'NoneType' object has no attribute 'invoke_method'错误的核心原因有两个:
- 虽然将
self.sound添加到了page.overlay,但未触发页面更新完成组件的初始化绑定,导致Audio组件的底层控制对象未创建。 - 在子线程的
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
相关产品推荐
相关产品推荐

