Pokémon, Python & Flet

Tutorial - Introducción al diseño de interfaces de usuario con Python & Flet

¿Qué es Flet?

Flet es una framework de código abierto que facilita desarrollar aplicaciones web, móviles y de escritorio con Python. Flet es basado en Flutter, un framework para el desarrollo de interfaces de usuario multiplataforma de código abierto de Google.

Entre los distintos beneficios que ofrece Flet, se destacan:

  • Productividad: Con Flet, los desarrolladores pueden crear aplicaciones complejas de manera ágil y con menos líneas de código.

  • Portabilidad: Flet permite a los desarrolladores crear aplicaciones capaces de ejecutarse en diversos sistemas, abarcando web, iOS, Android y macOS.

  • Eficiencia: Concebido para optimizar recursos.

  • Facilidad de adopción: Flet se distingue por su sencillez de aprendizaje y uso, así no se cuente con experiencia previa en Flutter.

Paso #1 - Descargar el código base y ejecutarlo en una terminal

Este programa, diseñado para ejecutarse en la terminal, permite cargar una lista de Pokémon desde un archivo CSV. Esta base de código permite realizar búsquedas por nombre y visualizar la imagen del Pokémon directamente en pantalla.

Descargue la base de código desde el siguiente repositorio. Si prefiere, utilice git para hacerlo.

https://gitlab.com/jpadillaa/simple-pokemon-database

$ git clone https://gitlab.com/jpadillaa/simple-pokemon-database.git

Cargue la base de código en su IDE de preferencia (Visual Studio Code en mi caso).

Para ejecutar este programa, necesitamos ubicarnos en el archivo cli_main.py. Antes de hacerlo, es necesario instalar la librería Pillow, la cual es un conjunto de herramientas para el procesamiento de imágenes en Python. Esta librería nos brinda la capacidad de abrir, manipular y guardar varios formatos de archivos de imagen.

(Opcional) - Crear un ambiente virtual

Nota: Se recomienda el uso de un entorno virtual con virtualenv para llevar a cabo los siguientes pasos. Si no es de su preferencia, puede omitir estas instrucciones y proceder a instalar Pillow directamente.

En el directorio de trabajo, cree un nuevo entorno virtual utilizando el siguiente comando:

virtualenv env

Para activar el entorno virtual, ejecute el siguiente comando en PowerShell (en el caso de utilizar Windows):

.\env\Scripts\activate.ps1

En Bash (en el caso de utilizar Linux):

source env/bin/activate

Nota: Si está trabajando en Windows con PowerShell y recibe el mensaje de error + Set-ExecutionPolicy Unrestricted al tratar de ejecutar el ambiente virtual, abra PowerShell como administrador y ejecute el comando Set-ExecutionPolicy Unrestricted

Para intentarlo de nuevo con el entorno virtual, vuelva a ejecutar el siguiente comando:

.\env\Scripts\activate.ps1

Ejecutar el programa

Para instalar Pillow, puede usar pip, el gestor de paquetes de Python. Ejecute el siguiente comando en la terminal:

$ pip install Pillow

Ejecute el programa con la siguiente instrucción:

$ python cli_main.py

El programa es simple, basta con proporcionar el nombre de un Pokémon en la consola y visualizará su nombre, tipo en el CLI y la imagen del Pokémon en pantalla gracias a Pillow.

Este programa se compone de varios archivos:

Pokemon.py: Aquí encontrará una clase que modela el objeto Pokémon. Cada Pokémon tiene atributos como nombre, tipo (o incluso un segundo tipo si aplica) y la ruta a su imagen correspondiente.

load.py: En este archivo se aloja una función que lee un archivo CSV y lo transforma en un diccionario de diccionarios, representando los datos de los Pokémon.

cli_main.py: Esta pieza es fundamental. Contiene dos funciones clave:

  • search_pokemon: Encargada de buscar en el diccionario de diccionarios los datos de un Pokémon a partir de su nombre y devuelve un objeto Pokémon.

  • main: La función principal del programa. Se encarga de cargar el archivo, solicitar al usuario el nombre del Pokémon que quiere buscar y, si la información es correcta, mostrar la imagen en pantalla.

