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的刷新,效率极低,尤其是处理大文件时。更优的方式是:
- 在子线程中把所有CSV数据转换成RecycleView需要的字典格式,存入一个临时列表
- 完成后在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
关键修改点说明
解决AssertionError:
- 子线程仅负责数据格式转换,不直接操作RecycleView的
data - 所有UI操作(进度条更新、RecycleView数据替换)通过
Clock.schedule_once在主线程执行,避免线程冲突
- 子线程仅负责数据格式转换,不直接操作RecycleView的
进度条优化:
- 提前设置进度条的
max值为数据总条数,确保进度显示准确 - 用
partial传递当前计数,避免依赖全局变量导致的进度混乱
- 提前设置进度条的
延迟初始化:
ExampleViewer初始化时留空,不在启动时加载数据display_screen的on_pre_enter事件触发首次数据加载,保证用户切换到屏幕时才初始化
数据加载可靠性:
- 使用
csv.reader读取文件,比手动split(',')更能处理复杂CSV格式(如带引号的字段) - 加载新文件时清空旧数据,避免数据累积
- 使用
内容的提问来源于stack exchange,提问作者Llewellyn
相关产品推荐
相关产品推荐

