Introducción a los eventos de ratón (mouse) y teclado con Tkinter – 100 días de Python #12

Introducción a los eventos de ratón (mouse) y teclado con Tkinter – 100 días de Python #12

Introducción a los eventos de ratón (mouse) y teclado con Tkinter

Los eventos de ratón y teclado con Tkinter van a ser una parte importante en este curso, ya que nos permitirán hacer que nuestro programa pueda reaccionar a cosas como pulsar un botón del ratón o una tecla del teclado, desencadenando acciones.

Estos eventos requieren de una función de devolución (recuerda return en las funciones). Es decir, que cuando hagamos una acción sobre un widget, por ejemplo con el click izquierdo del ratón, que este llame a una función y devuelva algo al programa, provocando algún tipo de cambio.

Realmente esto no es tan complicado como pueda parecer con esta explicación teórica.

Antes de empezar, te suelto un dato extra, para aprovechar el día y que aprendas alguna cosa más. Aunque puede que ya lo supieras.

¿Qué es GUI?

GUI son las siglas en inglés de Graphical User Interface. Interfaz Gráfica de Usuario en español.

Este término es el que se utiliza frecuentemente para describir los programas de interfaz gráfica como los de Tkinter. Te lo indico porque usaré el término de ahora en adelante y lo verás en cualquier información que busques sobre programas como los de Tkinter.

Ahora sí, continuemos con los eventos.

El atributo command de Tkinter

En Tkinter, algunos widgets poseen el atributo "command" con el que podemos asignar un evento al mismo. Por ejemplo, que al pulsar un botón, que se reproduzca un archivo de música. Cualquier acción que queramos que haga.

El widget Button() de Tkinter

Para realizar los ejemplos, vas a ver un widget nuevo en el curso. El widget Button() de Tkinter.

Vamos a hacer algo sencillo para que te quede todo este tema quede más claro.

Creamos un simple botón en la línea 13. Gracias al atributo "text", le pongo un texto.

Por cierto. Los widgets, si solo los queremos declarar y mostrar en la ventana, no hace falta que los guardemos en variables si no van a ser llamados en otro lado del código. Por ejemplo, la ventana Tk() (la principal) está en una variable llamada "root", ya que después, la necesito para aplicarle métodos de Tkinter como title() o mainloop().

#Importaciones
from tkinter import *

#Creación de la ventana principal
root = Tk()
#Título de la ventana
root.title("Curso de Tkinter de Programación Fácil")

# Botón
Button(root, text="¡Púlsame!").pack()

#Bucle de ejecución
root.mainloop()

Nos aparece un botón con el texto, que puede ser presionado, pero no tiene ninguna funcionalidad, no hace nada, no tiene un evento que desencadene alguna acción cuando lo pulsemos. Por lo tanto, hay que indicarle a Python, de alguna forma, algo como esto: Oye Python, cuando alguien pulse este botón, quiere que hagas esto.

Esto se consigue con funciones. Ya has visto en el curso, que las funciones sirven para que el programa efectúe acciones cuando son llamadas.

Ahí está la función. Esta, cuando es llamada, debería mostrar este print() en la consola.

def pulsar_boton():
    print("Botón pulsado.")

Todo esto está muy bien, pero ¿cómo le indicamos a Python que debe llamar a esa función solo cuando pulsemos el botón izquierdo del ratón sobre el botón?

Fácil, asociándole un evento directamente al botón. El evento será que llame a la función.

Gracias al atributo "command" anteriormente mencionado, podemos hacerlo.

Quedaría de esta forma:

# Botón
def pulsar_boton():
    print("Botón pulsado.")

Button(root, text="¡Púlsame!", command=pulsar_boton).pack()

Por cierto, fíjate que en la llamada a la función desde el "command", no utilizo paréntesis como si haríamos desde fuera con cualquier llamada a función.

Cada vez que pulsamos el botón se desencadena un evento. En este caso, es solo un print(), pero puede ser todo el código que quieras añadir a la función que es llamada con el "command".

