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

Kivy开发:如何在更新RecycleView表格数据时添加加载进度条?

解决Kivy RecycleView更新的AssertionError及进度条、初始化优化问题

先来说说你遇到的AssertionError:这个错误是因为你在子线程中直接往RecycleView的data列表里逐个添加元素,而Kivy的UI组件(包括RecycleView)不是线程安全的。当子线程在append数据的同时,UI线程可能正在执行RecycleView的刷新逻辑,导致内部维护的布局选项(opts)数量和data长度不匹配,触发了那个断言。

接下来针对你的三个核心需求逐一解决:


1. 给RecycleView更新添加有效加载进度条

要实现这个,需要做到:

  • 子线程负责准备数据格式(而不是直接操作RecycleView的data)
  • 进度条的max值要提前设置为数据的总条数
  • 用Clock.schedule_once在UI线程中更新进度条和RecycleView的data(避免线程安全问题)

具体逻辑:

  • 弹出进度条弹窗时,先把进度条最大值设为数据总条数
  • 子线程中遍历数据生成RecycleView需要的字典格式,每处理一条就调度更新进度条
  • 所有数据处理完成后,一次性把生成的列表赋值给RecycleView的data(而非逐个append)

2. 更优的RecycleView数据更新方案

逐个往self.data里append元素会频繁触发RecycleView的刷新,效率极低,尤其是处理大文件时。更优的方式是:

  1. 在子线程中把所有CSV数据转换成RecycleView需要的字典格式,存入一个临时列表
  2. 完成后在UI线程中直接替换RecycleView.data为这个临时列表(一次性刷新)
    这样既避免了线程安全问题,也减少了UI刷新的次数,大幅提升性能。

3. 延迟初始化第二个表格屏幕

推荐通过屏幕预进入事件实现延迟初始化,灵活且用户体验更好:

  • 用户第一次切换到第二个屏幕时,再初始化RecycleView的data
  • 既避免了初始状态下第二个屏幕为空的问题,也不会影响用户自由切换屏幕的操作

修改后的完整代码

Python代码

from kivy.app import App
from kivy.uix.button import Button
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.floatlayout import FloatLayout
from kivy.uix.screenmanager import Screen, ScreenManager
from kivy.uix.popup import Popup
from kivy.uix.recycleview import RecycleView
from kivy.properties import ObjectProperty
from kivy.clock import Clock
from functools import partial
import threading
import csv

new_d = list()

def refreshview():
    rvobj = App.get_running_app().root.get_screen('scr2').ids.rv_list
    if len(new_d) != 0:
        pol = main_load()
        # 提前设置进度条最大值为数据总条数
        pol.cnt1.max = len(new_d)
        pol.open()
        # 传递弹窗和RecycleView对象到子线程
        threading.Thread(target=rvobj.prepare_data, args=(pol,)).start()

class Filechooser(BoxLayout):
    lines = 0
    file_name = ''
    def select(self, filename):
        try:
            self.file_name = filename[0]
        except:
            pass
    def load(self):
        pop = progress_d()
        pop.open()
        threading.Thread(target=self.load_data, args=(pop,)).start()
    def load_data(self,obj):
        global new_d
        new_d.clear()  # 加载新文件前清空旧数据
        with open(self.file_name) as f:
            self.lines = sum(1 for _ in f)
            obj.cnt.max = self.lines
        with open(self.file_name,'r') as f:
            reader = csv.reader(f)  # 用csv模块读取更可靠
            for idx, line in enumerate(reader):
                new_d.append(line)
                # 调度进度条更新,传递当前计数
                Clock.schedule_once(partial(obj.update_progress, idx+1))

class ExampleViewer(RecycleView):
    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        # 初始状态留空,不加载数据
        self.data = []
    
    def butt(self, x):
        print('button', x, 'pressed')
    
    def prepare_data(self, obj):
        # 子线程中准备RecycleView需要的数据格式
        temp_data = []
        for idx, x in enumerate(new_d):
            d = {'text': str(x),'color': (0,0,0,1) ,'on_press': partial(self.butt, x)}
            temp_data.append(d)
            # 调度进度条更新
            Clock.schedule_once(partial(obj.update_progress, idx+1))
        # 数据准备完成后,在UI线程一次性更新RecycleView
        Clock.schedule_once(partial(self.update_recycle_data, temp_data))
    
    def update_recycle_data(self, temp_data, dt):
        # 一次性替换data,触发单次刷新
        self.data = temp_data

class progress_d(Popup):
    cnt = ObjectProperty(None)
    def update_progress(self, current, dt):
        self.cnt.value = current
        if current >= self.cnt.max:
            self.dismiss()

