从 Kivy 2.x.x 迁移至 Kivy 3.x.x

介绍

Kivy 3.x.x 相较于 Kivy 2.x.x 引入了几项变更与改进。本指南将帮助您将现有的 Kivy 2.x.x 代码库迁移至 Kivy 3.x.x。

重命名的模块和环境变量

从kivy.core.audio迁移至kivy.core.audio_output

在Kivy 3.x.x中,kivy.core.audio`模块已更名为`kivy.core.audio_output

导入语句变更

要迁移你的代码,你需要更新代码库中的导入语句。例如,如果你的代码中有以下导入语句:

from kivy.core.audio import SoundLoader

你需要将其更新为:

from kivy.core.audio_output import SoundLoader

环境变量变更

环境变量也已从 KIVY_AUDIO 更名为 KIVY_AUDIO_OUTPUT

如果你之前使用 KIVY_AUDIO 环境变量来指定音频提供者的偏好,你需要将其更新为 KIVY_AUDIO_OUTPUT。例如:

在导入Kivy之前,在Python中:

import os

# Kivy 2.x.x
os.environ['KIVY_AUDIO'] = 'sdl3,gstplayer'
import kivy

# Kivy 3.x.x
os.environ['KIVY_AUDIO_OUTPUT'] = 'sdl3,gstplayer'
import kivy

移除项

从 `kivy.uix.video.Video` 和 `kivy.uix.videoplayer.VideoPlayer` 中移除 `.play` 属性

在Kivy 3.x.x中,`.play`属性已从`kivy.uix.video.Video`和`kivy.uix.videoplayer.VideoPlayer`类中移除。

要迁移您的代码,您需要更新代码库中对 .play 属性的引用。例如,如果您的 Kivy 2.x.x 代码库中有以下代码:

video = Video(source='video.mp4')

# Play the video
video.play = True

# Stop the video
video.play = False

你需要将其更新为:

video = Video(source='video.mp4')

# Play the video
video.state = 'play'

# Stop the video
video.state = 'stop'

# Pause the video
video.state = 'pause'

从`kivy.uix.textinput.TextInput`中移除`padding_x`和`padding_y`属性

在 Kivy 3.x.x 中,padding_xpadding_y 属性已从 kivy.uix.textinput.TextInput 类中**移除**。取而代之的是,内边距现在通过统一的 padding 属性进行管理。

要更新你的代码,请将 padding_xpadding_y 的实例替换为 padding 属性。

padding 属性接受一个值列表,从而允许更灵活的填充配置:

  • [horizontal, vertical] — 例如,[10, 10]

  • [padding_left, padding_top, padding_right, padding_bottom] — 例如,[10, 5, 10, 5]

关于如何使用 padding 属性的更多细节,请参阅相关文档。

从`kivy.uix.filechooser.FileChooserController`中移除`file_encodings`属性

在Kivy 3.x.x中,`file_encodings`属性已从`kivy.uix.filechooser.FileChooserController`类中移除。

file_encodings 属性已被弃用,仅为向后兼容而保留,但该属性已被忽略,不再在内部使用。

要迁移您的代码,只需移除代码库中对 file_encodings 属性的所有引用即可。

移除已弃用的 `on_dropfile` 窗口事件名称

在Kivy 3.x.x中,先前已弃用的`on_dropfile`事件名称已被移除。请改用`on_drop_file`。

该事件在Kivy 2.1.0中已重命名,因此任何仍绑定到`on_dropfile`的兼容性代码现在都需要更新。

# Kivy 2.x.x (legacy/deprecated name)
from kivy.core.window import Window

def handle_drop(window, filename):
    print(filename)

Window.bind(on_dropfile=handle_drop)

# Kivy 3.x.x
from kivy.core.window import Window

def handle_drop(window, filename, x, y, *args):
    print(filename, x, y)

Window.bind(on_drop_file=handle_drop)

如果你直接分发或覆盖该事件,还需将任何 on_dropfile 方法的实现重命名为 on_drop_file

移除 Kv 语言模板功能

在Kivy 3.x.x中,已弃用的Kivy语言模板功能(自1.0.5版本引入,1.7.0版本弃用)已被移除。以下内容已不再存在:

  • [Name@Base]: Kv 语言模板语法(任何包含 [...]: 选择器的 kv 文件现在在加载时会引发 ParserException)。

  • Builder.template(name, **ctx) 方法和 Builder.templates 字典。

  • Factory.is_template() 以及 Factory.register()is_template= 关键字参数。

