KivyMD提示.kv多次加载 待办功能整合后非预期行为排查
问题根因
运行时出现.kv is loaded multiples times警告及后续异常行为,核心诱因有以下几点:
- Kivy框架默认存在kv自动加载机制:继承自
App/MDApp的应用类启动时,会自动加载同目录下、与类名匹配(去除App后缀后全小写)的kv文件。当前应用类名为MainApp,框架会自动加载main.kv,但build()方法中又手动调用Builder.load_file("main.kv")重复加载,直接触发警告。重复加载会导致kv中定义的自定义组件模板(DialogContent、ListItemWithCheckbox等)重复注册,引发布局错乱、控件重复渲染等非预期问题。 - 控件id访问逻辑错误:
on_start、add_task方法中直接通过self.root.ids.container访问待办列表容器,但self.root指向的是ScreenManager实例,containerid实际定义在名为tasks的子Screen下,直接访问会找不到目标控件,导致待办任务加载、新增失败。 - 数据库逻辑存在缺陷:MySQL连接与游标被定义为App类属性,会在App实例化前就创建连接,生命周期与应用不绑定,易出现连接失效、游标冲突问题;同时SQL语句直接用f字符串拼接用户输入,存在严重SQL注入风险。
- 待办项状态切换逻辑错误:取消任务完成状态时,直接将数据库方法的返回值转为字符串赋值给任务文本,会导致任务内容被替换为数据库操作的返回值(如None、影响行数),显示异常。
修复方案
1. 解决kv重复加载问题
二选一即可:
- 方案一(改动最小):删除
build()方法中screen_manager.add_widget(Builder.load_file("main.kv"))这行代码,让框架按默认规则自动加载main.kv即可。 - 方案二(手动控制所有kv加载):重写
load_kv方法关闭自动加载逻辑,保留手动加载的代码:
class MainApp(MDApp): def load_kv(self, *args, **kwargs): return False def build(self): self.screen_manager = ScreenManager() self.screen_manager.add_widget(Builder.load_file("login.kv")) self.screen_manager.add_widget(Builder.load_file("register.kv")) self.tasks_screen = Builder.load_file("main.kv") self.screen_manager.add_widget(self.tasks_screen) return self.screen_manager
*注意:这里把全局screen_manager改为App实例属性,同时单独保存tasks_screen的引用,方便后续访问其内部id。
2. 修复控件id访问错误
所有原来用self.root.ids.container的位置,全部替换为self.tasks_screen.ids.container,比如:
def on_start(self): try: completed_tasks, uncomplete_tasks = db.get_tasks() if uncomplete_tasks: for task in uncomplete_tasks: add_task = ListItemWithCheckbox(pk=task[0], text=task[1], secondary_text=task[2]) self.tasks_screen.ids.container.add_widget(add_task) if completed_tasks: for task in completed_tasks: add_task = ListItemWithCheckbox(pk=task[0], text=f'[s]{task[1]}[/s]', secondary_text=task[2]) add_task.ids.check.active = True self.tasks_screen.ids.container.add_widget(add_task) except Exception as e: print(e) def add_task(self, task, task_date): created_task = db.create_task(task.text, task_date) self.tasks_screen.ids['container'].add_widget( ListItemWithCheckbox(pk=created_task[0], text=f'[b]{created_task[1]}[/b]', secondary_text=created_task[2]) ) task.text = ''
所有原来用全局变量screen_manager切换屏幕的地方,替换为self.screen_manager.current = "xxx",去掉global声明。
3. 修复数据库逻辑
把MySQL连接初始化移到build方法中,与应用生命周期绑定,同时所有SQL语句改用参数化查询,避免注入:
def build(self): # 初始化数据库连接 self.database = mysql.connector.connect( host='localhost', database='loginform', user='root', password='****' ) self.cursor = self.database.cursor() # 原有screen_manager初始化逻辑不变 self.screen_manager = ScreenManager() self.screen_manager.add_widget(Builder.load_file("login.kv")) self.screen_manager.add_widget(Builder.load_file("register.kv")) self.tasks_screen = Builder.load_file("main.kv") self.screen_manager.add_widget(self.tasks_screen) return self.screen_manager # 注册逻辑参数化查询 def send_data(self, user, password): self.cursor.execute( "INSERT INTO example2 (user, password) VALUES (%s, %s)", (user.text, password.text) ) self.database.commit() # 登录逻辑参数化查询 def receive_data(self, user, password): if not user.text: return self.cursor.execute("SELECT password FROM example2 WHERE user = %s", (user.text,)) res = self.cursor.fetchone() if not res: open_regist = MDFlatButton(text="Register", on_release=self.open_regist) close_button = MDFlatButton(text='Close', on_release=self.close_dialg) self.dialog = MDDialog(title='Incorrect User', text='There are no such user', buttons=[close_button, open_regist]) self.dialog.open() return if password.text == res[0]: open_button = MDFlatButton(text='Open the App', on_release=self.open_app) close_button = MDFlatButton(text='Close', on_release=self.close_dialg) self.dialog = MDDialog(text='Welcome to the *****', buttons=[close_button, open_button]) self.dialog.open() else: try_button = MDFlatButton(text='Try Again', on_release=self.close_dialg) self.dialog = MDDialog(title='Incorrect Password', text='You entered wrong password please try again', buttons=[try_button]) self.dialog.open()
4. 修复待办项状态切换逻辑
修改mark方法,避免把数据库返回值设为任务文本,同时处理文本里的标记标签:
def mark(self, check, the_list_item): # 先去除文本里已有的格式标签,拿到原始任务内容 raw_text = the_list_item.text.replace('[s]', '').replace('[/s]', '').replace('[b]', '').replace('[/b]', '') if check.active: the_list_item.text = f'[s]{raw_text}[/s]' db.mark_task_as_complete(the_list_item.pk) else: the_list_item.text = raw_text db.mark_task_as_incomplete(the_list_item.pk)
额外优化建议
- 密码不要明文存储在数据库中,建议使用bcrypt等库做哈希加盐处理后再存储,避免数据泄露。
- 数据库操作增加异常捕获,连接失败、SQL执行错误时给用户弹出明确提示,不要直接静默打印错误。
- 弹窗关闭后可根据需要重置实例状态,避免长期持有占用内存。
- 数据库连接增加心跳检测或重连逻辑,避免连接超时后操作失败。
内容的提问来源于stack exchange,提问作者Abylaikhan Zhumanov
相关产品推荐
相关产品推荐