No es tan difícil como parecía al principio, ¿verdad?

Tkinter command - Paréntesis en la llamada de las funciones

¿Por qué la llamada no se realiza con los paréntesis en el "command"?

Cuando llamamos a cualquier función de Python, lo hacemos con los paréntesis. ¿Qué ocurre si lo hacemos también en el "command"?

Button(root, text="¡Púlsame!", command=pulsar_boton()).pack()

Nos aparece el print() (se desencadena el evento) al ejecutar el programa y no nos deja desencadenarlo por nuestra cuenta. Cada vez que pulsamos el botón, no hace nada.

Generar widgets de Tkinter a partir de eventos

Con Tkinter, podemos generar a partir de un widget otros widgets que no existen hasta que se desencadena un evento.

Hagamos que la función "pulsar_boton()" en lugar de ejecutar un print(), muestre una etiqueta en la ventana cada vez que pulsamos el botón (widget Label).

Solo hay que cambiar el print por el Label y mostrarlo con un pack(). Puesto que está en una función, no se carga en la ventana hasta no ser llamada.

def pulsar_boton():
    Label(root, text="Botón pulsado.").pack()

Entrada de datos con Tkinter

Continuemos ahora con la entrada de datos al programa con Tkinter.

Hasta ahora, hemos utilizado el método input() de Python con el fin de introducir datos desde la consola. Ha llegado el momento de poder hacer esto con un programa con interfaz gráfica.

El widget Entry de Tkinter

El widget Entry de Tkinter, es el que nos permitirá meter datos en el programa. Creemos uno. Lo voy a situar encima del botón.

#Entrada de datos
entrada = Entry(root).pack()

Nos queda un rectángulo de texto en el que podemos escribir.

Una vez creado, habrá que crear una forma de almacenar lo que escribimos y un evento para que esto pueda ocurrir. Esto lo conseguiremos añadiendo más código a la función "pulsar_boton". Puedes dejar la etiqueta para avisar de que se ha pulsado el botón. El código, de momento, será este:

#Importaciones
from tkinter import *

#Creación de la ventana principal
root = Tk()
#Título de la ventana
root.title("Curso de Tkinter de Programación Fácil")

#Entrada de datos
entrada = Entry(root).pack()

# Evento para el botón
def pulsar_boton():
  	texto = entrada.get()
    Label(root, text="Botón pulsado.").pack()

# Botón
Button(root, text="¡Púlsame!", command=pulsar_boton).pack()

#Bucle de ejecución
root.mainloop()

Fíjate en la línea 14. He creado una función que almacena el valor que se escribe en el Entry() gracias a su método get().

Este es el equivalente en Tkinter a esto:

nombre = input()

Entonces, cuando llamemos a la función, se guarda en la variable "texto", el valor escrito. ¿Cuándo? Cuando llames a la función "pulsar_boton" con el botón.

Por el momento, nos avisa de que se ha pulsado el botón:

El texto se ha guardado, pero no se borra ni nos avisa de nada. Comprobemos en la consola si se va guardando lo que escribimos. El print() es el que lo mostrará.

#Entrada de datos
entrada = Entry(root).pack()

# Evento para el botón
def pulsar_boton():
    # Se obtiene el valor del Entry
    texto = entrada.get()
    # Se imprime en la consola el valor del Entry
    print(f"El valor del Entry es: {texto}.")
    # Se avisa de que el botón ha sido pulsado
    Label(root, text="Botón pulsado.").pack()

# Botón
Button(root, text="¡Púlsame!", command=pulsar_boton).pack()

Al ejecutar esto, nos da un error de atributo. El objeto de clase NoneType no tiene el atributo .get().

Error en la consola

AttributeError: 'NoneType' object has no attribute 'get'

Problemas con pack y grid en Tkinter

Entonces, si tenemos un objeto de la clase Entry de Tkinter, ¿por qué lo que hay en entrada es un objeto de la clase NoneType de Python?