迁移到*动态类*(<Name@Base>:)。自Kivy 1.7.0起,动态类在很大程度上取代了模板,并支持常规的Kivy属性、绑定和继承。

在 `.kv` 文件中迁移模板

# Kivy 2.x.x
[IconItem@BoxLayout]:
    Image:
        source: ctx.image
    Label:
        text: ctx.title

# Kivy 3.x.x
<IconItem@BoxLayout>:
    image: ''
    title: ''
    Image:
        source: root.image
    Label:
        text: root.title

注意这两处变化:[...]: 变为 <...>:,且 ctx.foo 引用变为针对规则自身声明的属性的 root.foo 引用。

迁移 `Builder.template(...)` 实例化

# Kivy 2.x.x
from kivy.lang import Builder
icon = Builder.template('IconItem', title='Hello', image='myimage.png')

# Kivy 3.x.x
from kivy.factory import Factory
icon = Factory.IconItem()
icon.title = 'Hello'
icon.image = 'myimage.png'

由于动态类属性是由规则添加到控件上的(而非在类中声明),因此在``__init__``处理其kwargs时,这些属性尚不存在——所以应在构造后通过``setattr``(或属性赋值)传递值,而不是作为构造函数kwargs。若需要构造函数kwargs支持,请将控件定义为带有显式:class:`~kivy.properties.Property`声明的常规Python类。

AccordionItem:`title_template` 和 `title_args` 已被 `title_class` 取代

: AccordionItem 控件此前使用 Kv 语言模板功能,通过 ``title_template``(字符串)和 ``title_args``(字典)属性来渲染其标题栏。这两个属性现已被移除。

替换为 title_class。它接受一个类对象或可通过工厂解析的字符串。该类使用两个关键字参数实例化:titleitem

要自定义标题部件的外观,可以继承 AccordionItemTitle 类(或编写任何接受 titleitem 关键字参数的部件),并通过 title_class 传递它。

参见

title_class 文档。

ButtonBehavior

移除 `state`、`min_state_time`、`last_touch` 属性和 `trigger_action()` 方法

在Kivy 3.x.x中,ButtonBehavior`类已被显著简化和改进。`state OptionProperty、min_state_time NumericProperty和`last_touch` ObjectProperty已被**移除**,同时`trigger_action()`方法也被删除。现在使用一个更简单的只读`pressed` BooleanProperty来表示按钮的状态。

事件签名变更

on_presson_releaseon_cancel`(新事件)事件现在接收一个 `touch 参数,其中包含触发该事件的 MotionEvent 对象。

# Kivy 2.x.x
class MyButton(ButtonBehavior, Label):
    def on_press(self):
        print("Button pressed")

    def on_release(self):
        print("Button released")

# Kivy 3.x.x
class MyButton(ButtonBehavior, Label):
    def on_press(self, touch):
        print(f"Button pressed at {touch.pos}")

    def on_release(self, touch):
        print(f"Button released at {touch.pos}")

    def on_cancel(self, touch):  # NEW event
        print(f"Button cancelled at {touch.pos}")

在绑定这些事件时,回调函数会接收到两个参数:控件实例和触摸事件。

# Kivy 2.x.x
def on_press_callback(instance):
    print(f"{instance} was pressed")

button.bind(on_press=on_press_callback)

# Kivy 3.x.x
def on_press_callback(instance, touch):
    print(f"{instance} was pressed at {touch.pos}")

button.bind(on_press=on_press_callback)
class MyButton(ButtonBehavior, Label):
    def on_press(self, touch):
        # Store press time for duration calculation
        self.press_time = touch.time_start

    def on_release(self, touch):
        # Calculate how long the button was pressed
        duration = touch.time_end - self.press_time
        print(f"Button pressed for {duration:.2f} seconds")

        # Access touch position
        print(f"Released at ({touch.x}, {touch.y})")

多点触控的触摸参数行为

在多触点场景中:

  • on_press:接收触发按下事件的**首次触摸**。

  • on_release:接收被释放的**最后一个触摸**。

  • on_cancel:接收**最后一个触摸**,该触摸移出了边界。

