← Volver al listado de tecnologías

Lenguaje KV

Por: SiempreListo
kivykvuidiseño

¿Qué es el Lenguaje KV?

KV es un lenguaje declarativo para diseñar interfaces en Kivy. Separa la lógica (Python) del diseño (KV).

Ventajas

Sintaxis Básica

Estructura

# Comentario en KV

# Widget raíz (sin <>)
BoxLayout:
    orientation: 'vertical'

    Label:
        text: 'Hola'

    Button:
        text: 'Presionar'

Reglas de Estilo (<>)

# Definir estilo para clase
<MiWidget>:
    canvas.before:
        Color:
            rgba: 0.2, 0.2, 0.2, 1
        Rectangle:
            pos: self.pos
            size: self.size

Carga de Archivos KV

Método 1: Automático por nombre

# main.py
from kivy.app import App
from kivy.uix.boxlayout import BoxLayout

class MiWidget(BoxLayout):
    pass

class MiApp(App):  # Busca 'mi.kv'
    def build(self):
        return MiWidget()
# mi.kv
<MiWidget>:
    Label:
        text: 'Cargado automáticamente'

Método 2: Builder.load_file

from kivy.app import App
from kivy.lang import Builder

Builder.load_file('interfaz.kv')

class OtraApp(App):
    def build(self):
        return Builder.load_file('pantalla.kv')

Método 3: Builder.load_string

from kivy.app import App
from kivy.lang import Builder

KV = '''
BoxLayout:
    Label:
        text: 'Desde string'
'''

class StringApp(App):
    def build(self):
        return Builder.load_string(KV)

Propiedades en KV

Valores Estáticos

Label:
    text: 'Texto fijo'
    font_size: 24
    color: 1, 0, 0, 1

Referencias con self

Widget:
    canvas:
        Rectangle:
            pos: self.pos      # Posición del widget
            size: self.size    # Tamaño del widget

Referencias con root

<MiLayout>:
    Label:
        text: root.titulo    # Propiedad del widget raíz

Referencias con app

Label:
    text: app.nombre        # Propiedad de la clase App

Binding Automático

Las propiedades en KV se actualizan automáticamente:

<Contador>:
    BoxLayout:
        Label:
            text: str(root.valor)  # Se actualiza cuando cambia valor
        Button:
            text: '+'
            on_press: root.incrementar()
from kivy.uix.boxlayout import BoxLayout
from kivy.properties import NumericProperty

class Contador(BoxLayout):
    valor = NumericProperty(0)

    def incrementar(self):
        self.valor += 1

Eventos en KV

on_press, on_release

Button:
    text: 'Click'
    on_press: print('Presionado')
    on_release: print('Soltado')

on_text, on_focus

TextInput:
    on_text: print('Texto:', self.text)
    on_focus: print('Enfocado:', args[1])

Llamar métodos de root

<MiWidget>:
    Button:
        on_press: root.mi_metodo()
        on_release: root.otro_metodo(self.text)

IDs y Referencias

Definir IDs

<Formulario>:
    BoxLayout:
        orientation: 'vertical'

        TextInput:
            id: campo_nombre

        TextInput:
            id: campo_email

        Button:
            text: 'Enviar'
            on_press: root.enviar(campo_nombre.text, campo_email.text)

Acceder desde Python

class Formulario(BoxLayout):
    def enviar(self, nombre, email):
        print(f'Nombre: {nombre}, Email: {email}')

    def limpiar(self):
        self.ids.campo_nombre.text = ''
        self.ids.campo_email.text = ''

Widgets Dinámicos

Agregar widgets desde KV

<ListaItems>:
    id: lista
    orientation: 'vertical'
class ListaItems(BoxLayout):
    def agregar_item(self, texto):
        self.add_widget(Label(text=texto))

Factory para crear widgets

from kivy.factory import Factory

# Registrar widget
Factory.register('MiBoton', cls=MiBoton)

Directivas KV

#:import

#:import rgba kivy.utils.get_color_from_hex
#:import Window kivy.core.window.Window

Label:
    color: rgba('#FF5733')
    text: str(Window.size)

#:set

#:set color_primario (0.2, 0.6, 0.8, 1)
#:set padding_base 20

BoxLayout:
    padding: padding_base

    Button:
        background_color: color_primario

Canvas en KV

canvas.before y canvas.after

<WidgetConFondo>:
    canvas.before:
        Color:
            rgba: 0.2, 0.2, 0.2, 1
        Rectangle:
            pos: self.pos
            size: self.size

    canvas.after:
        Color:
            rgba: 1, 0, 0, 0.5
        Line:
            rectangle: self.x, self.y, self.width, self.height
            width: 2

Ejemplo Completo: Calculadora

# main.py
from kivy.app import App
from kivy.uix.boxlayout import BoxLayout
from kivy.properties import StringProperty

class CalculadoraWidget(BoxLayout):
    display = StringProperty('0')

    def agregar_digito(self, digito):
        if self.display == '0':
            self.display = digito
        else:
            self.display += digito

    def calcular(self):
        try:
            self.display = str(eval(self.display))
        except:
            self.display = 'Error'

    def limpiar(self):
        self.display = '0'

class CalculadoraApp(App):
    def build(self):
        return CalculadoraWidget()

if __name__ == '__main__':
    CalculadoraApp().run()
# calculadora.kv
<CalculadoraWidget>:
    orientation: 'vertical'
    padding: 10
    spacing: 5

    Label:
        text: root.display
        font_size: '32sp'
        halign: 'right'
        size_hint_y: 0.2

    GridLayout:
        cols: 4
        spacing: 5

        Button:
            text: '7'
            on_press: root.agregar_digito('7')
        Button:
            text: '8'
            on_press: root.agregar_digito('8')
        Button:
            text: '9'
            on_press: root.agregar_digito('9')
        Button:
            text: '/'
            on_press: root.agregar_digito('/')

        Button:
            text: 'C'
            on_press: root.limpiar()
        Button:
            text: '='
            on_press: root.calcular()

Testing con Archivos KV

# test_kv.py
import unittest
from kivy.lang import Builder
from kivy.uix.boxlayout import BoxLayout

class TestKV(unittest.TestCase):
    def test_cargar_kv_string(self):
        kv = '''
BoxLayout:
    Label:
        id: mi_label
        text: 'Test'
'''
        widget = Builder.load_string(kv)
        self.assertIsInstance(widget, BoxLayout)
        self.assertEqual(widget.ids.mi_label.text, 'Test')

    def test_ids_accesibles(self):
        kv = '''
BoxLayout:
    Button:
        id: btn
        text: 'Click'
'''
        widget = Builder.load_string(kv)
        self.assertIn('btn', widget.ids)

    def test_binding_funciona(self):
        kv = '''
BoxLayout:
    value: 10
    Label:
        text: str(root.value)
'''
        widget = Builder.load_string(kv)
        self.assertEqual(widget.children[0].text, '10')

if __name__ == '__main__':
    unittest.main()

Probar en Dispositivo con ADB

# Ver errores de parsing KV
adb logcat | grep -i "kv\|parser\|builder"

# Copiar archivo KV modificado al dispositivo (hot reload manual)
adb push main.kv /sdcard/kivy/miapp/main.kv

# Ver archivos de la app en el dispositivo
adb shell ls /data/data/com.ejemplo.miapp/files/app/

Resumen