Kivy的属性系统
-
- [1 Kivy的属性系统](#1 Kivy的属性系统)
- [2 Kivy 属性 (Properties)](#2 Kivy 属性 (Properties))
-
- [2.1 属性类型](#2.1 属性类型)
- [2.2 类属性 🆚⚖️⚔️ 实例属性对比表](#2.2 类属性 🆚⚖️⚔️ 实例属性对比表)
- [2.3 基础示例](#2.3 基础示例)
- [2.4 属性初始化](#2.4 属性初始化)
- [2.5 属性绑定](#2.5 属性绑定)
- [2.6 在 Kv 语言中使用属性](#2.6 在 Kv 语言中使用属性)
- [2.7 AliasProperty:计算属性 并不是存储属性,它的值依赖于其他的属性](#2.7 AliasProperty:计算属性 并不是存储属性,它的值依赖于其他的属性)
- [2.8 重要注意事项](#2.8 重要注意事项)
- [2.9 实际应用示例](#2.9 实际应用示例)
- [2.11 常见问题解答](#2.11 常见问题解答)
- [3 kivy的属性实现原理](#3 kivy的属性实现原理)
- [4 案例](#4 案例)
-
- [4.1 按钮点击修改属性](#4.1 按钮点击修改属性)
- [4.2 ListProperty和DictProperty的属性更新](#4.2 ListProperty和DictProperty的属性更新)
- [4.3 属性的继承](#4.3 属性的继承)
- [4.4 属性和控件的相互绑定](#4.4 属性和控件的相互绑定)
1 Kivy的属性系统
Properties (属性) 是 Kivy 框架中一个非常重要的概念,用于实现数据绑定和自动更新。
Kivy 的属性系统是框架最强大的功能之一,它实现了:
- 数据绑定:自动同步数据和UI
- 观察者模式:无需手动管理状态更新
- 类型安全:确保属性值的类型正确性
- 事件驱动:响应式更新界面
掌握属性系统是高效使用 Kivy 进行应用开发的关键。通过合理利用属性绑定,您可以创建响应迅速、维护性强的用户界面应用程序。
2 Kivy 属性 (Properties)
介绍
Kivy 的属性类用于实现属性自动分发机制 。当您为属性赋值时,与该属性绑定的所有部件都会收到该值已更改的通知,从而可以更新它们的显示状态。
这种机制是 Kivy 框架响应式设计的核心,允许您在修改 Python 代码中的属性值时,自动更新用户界面。
2.1 属性类型
Kivy 提供了多种类型的属性,每种都针对特定数据类型设计:
StringProperty- 字符串属性NumericProperty- 数值属性BoundedNumericProperty- 有界数值属性(带最小/最大值限制)ObjectProperty- 对象属性DictProperty- 字典属性ListProperty- 列表属性OptionProperty- 选项属性(从预定义选项中选择)AliasProperty- 别名属性(通过 getter/setter 方法访问)BooleanProperty- 布尔属性ColorProperty- 颜色属性
2.2 类属性 🆚⚖️⚔️ 实例属性对比表
| 特性 | 类级别定义 (Kivy Property) | 实例级别定义 (普通属性) |
|---|---|---|
| 定义位置 | 在类内部,但在所有方法之外。 | 在 __init__ 或其他实例方法内部。 |
| 语法 | my_property = NumericProperty(默认值) |
self.my_attribute = 初始值 |
| 本质 | 它是一个类属性(属于类本身),但 Kivy 会通过描述符协议为每个实例管理独立的值。 | 它是一个实例属性(只属于该实例)。 |
| Kivy 特殊能力 | ✅ 有:自动数据绑定、类型校验、默认值、变化派发等。 | ❌ 无:只是一个普通的 Python 变量。 |
| 初始化时机 | 在类被加载时(即 import 时)就定义了属性描述符。 | 在对象被创建时(调用 __init__ 时)才创建。 |
2.3 基础示例
python
from kivy.event import EventDispatcher
from kivy.properties import NumericProperty
class MyClass(EventDispatcher):
# 定义一个数值属性,默认值为0
my_value = NumericProperty(0)
# 使用示例
obj = MyClass()
# 绑定回调函数,当属性变化时自动调用
obj.bind(my_value=lambda instance, value: print(f'值已改变: {value}'))
obj.my_value = 42 # 控制台输出: "值已改变: 42"
2.4 属性初始化
您可以在定义属性时设置默认值:
python
from kivy.properties import StringProperty, ListProperty
class MyWidget(Widget):
# 带默认值的字符串属性
title = StringProperty('默认标题')
# 带默认值的列表属性
items = ListProperty(['项目1', '项目2'])
2.5 属性绑定
属性绑定的核心优势在于自动通知机制:
python
class CustomWidget(Widget):
value = NumericProperty(0)
def __init__(self, **kwargs):
super().__init__(**kwargs)
# 绑定内部方法到属性变化
self.bind(value=self.on_value_change)
def on_value_change(self, instance, new_value):
# 当value属性改变时自动调用
print(f"值从 {instance.value} 变为 {new_value}")
2.6 在 Kv 语言中使用属性
Kivy 的 Kv 语言天然支持属性绑定:
kv
# 在 .kv 文件中
<MyLabel@Label>:
# 当text属性改变时,自动更新显示
text: root.custom_text if hasattr(root, 'custom_text') else "默认文本"
font_size: 20 if len(self.text) < 10 else 16
2.7 AliasProperty:计算属性 并不是存储属性,它的值依赖于其他的属性
如下area依赖于长和宽。获取时是self.width * self.height,修改时自动修改self.width 的值
使用 AliasProperty 创建计算属性:
python
from kivy.properties import AliasProperty
class Rectangle(EventDispatcher):
width = NumericProperty(1)
height = NumericProperty(1)
def get_area(self):
return self.width * self.height
def set_area(self, value):
# 假设设置面积时会调整宽度
self.width = value / self.height if self.height != 0 else 0
# 定义计算属性
area = AliasProperty(get_area, set_area, bind=['width', 'height'])
rect = Rectangle()
rect.width = 3
rect.height = 4
print(rect.area) # 输出: 12
rect.area = 24
print(rect.width) # 输出: 6.0
2.8 重要注意事项
-
必须继承自
EventDispatcher:只有继承自EventDispatcher或其子类(如Widget)的类才能使用 Kivy 属性。 -
类级别定义 :属性必须在类级别 定义,而不是在
__init__方法中。 -
内存管理:属性系统使用弱引用,通常不会造成内存泄漏,但需要注意循环引用问题。
-
性能考虑:频繁的属性更新可能影响性能,对于高频更新的数据需要考虑优化策略。
-
错误处理 :某些属性类型(如
BoundedNumericProperty)会在值超出范围时抛出异常。
2.9 实际应用示例
下面是一个完整的 Kivy 应用示例,展示了属性在实际中的应用:
python
from kivy.app import App
from kivy.uix.slider import Slider
from kivy.uix.label import Label
from kivy.uix.boxlayout import BoxLayout
from kivy.properties import NumericProperty
class ProgressDisplay(BoxLayout):
# 定义进度属性
progress = NumericProperty(0)
def __init__(self, **kwargs):
super().__init__(orientation='vertical', **kwargs)
# 创建标签显示进度
self.label = Label(text=f'进度: {self.progress}%')
self.add_widget(self.label)
# 创建滑块控制进度
self.slider = Slider(min=0, max=100, value=self.progress)
self.slider.bind(value=self.on_slider_change)
self.add_widget(self.slider)
# 绑定属性变化到更新函数
self.bind(progress=self.update_label)
def on_slider_change(self, instance, value):
# 滑块变化时更新属性
self.progress = value
def update_label(self, instance, value):
# 属性变化时更新标签
self.label.text = f'进度: {value:.1f}%'
class PropertyApp(App):
def build(self):
return ProgressDisplay()
if __name__ == '__main__':
PropertyApp().run()
- 划动进度条 -> on_slider_change -> update_label -> 更新显示

2.11 常见问题解答
Q: 为什么我的属性变化没有触发回调?
A: 请确保:1) 类继承自 EventDispatcher;2) 使用 bind() 方法正确绑定了回调;3) 直接为属性赋值(obj.property = value),而不是修改可变对象的内部状态。
Q: 如何取消属性绑定?
A: 使用 unbind() 方法或 unbind_uid() 方法取消绑定。
3 kivy的属性实现原理
Kivy 实现这种特殊功能的核心机制是 Python 描述符协议 + 观察者模式 + 事件分发系统。
python
# 简化的 Property 基类实现原理
class Property:
"""模拟 Kivy Property 的核心逻辑"""
def __init__(self, defaultvalue=None):
self.defaultvalue = defaultvalue
# 存储每个实例的属性值 {实例id: 值}
self.storage = {}
# 存储绑定关系 {实例id: [回调函数列表]}
self.observers = {}
def __get__(self, obj, objtype):
"""描述符协议:获取属性值时调用"""
if obj is None:
return self # 通过类访问时返回描述符本身
# 1. 获取或初始化该实例的值
obj_id = id(obj)
if obj_id not in self.storage:
self.storage[obj_id] = self.defaultvalue
return self.storage[obj_id]
def __set__(self, obj, value):
"""描述符协议:设置属性值时调用"""
obj_id = id(obj)
old_value = self.storage.get(obj_id, self.defaultvalue)
# 2. 类型检查和转换
value = self.convert(value)
# 3. 值实际改变时才触发事件
if old_value != value:
self.storage[obj_id] = value
# 4. 分发变化事件
if obj_id in self.observers:
for callback in self.observers[obj_id]:
callback(obj, value)
def bind(self, obj, callback):
"""绑定回调函数"""
obj_id = id(obj)
if obj_id not in self.observers:
self.observers[obj_id] = []
self.observers[obj_id].append(callback)
class NumericProperty(Property):
def __init__(self, defaultvalue=0, **kwargs):
super().__init__(defaultvalue, **kwargs)
def convert(self, value):
"""类型转换和验证"""
try:
return float(value) # 确保是数值类型
except (TypeError, ValueError):
raise ValueError(f"无法将 {value} 转换为数值")
# 简化的 EventDispatcher
class EventDispatcher:
def __init__(self):
self._event_listeners = {} # 存储事件监听器
def bind(self, **kwargs):
"""将属性绑定到回调函数"""
for prop_name, callback in kwargs.items():
if hasattr(self.__class__, prop_name):
prop = getattr(self.__class__, prop_name)
if isinstance(prop, Property):
prop.bind(self, callback)
def dispatch(self, event_type, *args):
"""分发事件给所有监听器"""
if event_type in self._event_listeners:
for callback in self._event_listeners[event_type]:
callback(self, *args)
class MyWidget(EventDispatcher):
counter = NumericProperty(0)
def __init__(self):
# print(MyWidget.__dict__)
dict_ss = {}
for k , v in MyWidget.__dict__.items():
if isinstance(v, Property):
method_name = 'on_' + k
if hasattr(self, method_name):
method = getattr(self, method_name)
# 自动绑定!
dict_ss[k] = method
if len(dict_ss) != 0:
self.bind(**dict_ss)
def on_counter(self, instance, value):
print("计数器变化:", self, instance, value)
return
# 创建实例时发生的事情:
w = MyWidget()
'''
执行顺序:
1. Python 解释器看到 w.counter = 10
2. 查找 MyWidget.counter → 找到 NumericProperty 描述符
3. 调用 counter.__set__(w, 10)
4. __set__ 方法:
a. 获取旧值:0
b. 类型转换:float(10) → 10.0
c. 比较:0 != 10.0 → 需要更新
d. 存储新值:storage[w_id] = 10.0
e. 查找 observers[w_id] 列表
f. 遍历调用每个回调:on_counter_change(w, 10.0)
5. 打印:"计数器变化: 10.0"
'''
w.counter += 10
w.counter += 10

4 案例
4.1 按钮点击修改属性

python
from kivy.uix.button import Button
from kivy.properties import StringProperty, NumericProperty
from kivy.app import App
class MyButton(Button):
# 定义两个属性
custom_text = StringProperty('默认')
click_count = NumericProperty(0)
# 自动绑定:方法名必须严格匹配 "on_属性名"
def on_custom_text(self, instance, value):
# 当 custom_text 变化时自动调用
print(f"文本变化: {value}")
self.text = value # 同步更新按钮显示文本
def on_click_count(self, instance, value):
# 当 click_count 变化时自动调用
print(f"点击次数: {value}")
def on_release(self):
print("========")
self.custom_text = 'new word' # 自动触发 on_custom_text()
self.click_count += 1 # 自动触发 on_click_count()
return super().on_release()
from kivy.app import App
from kivy.uix.button import Button
class TestApp(App):
def build(self):
# return a Button() as a root widget
return MyButton(text='hello world')
if __name__ == '__main__':
TestApp().run()
4.2 ListProperty和DictProperty的属性更新

python
from kivy.uix.widget import Widget
from kivy.properties import ListProperty, DictProperty
class CollectionDemo(Widget):
# 列表属性
items = ListProperty(['A', 'B', 'C'])
# 字典属性
settings = DictProperty({
'volume': 80,
'theme': 'dark',
'language': 'zh'
})
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.items.append('D') # 会触发 on_items
self.settings['volume'] = 90 # 会触发 on_settings
# 正确做法:重新赋值
new_items = self.items + ['D'] # 创建新列表
self.items = new_items # ✅ 触发事件
new_settings = dict(self.settings)
new_settings['volume'] = 900
self.settings = new_settings # ✅ 触发事件
def on_items(self, instance, value):
print(f"列表已更新: {value}")
def on_settings(self, instance, value):
print(f"设置已更新: {value}")
W = CollectionDemo()
4.3 属性的继承

python
from kivy.uix.widget import Widget
from kivy.properties import StringProperty
class Animal(Widget):
species = StringProperty('动物')
sound = StringProperty('叫声')
def on_species(self, instance, value):
print(f"物种: {value}")
def make_sound(self):
print(f"{self.species} 发出 {self.sound}")
class Dog(Animal):
# 继承并覆盖父类属性
species = StringProperty('狗')
sound = StringProperty('汪汪')
# 新增属性
breed = StringProperty('未知品种')
class Cat(Animal):
# 覆盖父类属性
species = StringProperty('猫')
sound = StringProperty('喵喵')
def __init__(self, **kwargs):
super().__init__(**kwargs)
# 可以访问父类属性
print(f"父类默认物种: {super().species}")
# 测试继承
dog = Dog()
dog.make_sound() # 狗 发出 汪汪
cat = Cat()
cat.make_sound() # 猫 发出 喵喵
4.4 属性和控件的相互绑定
- 控件的值bind到某方法,当控件的值发生变化时,方法被调用,方法内部修改属性的值。
- 属性的值发生变化,自动调用到属性的"on_"方法,"on_"方法修改其他控件的显示。

python
from kivy.app import App
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.slider import Slider
from kivy.uix.label import Label
from kivy.properties import NumericProperty
from kivy.clock import Clock
class Dashboard(BoxLayout):
# 核心数据属性
temperature = NumericProperty(25.0)
humidity = NumericProperty(50.0)
pressure = NumericProperty(1013.0)
def __init__(self, **kwargs):
super().__init__(orientation='vertical', **kwargs)
# 温度控制区
temp_box = BoxLayout(size_hint=(1, 0.3))
temp_box.add_widget(Label(text="temperature:"))
temp_slider = Slider(min=0, max=50, value=self.temperature)
temp_slider.bind(value=self.set_temp) # 绑定到属性
temp_box.add_widget(temp_slider)
self.temp_label = Label(text=f"{self.temperature}°C")
temp_box.add_widget(self.temp_label)
self.add_widget(temp_box)
# humidity控制区
temp_box1 = BoxLayout(size_hint=(1, 0.3))
temp_box1.add_widget(Label(text="humidity:"))
self.temp_slider1 = Slider(min=0, max=100, value=self.humidity)
temp_box1.add_widget(self.temp_slider1)
self.temp_label1 = Label(text=f"{self.humidity}")
temp_box1.add_widget(self.temp_label1)
self.add_widget(temp_box1)
# 模拟实时数据更新
Clock.schedule_interval(self.update_sensors, 1.0)
def set_temp(self, instance, value):
"""设置温度属性"""
self.temperature = round(value, 1)
def on_temperature(self, instance, value):
"""温度变化时更新显示"""
self.temp_label.text = f"{value}°C"
# 根据温度改变颜色
if value < 10:
self.temp_label.color = (0, 0.5, 1, 1) # 蓝色
elif value > 30:
self.temp_label.color = (1, 0, 0, 1) # 红色
else:
self.temp_label.color = (0, 1, 0, 1) # 绿色
def on_humidity(self, instance, value):
self.temp_slider1.value = value
self.temp_label1.text = f"{self.humidity}"
def update_sensors(self, dt):
"""模拟传感器数据更新"""
import random
self.humidity = random.uniform(40, 60)
self.pressure = random.uniform(1000, 1020)
class SensorApp(App):
def build(self):
return Dashboard()
if __name__ == '__main__':
SensorApp().run()