class MyButton(ButtonBehavior, Label):
    def on_press(self, touch):
        # This is the first touch
        print(f"First touch ID: {touch.id}")

    def on_release(self, touch):
        # This is the last touch being released
        print(f"Last touch ID: {touch.id}")

从 `state` 迁移到 `pressed`

state 属性(可能为 'normal''down')已被替换为布尔类型的 pressed 属性。请注意,与旧版可直接设置的 state 不同,pressed 是**只读**的(一个 AliasProperty)。

# Kivy 2.x.x
if button.state == 'down':
    print("Button is pressed")

button.state = 'down'  # Could set state directly

# Kivy 3.x.x
if button.pressed:
    print("Button is pressed")

# button.pressed = True  # NOT ALLOWED - read-only property

在KV语言中:

# Kivy 2.x.x
Button:
    color: (1, 0, 0, 1) if self.state == 'down' else (1, 1, 1, 1)

# Kivy 3.x.x
Button:
    color: (1, 0, 0, 1) if self.pressed else (1, 1, 1, 1)

绑定状态变化

请使用 on_pressed 属性事件,而不是 on_state

# Kivy 2.x.x
def on_state(self, instance, value):
    if value == 'down':
        print("Pressed")
    else:
        print("Released")

# Kivy 3.x.x
def on_pressed(self, instance, is_pressed):
    if is_pressed:
        print("Pressed")
    else:
        print("Released")

从 `min_state_time` 迁移

min_state_time 属性(用于强制“按下”状态的最短持续时间)已被移除。如果你需要类似的行为,可以使用 Clock.create_trigger() 手动实现:

from kivy.clock import Clock
from kivy.uix.behaviors import ButtonBehavior
from kivy.uix.label import Label

class DelayedButton(ButtonBehavior, Label):
    MIN_STATE_TIME = 0.5  # seconds

    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        self._release_trigger = Clock.create_trigger(
            self._delayed_action,
            self.MIN_STATE_TIME
        )
        self.bind(pressed=self._pressed_changed)

    def _pressed_changed(self, instance, is_pressed):
        if is_pressed:
            self._release_trigger.cancel()
        else:
            self._release_trigger()  # schedule delayed action

    def _delayed_action(self, dt):
        # Your delayed logic here
        print("Action executed after minimum time")

移除 `trigger_action()` 方法

trigger_action() 方法已被移除。该方法曾用于以编程方式模拟按钮按下,但它违反了 UI 事件模拟的原则,并且未与触摸系统正确集成。

# Kivy 2.x.x
button.trigger_action(duration=0.1)

在Kivy 3.x.x中,如果你需要以编程方式触发按钮动作,有两种选择:

选项1:直接分发事件(推荐用于简单情况)

只需分发 on_presson_release 事件。你需要提供一个类似触摸的对象:

# Kivy 3.x.x - Direct event dispatch
# Create a simple object with touch attributes
class TouchProxy:
    def __init__(self, pos):
        self.pos = pos
        self.x, self.y = pos
        self.id = 0
        self.time_start = 0
        self.time_end = 0
        self.ud = {}

touch = TouchProxy(button.center)
button.dispatch('on_press', touch)
# ... your logic ...
button.dispatch('on_release', touch)

请注意,此方法不会更新`pressed`属性或触发内部状态变化,因为这些与实际的触摸事件相关联。

选项 2:模拟触摸事件(用于完整按钮状态模拟)

如果你需要按钮完全模拟触摸行为(包括更新`pressed`属性),你必须模拟真实的触摸事件:

from kivy.input.motionevent import MotionEvent
from kivy.clock import Clock

class MyButton(ButtonBehavior, Label):
    def simulate_press(self, duration=0.1):
        """Simulate a complete button press with touch events."""
        # Create a mock touch event
        touch = MotionEvent('mock', 0, {
            'x': self.center_x,
            'y': self.center_y,
            'pos': (self.center_x, self.center_y)
        })

        # Simulate touch down
        self.on_touch_down(touch)

        # Simulate touch up after duration
        def release_touch(dt):
            self.on_touch_up(touch)

        if duration > 0:
            Clock.schedule_once(release_touch, duration)
        else:
            release_touch(0)

或者在您的自定义按钮类中创建一个辅助方法:

from kivy.clock import Clock
from kivy.uix.behaviors import ButtonBehavior
from kivy.uix.label import Label

