Kivy跨类绑定事件与属性异常:媒体播放器视频状态监听失败
Kivy视频播放器绑定事件问题:监听加载完成与播放位置
我正在开发一款通过I2C定时发送指令的媒体播放器,需要监听视频加载完成事件以及当前播放位置(时间)。我推测自己没搞懂Kivy的bind机制,我理解的是bind(*event/property* = function),也就是属性变化或事件触发时执行对应函数,但试了多种绑定方式,要么报错要么没效果。
原Python主文件代码
from kivy.app import App from kivy.clock import Clock from kivy.uix.video import Video from kivy.uix.button import Button from kivy.uix.label import Label from kivy.lang.builder import Builder from kivy.uix.gridlayout import GridLayout from kivy.properties import Property from kivy.properties import ObjectProperty import tkinter from tkinter import filedialog import csv import numpy as np import time import os # import busio # import board # import adafruit_mcp4725 # # os.environ[\"BLINKA_FT232H\"] # i2c = busio.I2C(board.SCL, board.SDA) # dac = adafruit_mcp4725.MCP4725(i2c) Builder.load_file(\"Main.kv\") class Videos(Video): def __(self, **kwargs): super(Videos, self).__init__(**kwargs) self.bind(on_loaded=MainApp.get_running_app().loading) #self.bind(position=MainApp.get_running_app().slider_position) pass class RootWindow(GridLayout): default_time = '0:00 / 0:00' tkinter.Tk().withdraw() interval_event = None def __init__(self, **kwargs): super().__init__(**kwargs) self.values = [None] self.times = [None] def start_checking(self): self.interval_event = Clock.schedule_interval(self.check_time, .9) self.interval_event() def stop_checking(self): if self.interval_event is not None: self.interval_event.cancel() def loading(self): print('loaded') def slider_position(self): print('position') def load_video(self): file_input = filedialog.askopenfilename(title='Video') if file_input != '': self.ids.video.source = file_input # Videos.video._do_video_load(self.ids.video) def load_data(self): csv_file_path = filedialog.askopenfilename(title='Data') with open(csv_file_path) as import_data: reader = csv.reader(import_data) times_reader = next(reader) times = [] for time in times_reader: times.append(time) values_reader = next(reader) values = [] for value in values_reader: values.append(value) self.times = times self.values = values def play_pause(self): print(self.ids.video.loaded) if self.ids.video.state == 'play': self.ids.video.state = 'pause' self.ids.play_pause_img.source = \"play_icon.png\" else: self.ids.video.state = 'play' self.ids.play_pause_img.source = \"pause_icon.png\" # Clock.schedule_interval(self.check_time, .9) def check_time(self, dt): print(self.ids.video.state) # if RootWindow().ids.video.state == 'stop' or RootWindow().ids.video.state == 'pause': # # dac.raw_value = 0 # print('0') # else: # print('playing') # for idx, row in enumerate(times): # if row == position: # print(row) # send = (4095 / 80) * values[idx] # dac.raw_value = send class MainApp(App): def build(self): return RootWindow() MainApp().run()
原KV文件代码
<RootWindow> video:video rows: 2 GridLayout: rows: 1 size_hint: 1, 1 pos_hint: {'center_x': 0.5, 'center_y': 0.5} Videos: id: video #source: 'RepMot.mp4' #state: 'play' ##on_load: root.start_checking on_eos: root.stop_checking GridLayout: rows: 1 size_hint: 1, None height: 35 row_force_default: True row_default_height: 35 pos_hint: {'y': 0, 'center_x': 0.5} Button: id: vid #text: 'vid' background_color: 0,0,0,0 size_hint: None, 1 width: 35 on_press: root.load_video() Image: id: vid_img source: \"vid_icon.png\" pos_hint: {'center_x': 0.5, 'center_y': 0.5} size: 35,35 Button: id: data #text: 'data' background_color: 0,0,0,0 size_hint: None, 1 width: 35 on_press: root.load_data() Image: id: data_img source: \"data_icon.png\" center_x: self.parent.center_x center_y: self.parent.center_y size: 35,35 Button: id: play_pause #text: 'play' background_color: 0,0,0,0 size_hint: None, 1 width: 35 on_press: root.play_pause() Image: id: play_pause_img source: \"play_icon.png\" center_x: self.parent.center_x center_y: self.parent.center_y size: 35,35 Button: id: vol #text: 'vol' background_color: 0,0,0,0 size_hint: None, 1 width: 35 Image: id: volume_img source: \"volume_icon.png\" center_x: self.parent.center_x center_y: self.parent.center_y size: 35,35 Slider: size_hint_x: 1 id: slider background_width: '26sp' min: 0 max: 100 Label: id: timestamp size_hint: None, 1 pos_hint: {'right': 1} width: 80 text: '0:00 / 0:00'
问题分析与修正方案
核心问题点
- 构造函数名称错误:
Videos类的构造函数写为__而非__init__,导致绑定代码完全没执行。 - 绑定时机错误:直接调用
MainApp.get_running_app()绑定事件,可能在App实例未完全初始化时触发报错。 - 回调函数参数缺失:绑定
position属性时,回调函数需要接收实例对象和新属性值两个参数,原函数无参数会触发报错。 - 事件名称混淆:Video组件的加载完成事件是
on_load而非on_loaded,监听错误事件导致无响应。
修正后Python代码
from kivy.app import App from kivy.clock import Clock from kivy.uix.video import Video from kivy.uix.button import Button from kivy.uix.label import Label from kivy.lang.builder import Builder from kivy.uix.gridlayout import GridLayout from kivy.properties import Property from kivy.properties import ObjectProperty import tkinter from tkinter import filedialog import csv import numpy as np import time import os # import busio # import board # import adafruit_mcp4725 # # os.environ["BLINKA_FT232H"] = "1" # i2c = busio.I2C(board.SCL, board.SDA) # dac = adafruit_mcp4725.MCP4725(i2c) Builder.load_file("Main.kv") class Videos(Video): def __init__(self, **kwargs): super(Videos, self).__init__(**kwargs) class RootWindow(GridLayout): default_time = '0:00 / 0:00' tkinter.Tk().withdraw() interval_event = None def __init__(self, **kwargs): super().__init__(**kwargs) self.values = [None] self.times = [None] # 直接绑定到当前容器的方法,避免依赖App实例 self.ids.video.bind(on_load=self.loading) self.ids.video.bind(position=self.slider_position) def start_checking(self): self.interval_event = Clock.schedule_interval(self.check_time, .9) self.interval_event() def stop_checking(self): if self.interval_event is not None: self.interval_event.cancel() def loading(self, instance): print('视频加载完成') self.start_checking() def slider_position(self, instance, new_pos): print(f'当前播放位置: {new_pos}') # 格式化时间并更新标签 current_time = self.format_time(new_pos) total_time = self.format_time(instance.duration) self.ids.timestamp.text = f'{current_time} / {total_time}' def format_time(self, seconds): if seconds is None: return '0:00' mins = int(seconds // 60) secs = int(seconds % 60) return f'{mins}:{secs:02d}' def load_video(self): file_input = filedialog.askopenfilename(title='选择视频') if file_input != '': self.ids.video.source = file_input def load_data(self): csv_file_path = filedialog.askopenfilename(title='选择数据文件') with open(csv_file_path) as import_data: reader = csv.reader(import_data) times_reader = next(reader) self.times = [float(t) for t in times_reader] values_reader = next(reader) self.values = [float(v) for v in values_reader] def play_pause(self): print(self.ids.video.loaded) if self.ids.video.state == 'play': self.ids.video.state = 'pause' self.ids.play_pause_img.source = "play_icon.png" else: self.ids.video.state = 'play' self.ids.play_pause_img.source = "pause_icon.png" def check_time(self, dt): print(f'当前播放状态: {self.ids.video.state}') current_pos = self.ids.video.position if self.ids.video.state == 'play' and current_pos is not None: # 匹配CSV时间点,允许0.1秒误差 for idx, t in enumerate(self.times): if abs(current_pos - t) < 0.1: print(f'匹配到时间点 {t}, 发送值 {self.values[idx]}') # dac.raw_value = int((4095 / 80) * self.values[idx]) class MainApp(App): def build(self): return RootWindow() if __name__ == "__main__": MainApp().run()
修正后KV代码
<RootWindow> video: video rows: 2 GridLayout: rows: 1 size_hint: 1, 1 pos_hint: {'center_x': 0.5, 'center_y': 0.5} Videos: id: video # source: 'RepMot.mp4' # state: 'play' on_eos: root.stop_checking GridLayout: rows: 1 size_hint: 1, None height: 35 row_force_default: True row_default_height: 35 pos_hint: {'y': 0, 'center_x': 0.5} Button: id: vid background_color: 0,0,0,0 size_hint: None, 1 width: 35 on_press: root.load_video() Image: id: vid_img source: "vid_icon.png" pos_hint: {'center_x': 0.5, 'center_y': 0.5} size: 35,35 Button: id: data background_color: 0,0,0,0 size_hint: None, 1 width: 35 on_press: root.load_data() Image: id: data_img source: "data_icon.png" center_x: self.parent.center_x center_y: self.parent.center_y size: 35,35 Button: id: play_pause background_color: 0,0,0,0 size_hint: None, 1 width: 35 on_press: root.play_pause() Image: id: play_pause_img source: "play_icon.png" center_x: self.parent.center_x center_y: self.parent.center_y size: 35,35 Button: id: vol background_color: 0,0,0,0 size_hint: None, 1 width: 35 Image: id: volume_img source: "volume_icon.png" center_x: self.parent.center_x center_y: self.parent.center_y size: 35,35 Slider: size_hint_x: 1 id: slider background_width: '26sp' min: 0 max: 100 # 双向绑定滑块与视频位置 value: video.position / video.duration * 100 if video.duration else 0 on_value: video.seek(self.value / 100) if video.duration else None Label: id: timestamp size_hint: None, 1 pos_hint: {'right': 1} width: 80 text: root.default_time
关键优化说明
- 修复
Videos类构造函数名称,确保绑定逻辑执行。 - 将事件绑定移至
RootWindow的__init__方法,直接通过self.ids.video绑定到容器方法,避免App实例依赖问题。 - 给
slider_position回调添加必要参数,符合Kivy绑定规则。 - 新增时间格式化方法,自动更新播放时长标签。
- 实现滑块与视频位置的双向绑定,支持拖动跳转。
- 优化时间点匹配逻辑,添加误差容忍,适配实际播放场景。
内容的提问来源于stack exchange,提问作者Starb3an
相关产品推荐
相关产品推荐