Revisemos el tipo que devuelve la variable con el objeto Entry().

print(type(entrada))

Resultado en la consola

<class 'NoneType'>

Efectivamente, esto es un NoneType.

Esto ocurre debido al .pack().

Al intentar utilizar esto:

texto = entrada.get()

Es como si hiciera esto:

entrada = Entry(root).pack().get()

Este problema lo produce el hecho de poner el pack() o grid() para mostrar el elemento en la misma línea. Cuando utilices "command", siempre utiliza la forma de dos líneas que he mostrado ya en el curso.

Para solucionar esto, cuando necesitemos utilizar métodos con un widget, lo ponemos el pack() en otra línea, como hacíamos en capítulos anteriores.

#Entrada de datos
entrada = Entry(root)
entrada.pack()

Resultado en la consola

El valor del Entry es: Programación Fácil :D.

Ya estamos entrando datos desde una interfaz gráfica.

Viendo la clase Entry de Tkinter

Tanto el widget Entry como cualquier otro, son clases con sus respectivos métodos. El ".get()" aplicado en el Entry() para obtener el valor escrito en el cuadro de texto, es solo eso. Ten en cuenta ir mirando el código interno cuando quieras profundizar un poco más en lo que haces. También podrás ver qué métodos tiene cada widget.

Además, si miras el comentario tan grande que lleva. Verás ahí una referencia de los atributos que podemos usar con el widget. Cosas como "background", "font", etc.

Estos los veremos a partir del siguiente capítulo.

Los comentarios multilínea de Python

Por cierto, no había especificado todavía los comentarios multilínea. Hasta ahora, solo los de una línea. Pues ya que los tiene Tkinter en su código interno, aprovecho para que los veas. Se trata de poner tres comillas simples o dobles de apertura y tres más de cierre.

Utilizar los datos del Entry en la propia GUI

Utilizar los datos introducidos por el usuario internamente en le programa y en la consola, está muy bien para realizar pruebas al desarrollar algo. Sin embargo, seguro que querrás poder mostrar cosas en la ventana del programa.

Solo tienes que cambiar el código que tiene la etiqueta. Cuando se pulse el botón, que muestre el valor de la variable "entrada".

#Entrada de datos
entrada = Entry(root)
entrada.pack()

#Evento para el botón
def pulsar_boton():
    #Se obtiene el valor del Entry
    nombre = entrada.get()
    #Se muestra en una etiqueta el valor del Entry
    Label(root, text=f"Hola {nombre}.").pack()

#Botón
Button(root, text="¡Púlsame!", command=pulsar_boton).pack()

Lo ejecuto y le pongo un nombre. Pulso el botón y dice "Hola Quique", pero esta vez, los datos salen en la ventana. Después, le pongo Paula y Javier. El dato de la entrada va cambiando cada vez.

Mensaje por defecto en el widget Entry de Tkinter

El tener un cuadro de texto donde poder escribir, está muy bien, pero, ¿no sería mejor que pusiese algo para que el usuario supiera que debe escribir en él? Algo así como, "Escriba su nombre...".

Esto lo podemos hacer mediante el uso del método insert(). Le paso dos argumentos al método. 0 y el string que se va a mostrar.

#Entrada de datos
entrada = Entry(root)
entrada.insert(0,"Escriba su nombre...")
entrada.pack()

Las posiciones del widget Entry de Tkinter

¿Qué representa este cero?

Vayamos a la referencia de Tkinter de nuevo. Este método espera el argumento "index" y un string. Por lo tanto, vemos que lo que se espera es un valor de índice.

def insert(self, index, string):
        """Insert STRING at INDEX."""
        self.tk.call(self._w, 'insert', index, string)

Los strings, como hemos visto, tienen índice de posiciones. Son elementos iterables. Cada carácter es una posición en el índice. Las listas y otros también.

Pues el método insert() de Entry, espera una posición de índice desde el 0.