class TouchProxy:
    """Simple object that mimics touch attributes."""
    def __init__(self, pos):
        self.pos = pos
        self.x, self.y = pos
        self.id = 0
        self.time_start = 0
        self.time_end = 0
        self.ud = {}

class MyButton(ButtonBehavior, Label):
    def trigger_action(self, duration=0.1):
        """Simulate button press/release."""
        # Create touch proxy
        touch = TouchProxy(self.center)

        self._do_press(touch)
        self.dispatch('on_press', touch)

        def trigger_release(dt):
            self._do_release(touch)
            self.dispatch('on_release', touch)

        if not duration:
            trigger_release(0)
        else:
            Clock.schedule_once(trigger_release, duration)

移除 `last_touch` 属性

last_touch ObjectProperty 已被**移除**。不过,由于事件现在会将触摸作为参数传递,如有需要,您可以轻松地对其进行跟踪:

# Kivy 2.x.x
class MyButton(ButtonBehavior, Label):
    def on_press(self):
        print(f"Touch at: {self.last_touch.pos}")

# Kivy 3.x.x - Option 1: Use touch argument directly
class MyButton(ButtonBehavior, Label):
    def on_press(self, touch):
        print(f"Touch at: {touch.pos}")

# Kivy 3.x.x - Option 2: Track manually if needed elsewhere
class MyButton(ButtonBehavior, Label):
    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        self.last_touch = None

    def on_press(self, touch):
        self.last_touch = touch
        print(f"Touch at: {touch.pos}")

改进的多点触控行为

on_release 事件在多触点场景下的行为已发生变化:

  • Kivy 2.x.x:当**第一个触摸**被释放时触发 `on_release`(即使其他触摸仍然处于活动状态)。

  • Kivy 3.x.xon_release 仅在**所有活动触摸**释放后触发。

# Example: Multi-touch behavior difference
class MyButton(ButtonBehavior, Label):
    def on_release(self, touch):
        print(f"Button released - last touch ID: {touch.id}")

# Scenario: User presses button with 3 fingers, then lifts them one by one
# Kivy 2.x.x: "Button released" prints when the FIRST finger is lifted
# Kivy 3.x.x: "Button released" prints only when ALL 3 fingers are lifted

如果你需要旧的行为(在第一次触摸抬起时释放),请重写 on_touch_up

class LegacyButton(ButtonBehavior, Label):
    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        self._first_touch_released = False

    def on_touch_down(self, touch):
        result = super().on_touch_down(touch)
        if result:
            self._first_touch_released = False
        return result

    def on_touch_up(self, touch):
        if touch.grab_current is self and not self._first_touch_released:
            self._first_touch_released = True
            self._do_release(touch)
            self.dispatch('on_release', touch)
        return super().on_touch_up(touch)

新增 `on_cancel` 事件

现在,在拖拽操作期间,当触摸移出按钮边界时,会触发一个新事件 on_cancel。这仅在 `always_release=False`(默认值)时发生。

class MyButton(ButtonBehavior, Label):
    def on_press(self, touch):
        self.color = (1, 0, 0, 1)  # Red when pressed
        print(f"Pressed at {touch.pos}")

    def on_release(self, touch):
        self.color = (0, 1, 0, 1)  # Green on successful release
        print(f"Released at {touch.pos}")
        print("Button action executed")

    def on_cancel(self, touch):
        self.color = (1, 1, 1, 1)  # White when cancelled
        print(f"Cancelled at {touch.pos}")
        print("Button action cancelled")

该事件允许你在用户将手指/指针拖出按钮外部时提供视觉反馈,表明该操作将不会被执行。

`always_release` 行为变更

`always_release=False`(默认值)时的行为已得到改进:

Kivy 2.x.x:当触摸移出边界时,on_release 不会触发。然而,如果用户在释放前将触摸移回按钮边界内,on_release 则会正常触发。这可能导致意外的副作用。

Kivy 3.x.x:当触摸在拖动过程中移出边界时: - on_cancel 事件立即触发(新增) - 触摸被永久标记为已取消 - 即使触摸在释放前移回边界内,on_release 也不会触发 - 提供关于取消的明确反馈 - 现在,被取消的 on_release 会明确地通过 on_cancel 进行取消。