gui_main.py: Este archivo no se utilizará, dado que corresponde a la solución del tutorial que desarrollaremos con Flet.

Si desea detalles adicionales, revise los comentarios incluidos en cada uno de los archivos.

Paso #2 - Interfaz en Flet

Crear un nuevo archivo main

Antes de comenzar a desarrollar la interfaz, es necesario instalar Flet. Para ello, ejecute la siguiente instrucción en su terminal:

$ pip install flet

Para comenzar, cree un nuevo archivo llamado main.py y copie en él el siguiente segmento de código:

import flet as ft
import load as cg
import pokemon as pk

def search_pokemon(pokemons: dict, name: str) -> pk.Pokemon:
    """
    Busca un Pokémon por su nombre en un diccionario de Pokémon.

    Args:
    - pokemons (dict): Un diccionario que contiene los datos de los Pokémon, donde las claves son los nombres de los Pokémon.
    - name (str): El nombre del Pokémon que se desea buscar.

    Returns:
    - pk.Pokemon or None: Si se encuentra el Pokémon, devuelve un objeto pk.Pokemon con los datos correspondientes.
                          Si no se encuentra, devuelve None.
    """
    pokemon = pokemons.get(name)

    if pokemon != None:        
        pokemon = pk.Pokemon(name, pokemon["Type1"], pokemon["Type2"], "\\images\\" + name + ".png")  
        return pokemon

    return None

Como puede observar, este código tiene solo una pequeña modificación. La primera línea importa Flet, mientras que el resto del código mantiene la función search_pokemon y los mismos imports que utilizamos en la versión de terminal previa.

import flet as ft

Ahora, escribiremos una nueva función main. Agregue el siguiente bloque de código debajo de la función search_pokemon.

def main(page: ft.Page):
    pass

ft.app(
    target = main,
    assets_dir = "poke_db"
)

Este bloque de código dibuja solo una ventana con Python y Flet. Analicemos línea por línea:

  • def main(page: ft.Page): La función main() es el punto de entrada en una aplicación de Flet.

  • (page: ft.Page) indica que la función toma un argumento llamado page que es del tipo ft.Page. ft.Page es un objeto de la biblioteca Flet que representa la página principal de la aplicación. page es un lienzo específico para un usuario. Para construir una interfaz de usuario de la aplicación, agrega y elimina controles a un page, y actualiza sus propiedades.

  • pass es una palabra clave que indica que la función no hace nada en este momento. Es como un placeholder para el código que se agregará más adelante.

  • ft.app( target = main, assets_dir = "poke_db" ):ft.app es una función de la biblioteca Flet que inicia la aplicación.

  • target = main le dice a la función ft.app que use la función main como el punto de entrada de la aplicación. Cuando alguien acceda a la aplicación, se llamará a la función main con una nueva instancia de ft.Page.

  • assets_dir = "poke_db" le indica a la aplicación que use la carpeta poke_db para almacenar los recursos de la aplicación, como las imágenes de los pokémon, y el archivo CSV.

El ejemplo de código anterior mostrará solo una interfaz vacia a cada usuario sin propiedades.

Definir los elementos de la interfaz (Controles)

Ahora añadiremos nuevas líneas dentro de la función main. Modifique el bloque de código de la función main para que sea equivalente al siguiente:

def main(page: ft.Page):
    pokemon_list = cg.load_file("poke_db\\pokemon.csv")

    pokemon_name = ft.TextField(label="Pokemon name", autofocus = True)
    greetings = ft.Column()
    image = ft.Image(src = "\\images\\empty.png")

    page.add(
        pokemon_name,
        ft.ElevatedButton("Pokemon Search"),
        greetings,
        image,        
    )

La interfaz de usuario está compuesta por Controles (widgets). Para que los controles sean visibles para un usuario, deben agregarse a una Page o dentro de otros controles. El Page es el control más alto. Anidar controles unos dentro de otros se puede representar como un árbol con el Page como raíz.

Los controles son simplemente clases en Python. Se crean instancias (objetos) de un control mediante sus constructores. Este bloque de código dibuja los elementos de la interfaz. Analicemos línea por línea:

  • Utilizamos cg.load_file() para cargar la lista de Pokémon desde el archivo CSV llamado pokemon.csv.