class custPop_start(Popup):
    pass

class main_load(Popup):
    cnt1 = ObjectProperty(None)
    def update_progress(self, current, dt):
        self.cnt1.value = current
        if current >= self.cnt1.max:
            self.dismiss()

class load_screen(Screen):
    def open_popup(self):
        the_popup = custPop_start()
        the_popup.open()
    def refresh(self):
        refreshview()

class display_screen(Screen):
    def on_pre_enter(self, *args):
        # 用户第一次进入屏幕时,自动加载数据(如果已有数据)
        rvobj = self.ids.rv_list
        if len(new_d) > 0 and len(rvobj.data) == 0:
            refreshview()

class SampleApps(App):
    def build(self):
        root = ScreenManager()
        root.add_widget(load_screen(name='scr1'))
        root.add_widget(display_screen(name='scr2'))
        return root

if __name__=='__main__':
    l = SampleApps()
    l.run()

KV文件代码

<display_screen>:
    id: scr2
    orientation: 'vertical'
    FloatLayout:
        pos_hint: {'center_x':0.3,'top':1}
        size_hint: 0.6,0.5
        Label:
            text: 'DATA'
            size_hint: 1,0.1
            pos_hint: {'center_x':0.5,'top':1}
        ExampleViewer:
            id: rv_list
            size_hint: 1,0.9
            pos_hint: {'x':0,'y':0}
<load_screen>:
    id: scr1
    BoxLayout:
        orientation: 'vertical'
        size_hint: 0.3,0.4
        Label:
            text: "Read_file"
            size_hint: 1, 0.1
        Button:
            size_hint: 1,0.3
            back_color: 1,0,0,1
            text: "UPLOAD"
            on_press: root.open_popup()
        Button:
            size_hint: 1,0.3
            back_color: 1,0,0,1
            text: "ANALYSE"
            on_press: root.refresh()
        Button:
            size_hint: 1,0.3
            back_color: 1,0,0,1
            text: "Screen2"
            on_press: root.manager.current = 'scr2'
<custPop_start>:
    size_hint: 0.4,0.4
    pos_hint: {'x':0.3,'y':0.3}  # 调整位置避免超出屏幕
    auto_dismiss: False
    title: "the popup"
    BoxLayout:
        orientation: 'vertical'
        Filechooser:
            id: pie
            size_hint: 1, 0.8
        Button:
            size_hint: 1, 0.1
            text: 'LOAD'
            on_press: pie.load()
            on_release: root.dismiss()
        Button:
            size_hint: 1, 0.1
            text: 'CANCEL'
            on_press: root.dismiss()
<ExampleViewer>:
    id: btn2
    viewclass: 'Button'
    RecycleBoxLayout:
        color:(0, 0, 0, 1)
        default_size: None, dp(35)
        default_size_hint: 1, None
        size_hint_y: None
        height: self.minimum_height
        orientation: 'vertical'
<Filechooser>:
    id: Filechooser
    orientation: 'vertical'
    BoxLayout:
        orientation: 'vertical'
        FileChooserListView:
            id: file1
            on_selection: Filechooser.select(file1.selection)
<progress_d>:
    cnt : my_progress
    size_hint: 0.3,0.3
    auto_dismiss: False
    BoxLayout:
        orientation: 'vertical'
        ProgressBar:
            id: my_progress
            min: 0
            max: 1
            pos_hint: {'x':0.1}
            size_hint_x: 0.8
<main_load>:
    cnt1 : my_progress1
    size_hint: 0.5,0.3
    auto_dismiss: False
    BoxLayout:
        orientation: 'vertical'
        ProgressBar:
            id: my_progress1
            min: 0
            max: 1
            pos_hint: {'x':0.1}
            size_hint_x: 0.8

关键修改点说明

  1. 解决AssertionError:

    • 子线程仅负责数据格式转换,不直接操作RecycleView的data
    • 所有UI操作(进度条更新、RecycleView数据替换)通过Clock.schedule_once在主线程执行,避免线程冲突
  2. 进度条优化:

    • 提前设置进度条的max值为数据总条数,确保进度显示准确
    • 用partial传递当前计数,避免依赖全局变量导致的进度混乱
  3. 延迟初始化:

    • ExampleViewer初始化时留空,不在启动时加载数据
    • display_screen的on_pre_enter事件触发首次数据加载,保证用户切换到屏幕时才初始化
  4. 数据加载可靠性:

    • 使用csv.reader读取文件,比手动split(',')更能处理复杂CSV格式(如带引号的字段)
    • 加载新文件时清空旧数据,避免数据累积

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.04.29 09:54:07