# Example: Standard button behavior (always_release=False)
class StandardButton(ButtonBehavior, Label):
    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        self.always_release = False  # Default, but explicit here

    def on_press(self, touch):
        print("Action started")
        self.text = "Release here to confirm"

    def on_release(self, touch):
        print("Action confirmed!")
        self.text = "Confirmed"

    def on_cancel(self, touch):
        print("Action cancelled")
        self.text = "Cancelled - press again"

always_release=True 时,行为保持不变——on_release 始终触发,而 on_cancel 从不触发:

# Example: Always release (drag-and-drop style)
class DragButton(ButtonBehavior, Label):
    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        self.always_release = True  # Release fires anywhere

    def on_release(self, touch):
        print(f"Released at {touch.pos} - on_cancel never fires")

子类化的内部钩子

方法 _do_press()_do_release()_do_cancel() 现在被记录为子类(如 ToggleButtonBehavior)的内部钩子。这些方法在相应的公共事件分发之前被调用,并且现在也接收 touch 参数。

备注

避免使用这些方法,它们用于子类中的内部状态管理。应用程序代码应改用 on_presson_releaseon_cancel 事件。

class CustomButton(ButtonBehavior, Label):
    def _do_press(self, touch):
        # Internal state changes before event dispatch (for internal use only)
        super()._do_press(touch)
        self._internal_state = "pressing"
        self._press_position = touch.pos

    def on_press(self, touch):
        # Public event handler for application logic
        print(f"Button pressed at {touch.pos} - use this for your logic")

破坏性变更摘要

已移除/已更改

Kivy 2.x.x

Kivy 3.x.x

state 属性

'normal''down'

已移除 - 请使用 `pressed`(只读)

min_state_time

NumericProperty(0.035)

已移除 - 请手动实现

last_touch

ObjectProperty

已移除 - 事件接收 touch 参数

trigger_action()

可用的方法。

已移除 - 使用模拟对象分发事件

事件签名

on_press() on_release()

on_press(touch) - 添加了 touch 参数 on_release(touch)

on_release 行为

在第一次触摸抬起时触发。

所有触摸释放后触发。

on_cancel 事件

不可用

新增 - on_cancel(touch) 在拖拽超出边界时触发。

always_release=False

静默非发布

显式 on_cancel 事件与触摸

内部钩子

未记录。

已记录的 _do_*(touch) 方法

ToggleButtonBehavior

将 `state` 替换为 `activated` 及主要 API 改进

在Kivy 3.x.x中,ToggleButtonBehavior`经历了显著的改进和变化。最显著的变化是用`activated`布尔属性(`AliasProperty)取代了`state` OptionProperty,并引入了诸如作用域组和`toggle_on`属性等新功能。

从 `state` 迁移到 `activated`

state 属性('normal''down')已被替换为布尔类型的 activated 属性:

# Kivy 2.x.x
if toggle.state == 'down':
    print("Toggle is active")
    toggle.state = 'normal'  # Deactivate

# Kivy 3.x.x
if toggle.activated:
    print("Toggle is active")
    toggle.activated = False  # Deactivate

在KV语言中:

# Kivy 2.x.x
ToggleButton:
    text: "ON" if self.state == 'down' else "OFF"
    color: (0, 1, 0, 1) if self.state == 'down' else (1, 1, 1, 1)

# Kivy 3.x.x
ToggleButton:
    text: "ON" if self.activated else "OFF"
    color: (0, 1, 0, 1) if self.activated else (1, 1, 1, 1)

新增 `on_activated` 事件

on_state 绑定替换为 on_activated

# Kivy 2.x.x
class MyToggle(ToggleButtonBehavior, Label):
    def on_state(self, instance, value):
        if value == 'down':
            print("Activated")
            self.background_color = (0, 1, 0, 1)
        else:
            print("Deactivated")
            self.background_color = (1, 1, 1, 1)

# Kivy 3.x.x
class MyToggle(ToggleButtonBehavior, Label):
    def on_activated(self, instance, value):
        if value:
            print("Activated")
            self.color = (0, 1, 0, 1)
        else:
            print("Deactivated")
            self.color = (1, 1, 1, 1)

在KV语言中,绑定属性变化:

# Kivy 2.x.x
<MyToggle@ToggleButton>:
    on_state: app.handle_toggle(self, self.state)