Puesto que quiero que el mensaje aparezca a partir de esa posición. Pongo el 0. Puedes ver, que el carácter "E", queda en la posición 0, la más a la izquierda posible.

Si pruebas de poner otra posición que no sea la cero, te lo va a colocar en la cero si no hay otros elementos. Lo pongo en la 20 y la ventana se carga igual.

#Entrada de datos
entrada = Entry(root)
entrada.insert(20,"Escriba su nombre...")
entrada.pack()

Hagamos una prueba. Vamos a utilizar otro insert() para meter en el mismo Entry otro mensaje en diferente posición. Así podrás ver con tus propios ojos que esto funciona como te digo. Le pongo el string "PF" en la posición 7.

#Entrada de datos
entrada = Entry(root)
entrada.insert(0,"Escriba su nombre...")
entrada.insert(7,"PF")
entrada.pack()

El resultado es que "PF" se coloca justo en la posición 7 (contando desde el cero). Lo bueno es que no eliminamos ni un espacio del primer insert(), solo lo añadimos y no reemplazamos nada.

Una vez comprendes que los cuadros de texto en Tkinter van con posiciones, estoy seguro de que te será más fácil entender el uso de ciertas cosas que normalmente no se explican.

Ahora, se puede borrar con el teclado este mensaje por defecto y escribir un nombre. No obstante, seguro que la mayoría de veces, cuando ves un formulario de este tipo (cuadro de texto) y te sale un mensaje como "Escriba su nombre...", lo normal es que al hacer click para ir a escribir, se borre y deje el cuadro vacío.

El uso del método bind con Entry en Tkinter

Tengo la solución a este problema. Hay que perfeccionar el código. Lo haremos con un nuevo método de Entry.

Con el widget Entry, podemos utilizar un método llamado bind().

Este método nos permite crear un evento sobre una sola línea con una función lambda. Uno de los motivos por el cual realice el día número 10 con este tipo de funciones.

De todas formas, se puede utilizar sin lambda.

Para este bind(), voy a añadir dos argumentos. El botón que quiero que desencadene el evento y la llamada a otro método como argumento del primero.

Puede que se vea una locura, pero es muy práctico.

Fíjate en la línea 4:

#Entrada de datos
entrada = Entry(root)
entrada.insert(0,"Escriba su nombre...")
entrada.bind("<Button-1>", entrada.delete(0, END))
entrada.pack()

Al ejecutar esto, el cuadro, aparece automáticamente vacío. Esto es porque en el segundo argumento, le estamos diciendo que llame al método delete() de Entry y borre desde la posición 0 hasta la última. (END es una constante de Tkinter para representar "len(elemento)-1").

El botón <Button-1> es un botón preestablecido para indicar que se utilice el click izquierdo del ratón. Con <Button-2>, el botón de la rueda y con <Button-3>, el click derecho.

Traducido de manera muy simple, indicamos, que cuando se pulse el click izquierdo del ratón sobre el elemento entrada (el cuadro de texto), que se eliminen las posiciones de caracteres de la 0 (principio) hasta el fin (END).

Desafortunadamente, esto no funciona como podríamos esperar.

Para que funcione, necesitamos una forma de que se llame al método "entrada.delete(0, END)" solo cuando pulsemos el botón.

Esto se puede hacer sobre los argumentos con una función lambda.

El parámetro, lo puedes llamar como quieras, no tiene mucha importancia, ya que solo sirve para pasarle la llamada que nos interesa.

#Entrada de datos
entrada = Entry(root)
entrada.insert(0,"Escriba su nombre...")
entrada.bind("<Button-1>", lambda despejar : entrada.delete(0, END))
entrada.pack()

Recuerda que las funciones lambda hacían el return automáticamente. En este caso, cuando pulsas el botón, llama a la función lambda y elimina el texto del cuadro.

Para que veas clara la sintaxis y simplifiquemos esto, bind() espera esto:

widget.bind(evento, controlador)

