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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.27 18:39:27