# Kivy 3.x.x
<MyToggle@ToggleButton>:
    on_activated: app.handle_toggle(self, self.activated)

新增 `toggle_on` 属性

新的 toggle_on 属性控制切换状态何时改变——是在按下时还是释放时(默认)。

# Kivy 3.x.x - Toggle immediately on press
ToggleButton:
    toggle_on: 'press'

# Kivy 3.x.x - Toggle on release (default)
ToggleButton:
    toggle_on: 'release'

当你需要即时视觉反馈时,这非常有用。

from kivy.uix.behaviors import ToggleButtonBehavior
from kivy.uix.label import Label

class InstantToggle(ToggleButtonBehavior, Label):
    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        self.toggle_on = 'press'  # Toggle immediately

    def on_activated(self, instance, value):
        self.text = "ON" if value else "OFF"

作用域组(新元组语法)

现在可以使用元组语法将组限定到特定的部件所有者。这可以防止在创建可复用组件时发生冲突:

# Kivy 2.x.x - Only global groups (string)
ToggleButton:
    group: 'options'  # Global across entire application

# Kivy 3.x.x - Global groups (still supported)
ToggleButton:
    group: 'options'

# Kivy 3.x.x - Scoped groups (NEW)
ToggleButton:
    group: (root, 'options')  # Scoped to 'root' widget

作用域组格式(owner, name)

  • owner:用于限定组作用域的任何对象(通常是一个部件)

  • 名称:可哈希标识符(字符串、整数、枚举等)

可复用组件与作用域组的示例:

from kivy.uix.boxlayout import BoxLayout
from kivy.uix.togglebutton import ToggleButton

class FilterPanel(BoxLayout):
    """Reusable panel with independent toggle groups."""

    def __init__(self, **kwargs):
        super().__init__(**kwargs)

        # Each FilterPanel instance has its own "size" group
        for size in ["Small", "Medium", "Large"]:
            btn = ToggleButton(
                text=size,
                group=(self, "size"),  # Scoped to this FilterPanel instance
                allow_no_selection=False
            )
            self.add_widget(btn)

# Multiple panels won't interfere with each other
panel1 = FilterPanel()  # Has independent "size" group
panel2 = FilterPanel()  # Has independent "size" group

在KV语言中:

<FilterPanel@BoxLayout>:
    ToggleButton:
        text: "Small"
        group: (root, "size")  # Scoped to FilterPanel instance
    ToggleButton:
        text: "Medium"
        group: (root, "size")
    ToggleButton:
        text: "Large"
        group: (root, "size")

# Each instance has independent groups
BoxLayout:
    FilterPanel:  # Independent "size" group
    FilterPanel:  # Independent "size" group

何时使用作用域组与全局组

在以下情况下使用**全局组**(字符串): - 您希望应用中所有开关共享同一组 - 构建简单的单实例界面时

在以下情况下使用**作用域组**(元组): - 创建带有内部切换组的可复用组件时 - 同一组件的多个实例不应相互干扰时 - 构建包含嵌套切换组的复杂布局时

方法变更:`get_widgets()` → `get_group()`

静态方法 get_widgets(groupname) 已被实例方法 get_group() 取代:

# Kivy 2.x.x - Static method
widgets = ToggleButtonBehavior.get_widgets('mygroup')
for widget in widgets:
    print(widget.text)
del widgets  # Always delete to prevent memory leaks

# Kivy 3.x.x - Instance method
widgets = my_toggle_button.get_group()
for widget in widgets:
    print(widget.text)
del widgets  # Still recommended to delete to prevent memory leaks

新的 get_group() 方法: - 返回与调用实例同组的控件 - 自动适用于全局组和作用域组 - 返回的列表中包含调用控件本身

# Example usage
from kivy.app import App
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.togglebutton import ToggleButton

class MyApp(App):
    def build(self):
        layout = BoxLayout()

        btn1 = ToggleButton(text="Option 1", group="options")
        btn2 = ToggleButton(text="Option 2", group="options")
        btn3 = ToggleButton(text="Option 3", group="options")

        layout.add_widget(btn1)
        layout.add_widget(btn2)
        layout.add_widget(btn3)

        # Get all widgets in btn1's group
        group_widgets = btn1.get_group()
        print(f"Group has {len(group_widgets)} widgets")  # Prints: 3
        del group_widgets

        return layout