Con esta sintaxis, se puede reemplazar widget por cualquiera que tenga sentido, como el Entry. Los argumentos son, evento (la tecla o click que queremos) y el controlador, no es más que una función. Si en el controlador utilizas una llamada a un método como delete(), esta llamada se hace automáticamente al cargar esa línea de código. Sin embargo, el controlador, si tiene una lambda, esperará a que desencadenemos el evento (click o tecla) para realizar la llamada.

Utilizar cualquier tecla como evento en bind

Una cosa interesante que podemos poner como evento, es todo el teclado. En lugar de poner un string con un botón o tecla en concreto, podemos dejar siguiente evento para que se desencadene al pulsar la que sea. Filosofía del típico "Press any key...".

#Entrada de datos
entrada = Entry(root)
entrada.insert(0,"Escriba su nombre...")
entrada.bind("<Key>", lambda despejar : entrada.delete(0, END))
entrada.pack()

En esta ocasión, esto no tendrá sentido, ya que nos borrará el mensaje por defecto con la primera tecla que presionemos. Sin embargo, cada tecla que utilices para escribir, borrará la anterior.

Solo recuerda este evento para más adelante. Le daremos mejores usos.

Tecla backspace como evento en bind

Algo que quizás tendría mayor sentido, es utilizar la tecla backspace (retroceso) como evento, de ese modo, se borrará todo el texto solo con pulsarla.

entrada.bind("<BackSpace>", lambda despejar : entrada.delete(0, END))

Ya habrá tiempo de ver más eventos. Te dejo el código completo del capítulo para que puedas revisarlo con calma.

#Importaciones
from tkinter import *

#Creación de la ventana principal
root = Tk()
#Título de la ventana
root.title("Curso de Tkinter de Programación Fácil")


#Entrada de datos
entrada = Entry(root)
entrada.insert(0,"Escriba su nombre...")
entrada.bind("<Button-1>", lambda despejar : entrada.delete(0, END))
entrada.pack()

#Evento para el botón
def pulsar_boton():
    #Se obtiene el valor del Entry
    texto = entrada.get()
    #Se muestra en una etiqueta el valor del Entry
    Label(root, text=f"Hola {texto}.").pack()

#Botón
Button(root, text="¡Púlsame!", command=pulsar_boton).pack()

#Bucle de ejecución
root.mainloop()

Te dejo tres ejercicios para que practiques un poco toda esta teoría.

Un comentario en «Introducción a los eventos de ratón (mouse) y teclado con Tkinter – 100 días de Python #12»

  1. Buenos Dias
    Como se evita si tenemos dos eventos bind que estos se ejecuten al la vez?
    Ejemplo
    nde inicia el texto y que texto nos mostrara al inicio
    entrada1=Entry(root)root = Tk()
    root.wm_title(«Suma de numeros»)
    root.wm_geometry(centrar(root,1000,500))

    def val1(wid):
    if wid.get()==»»:
    print(«no puede estar en blanco»)
    wid.focus_set()
    if len(wid.get())<5:
    print("no puede ser menor de 5 caracteres")
    wid.focus_set()
    def hola(aaa):
    print(aaa)
    x=0
    entrada=Entry(root)
    entrada.pack()
    entrada.bind("», lambda c:val1(entrada))
    entrada.insert(0,»Cristian») #posicion do
    entrada1.pack()
    entrada1.bind(«», lambda d:hola(«hola»))
    bsalir=Button(root,text=»Salir»,width=15, command=lambda:root.destroy()).place(x=870, y=460)

    root.mainloop()

    Cuando vuelvo al primer entry me ejecuta las dos funciones indicadas en bind y Entry distintos.
    Desde ya muchas Gracias por la ayuda

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

curso de Python Entrada anterior Soluciones de ejercicios de programación orientada a objetos – 100 días de Python #11
curso de Python Entrada siguiente Ejercicios de Programación de eventos con Tkinter – 100 días de Python #12