Crear la interfaz de usuario:

  • Con ft.TextField(), creamos un cuadro de texto llamado pokemon_name, que hará las veces de caja de búsqueda con el rótulo Pokemon name, al ft.TextField le habilitamos la activación automática del foco. La activación automática de foco indica que el cursor estará activo por defecto en esta caja de texto.

  • Definimos un contenedor ft.Column() llamado greetings para almacenar dinámicamente la información que se mostrará sobre el Pokémon buscado.

  • Por último, agregamos un ft.Image() y lo llamamos image. A este por defecto le asignamos la ruta a una imagen vacía (\\images\\empty.png). A futuro se reemplaza esta imagen vacía con la imagen del Pokémon buscado por el usuario.

Organizar la página principal:

  • Mediante page.add(), le indicamos a la página principal de la aplicación que muestre los elementos previamente creados: el ft.TextFielda, un botón de búsqueda con la etiqueta Pokemon Search, el contenedor de información y la imagen vacía. Con esto ya tenemos el esqueleto de la interfaz.

  • Algunos controles, como los botones, podrían tener controladores de eventos que reaccionen ante una entrada del usuario. Por el momento no hemos especificado los eventos que gestionara nuestro botón.

Si ejecutamos el programa, el resultado será similar a lo siguiente:

Ajustar las propiedades de la interfaz

Ahora vamos a ajustar el tamaño de la interfaz, para esto vamos a modificar algunos atributos de Page en la función main. Modifique el bloque de código de la función main para que sea equivalente al siguiente:

def main(page: ft.Page):
    page.window_width = 320
    page.window_height = 320
    page.window_resizable = False
    page.padding = 24
    page.margin = 24

    pokemon_list = cg.load_file("poke_db\\pokemon.csv")

    pokemon_name = ft.TextField(label="Pokemon name", autofocus = True)
    greetings = ft.Column()
    image = ft.Image(src = "\\images\\empty.png")

    page.add(
        pokemon_name,
        ft.ElevatedButton("Pokemon Search"),
        greetings,
        image,        
    )

Este bloque de código realiza los siguientes ajustes a la interfaz. Analicemos línea por línea:

  • page.window_width = 320 y page.window_height = 320. Establecen el tamaño de la ventana a 320 x 320 píxeles.

  • page.window_resizable = False no permite que el usuario realice ajustes manuales a la ventana, esta conservara el tamaño fijo previamente definido.

  • page.padding = 24 y page.margin = 24 representan las márgenes de la aplicación, así la interfaz se ve menos saturada.

Si ejecutamos el programa, el resultado será similar a lo siguiente:

Administrar el evento del botón de búsqueda

Ahora, vamos a hacer que el botón de la interfaz busque un Pokémon y lo dibuje en la ventana. Para lograrlo, modificaremos y agregaremos algunas líneas de código a la función main.

La primera línea que modificaremos es la creación del botón, que se encuentra en la línea 40 de la imagen anterior. Ajustaremos esta línea para agregar el evento on_click.

ft.ElevatedButton("Pokemon Search", on_click = btn_click),

Esta línea de código continua creando el botón con el texto Pokemon Search y un llamado a la función btn_click cuando se genere el evento on_click. El parámetro on_click especifica que se debe llamar a la función btn_click cuando se hace clic en el botón.

Nota: El botón se genera directamente en la interfaz sin ser almacenado en una variable específica. No obstante, esto no impide que declaremos una variable que represente a este botón y luego lo agreguemos a la página. Por ejemplo:

un_boton = ft.ElevatedButton("Pokemon Search", on_click = btn_click) 

page.add(
        un_boton,
    )

Modifique el bloque de código de la función main para que sea equivalente al siguiente:

def main(page: ft.Page):
    page.window_width = 320
    page.window_height = 320
    page.window_resizable = False
    page.padding = 24
    page.margin = 24

    pokemon_list = cg.load_file("poke_db\\pokemon.csv")

    pokemon_name = ft.TextField(label="Pokemon name", autofocus = True)
    greetings = ft.Column()
    image = ft.Image(src = "\\images\\empty.png")

    def btn_click(e):      
        pokemon = search_pokemon(pokemon_list, pokemon_name.value.lower())
        if search_pokemon(pokemon_list, pokemon_name.value.lower()) != None:                    
            greetings.controls.clear()
            greetings.controls.append(ft.Text(f"Hello, {pokemon.get_name().capitalize()}!\nType: {pokemon.get_type1()}"))        
            image.src = pokemon.get_img()

        pokemon_name.value = ""        
        page.update()
        pokemon_name.focus()

    page.add(
        pokemon_name,
        ft.ElevatedButton("Pokemon Search", on_click = btn_click),
        greetings,
        image,                    
    )

Escribir una función dentro de otra función en Python se conoce como una función anidada o función interna. Esto se hace por varias razones:

  • Encapsulamiento: Las funciones internas pueden encapsular lógica específica que solo es relevante para la función externa.

  • Privacidad de alcance (scope): Las funciones internas tienen acceso al alcance (scope) de la función externa, lo que significa que pueden acceder a las variables locales de la función externa. Esto puede ser útil para realizar operaciones específicas dentro de la función externa sin exponer esas operaciones al alcance global.

La función btn_click es responsable de manejar la lógica que debe ejecutarse cuando se hace clic en el botón.

  • def btn_click(e): Esta línea define una función llamada btn_click que toma un objeto de evento e como argumento. El objeto e contiene información sobre el evento que desencadenó la llamada a la función, como el tipo de evento y el origen del evento.

  • pokemon = search_pokemon(pokemon_list, pokemon_name.value.lower()): Esta línea llama a una función llamada search_pokemon para buscar un Pokémon en pokemon_list con el nombre ingresado en el campo de texto pokemon_name. La función devuelve el objeto Pokémon si se encuentra, o None si no se encuentra.

  • if search_pokemon(pokemon_list, pokemon_name.value.lower()) != None: Este condicional verifica si se encontró el Pokémon. Si se encontró el Pokémon, se ejecutará el código dentro de la declaración del condicional para actualizar la interfaz, de lo contrario se mantiene el estado anterior de la interfaz.

  • greetings.controls.clear(): Esta línea borra los controles del contenedor greetings. Esto significa que se eliminarán todos los controles que se hayan agregado previamente en este contenedor.

  • greetings.controls.append(ft.Text(f"Hello, {pokemon.get_name().capitalize()}!\nType: {pokemon.get_type1()}")): Esta línea agrega un nuevo control de texto al contenedor greetings. El control de texto muestra un saludo al Pokémon y su tipo.

  • image.src = pokemon.get_img(): El método get_img() recupera la ruta de la imagen del Pokémon solicitado. Esta línea actualiza la ruta del control de imagen que dibuja en pantalla el Pokémon.

  • pokemon_name.value = "": Esta línea borra el valor del campo de texto pokemon_name. Esto significa que el campo de texto estará vacío después de hacer clic en el botón Pokemon Search.

  • page.update(): Esta línea actualiza el Page para reflejar los cambios que se han realizado.

  • pokemon_name.focus(): Esta línea establece el foco nuevamente en el campo de texto pokemon_name. Esto significa que el cursor estará ubicado nuevamente en el campo de texto.

Si ejecutamos el programa, el resultado será similar a lo siguiente::

Convertir la aplicación de escritorio a una aplicación web

Ahora, ajustaremos la aplicación para que se despliegue como una aplicación web. Modifique el siguiente bloque de código para que sea equivalente al que se muestra a continuación:

ft.app(
    target = main,
    view = ft.WEB_BROWSER,
    assets_dir = "poke_db"
)

Este bloque de código crea una aplicación Flet:

  • view: Este valor representa la vista que se utilizará para mostrar la aplicación. En este caso, la vista se configura con el valor ft.WEB_BROWSER, lo que significa que la aplicación se mostrará en un navegador web.

Si ejecutamos el programa, el resultado será similar a lo siguiente:

Referencias

Did you find this article valuable?

Support Jesse Padilla by becoming a sponsor. Any amount is appreciated!