移除 `_clear_groups()` 和 `_release_group()`

静态方法 _clear_groups() 和实例方法 _release_group() 已被移除。现在,组管理通过弱引用自动处理。

# Kivy 2.x.x - Manual group management
class MyToggle(ToggleButtonBehavior, Label):
    def custom_release(self):
        self._release_group(self)
        self.state = 'normal'

# Kivy 3.x.x - Automatic group management
class MyToggle(ToggleButtonBehavior, Label):
    def custom_release(self):
        self.activated = False  # Automatically manages group

改进的分组清理

组管理现在使用 WeakSet 实现当控件被删除时的自动清理。您不再需要手动清理组:

# Kivy 2.x.x - Potential memory leaks if not careful
toggle = ToggleButton(group='mygroup')
del toggle  # Weak reference might linger

# Kivy 3.x.x - Automatic cleanup
toggle = ToggleButton(group='mygroup')
del toggle  # Automatically removed from group via WeakSet

破坏性变更摘要

时钟

改进的@triggered装饰器行为、实例隔离与防抖功能

在Kivy 3.x.x中,:func:`~kivy.clock.triggered`装饰器得到了显著改进。此前,当它作为方法装饰器使用时,会在类的所有实例间共享同一个触发器和状态。这意味着在一个实例上调用该方法会限制所有其他实例上的调用,且不同实例的参数可能会相互覆盖。

行为变更

  • 改进的**实例隔离**:每个实例现在拥有自己独立的触发器和参数存储。在 widget_a 上调用触发方法不再影响 widget_b

  • 改进的**惰性初始化**:现在仅在首次调用被装饰函数时才创建触发器,从而提升初始化性能。

  • 新增 is_triggered 属性:为被装饰的函数/方法添加了一个新的 is_triggered 属性,使您能够检查当前是否有待处理的调用。

  • 新增 debounce 参数:添加了一个新的 ``debounce=False``(默认值)参数。

    • **节流**(默认):当触发器处于激活状态时,后续调用会更新参数,但*不会*重置计时器。函数在初始超时后仅触发一次。

    • **防抖**(debounce=True):后续调用会取消任何待执行的调度并重新安排。只有当调用者在 timeout 持续时间内停止调用该函数后,函数才会触发。

迁移影响

这主要是一个**错误修复**和一组**新功能**。对于大多数应用程序,它不应要求代码更改。然而,如果你的代码库有意依赖不同实例之间遗留的共享节流行为,你可以通过在``@triggered``上方使用**``@classmethod``**装饰器来恢复此行为。

这确保了触发器绑定到类对象而非单个实例,从而以惯用方式恢复了共享行为。

class MyWidget(Widget):
    # Default in 3.x.x: Isolated per instance
    @triggered(0.1)
    def sync_ui(self, *args):
        pass

    # Shared behavior (same as legacy 2.x.x): Shared by all instances
    @classmethod
    @triggered(0.1)
    def sync_shared_data(cls, *args):
        pass

    # Optional: Debouncing (0.1s from the LAST call)
    @triggered(0.1, debounce=True)
    def search_input(self, text):
        pass

SVG

移除实验性的 kivy.graphics.svg 模块

在Kivy 3.x.x中,位于``kivy.graphics.svg``的实验性``Svg``画布指令已被移除,同时移除的还有其位于``examples/svg/下的示例脚本(``benchmark.pymain.pymain-smaa.py)及其工厂注册。该模块自引入以来一直标记为实验性,现已被Kivy 3.x.x中新增的SVG支持所取代。

替换:SvgWidget / AsyncSvgWidget

对于大多数使用场景,直接使用 :class:`~kivy.uix.svg.SvgWidget`(本地资源)或 :class:`~kivy.uix.svg.AsyncSvgWidget`(网络资源)即可:

# Kivy 2.x.x
from kivy.graphics.svg import Svg

with widget.canvas:
    Svg('image.svg')

# Kivy 3.x.x
from kivy.uix.svg import SvgWidget

widget.add_widget(SvgWidget(source='image.svg'))

在KV语言中:

# Kivy 3.x.x
SvgWidget:
    source: 'image.svg'

替换:kivy.core.svg 图像提供器

