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

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'

问题分析与修正方案

核心问题点

  1. 构造函数名称错误:Videos类的构造函数写为__而非__init__,导致绑定代码完全没执行。
  2. 绑定时机错误:直接调用MainApp.get_running_app()绑定事件,可能在App实例未完全初始化时触发报错。
  3. 回调函数参数缺失:绑定position属性时,回调函数需要接收实例对象和新属性值两个参数,原函数无参数会触发报错。
  4. 事件名称混淆: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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.02 12:40:21