通过标准图像管线加载SVG时(例如作为 Image 的纹理),当加载 .svg 源文件时,会自动选择新的 kivy.core.svg 提供器;无需显式导入或注册 Factory。

工厂注册

指向 kivy.graphics.svgSvg 工厂条目已被移除。SvgWidgetAsyncSvgWidget 已以其自身名称在工厂中注册,可直接在 KV 中使用。

应用存储目录

Linux 用户数据目录路径变更(XDG 合规性修复)

在 Kivy 3.x.x 中,Linux 上的 App.user_data_dir 路径已修正以遵循 XDG 基础目录规范。此前,它错误地使用了 ``XDG_CONFIG_HOME``(用于配置文件);现在,它正确地使用了 ``XDG_DATA_HOME``(用于应用程序数据)。

Linux 上的路径变更:

属性

Kivy 2.x.x

Kivy 3.x.x

user_data_dir

~/.config/<app_name> (不正确)

``~/.local/share/<app_name>``(符合 XDG 规范)

影响:

如果你的Linux应用使用``App.user_data_dir``存储用户数据,升级到Kivy 3.x.x后,数据将存储在不同的位置。这是符合XDG规范的正确位置,但现有应用可能需要迁移其数据。

迁移选项:

  1. **手动迁移**(推荐用于生产应用)

    在应用启动期间,将现有数据从旧位置迁移到新位置:

注意: Windows、macOS、iOS 和 Android 的路径保持不变。

新增 App.user_cache_dir 属性

Kivy 3.x.x 引入了一个新的 App.user_cache_dir 属性,用于存放系统可能随时删除的临时/缓存数据。

这不是**破坏性变更**——它是一个新的可选属性。现有应用无需修改即可继续正常工作。

平台路径:

  • Windows:%APPDATA%\<app_name>\Cache

  • macOS:~/Library/Caches/<app_name>

  • Linux:~/.cache/<app_name>``(遵循 ``$XDG_CACHE_HOME

  • Android:Context.getCacheDir()

  • iOS:~/Library/Caches/<app_name>

新增 KIVY_DESKTOP_PATH_ID 环境变量

Kivy 3.x.x 引入了 KIVY_DESKTOP_PATH_ID,用于在桌面平台上设置用户友好的应用程序目录名称。

这不是**破坏性变更**——它是可选的。除非你明确设置此环境变量,否则现有应用将继续保持不变地运行。

关键特性:

设置 KIVY_DESKTOP_PATH_ID 会为 .kivy 目录创建一个**应用特定**的位置,该目录包含配置和日志文件。如果不设置 KIVY_DESKTOP_PATH_ID,配置和日志将放置在单一的全局 .kivy 目录(~/.kivy)中。

这意味着多个Kivy应用程序现在可以拥有各自独立的配置和日志目录,从而避免不同应用程序之间的冲突。

当设置时:

该变量为目录提供了人类可读的应用程序标题,使用户在浏览文件系统时更容易识别您的应用目录。

示例:

import os
os.environ['KIVY_DESKTOP_PATH_ID'] = 'My Photo Editor'

from kivy.app import App

# On Windows, creates: %APPDATA%\My_Photo_Editor\.kivy
# Instead of: %APPDATA%\photoeditor\.kivy

优先级:

KIVY_DESKTOP_PATH_ID 具有最高优先级,并影响:

  • KIVY_HOME 目录(覆盖 KIVY_HOME 环境变量和虚拟环境检测)

  • App.user_data_dir 目录

  • App.user_cache_dir 目录

平台行为:

  • **桌面平台**(Windows、macOS、Linux):使用规范化的 path_id 作为目录名称。

  • **移动平台**(iOS、Android):忽略 - 与之前一样使用 App.name

使用 KIVY_DESKTOP_PATH_ID='My Photo Editor' 的桌面路径示例:

目录

路径

KIVY_HOME

~/Library/Application Support/My_Photo_Editor/ .kivy (macOS)

user_data_dir

``%APPDATA%My_Photo_Editor``(Windows)

user_cache_dir

``%LOCALAPPDATA%My_Photo_EditorCache``(Windows)

警告:

如果你在现有应用中设置了 KIVY_DESKTOP_PATH_ID,你的数据将移动到新位置。你可能需要迁移现有数据(参见上面的 Linux 迁移示例)。

完整文档请参阅 控制环境examples/desktop_path_id/