Messagebox Tkinter, centrado de ventana y seguridad informática

Messagebox Tkinter, centrado de ventana y seguridad informática

Los messagebox de Tkinter, centrado de ventana y seguridad informática - 100 días de Python #18

En esta ocasión aprenderás a utilizar los messagebox, también temas algo más técnico sobre seguridad informática y finalmente, el centrado de ventanas en la pantalla. Así que viene un día muy cargado.

Empecemos por los messagebox.

¿Qué son los MessageBox?

Los "messagebox" de Tkinter son ventanitas desplegables con alertas, información, errores, preguntas, etc.

Importar MessageBox en Tkinter

La importación de "messagebox" la puedes hacer con la siguiente línea:

from tkinter.messagebox import *

Ahora, vamos a ver si funciona con algo simple.

MessageBox showinfo tkinter

En la función, he incluido un "showinfo" que va a mostrar un mensaje informativo cuando se pulse el botón.

#Importaciones
from tkinter import *
from tkinter.messagebox import *

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

#Función para mensaje informativo
def muestra_info():
   showinfo("Aquí se escribe el título de la ventana",
            "Este es el mensaje que se muestra en la ventana.")

#Botón para llamar a la función
boton = Button(root, text="Enviar", command=muestra_info, width=75).pack()

#Bucle de ejecución
root.mainloop()
messagebox tkinter showinfo

El primer argumento que le he dado al showinfo() es correspondiente al título. El segundo al contenido del mensaje.

Estos mensajes son propios del sistema operativo. Por ejemplo, en Windows, con un mensaje informativo nos sale así. Nos muestra una ventana con un icono de información y nos aparece el botón de aceptar que no hemos creado nosotros.

Además, el sistema operativo da unos sonidos diferentes para cada tipo de mensaje, estos MessageBox sacarán esos sonidos diferentes.

Al darle a aceptar, esta ventana se cerrará y podemos seguir utilizando el programa.

MessageBox showwarning tkinter

También contamos con la ventana showwarning(), la cual nos saca un mensaje de advertencia.

#Función para mensaje de advertencia
def muestra_alerta():
   showwarning("¡Cuidado!",
            "La acción que está tratando de realizar, no se puede deshacer.")

#Botón para llamar a la función
boton = Button(root, text="Enviar", command=muestra_alerta, width=75).pack()
messagebox tkinter showwarning

MessageBox showerror tkinter

Este tipo de mensaje soltará un error.

#Función para mensaje informativo
def muestra_error():
   showerror("¡Error!",
            "Imposible realizar esa acción. Error 307, compruebe la referencia para solucionarlo.")

#Botón para llamar a la función
boton = Button(root, text="Enviar", command=muestra_error, width=75).pack()
messagebox tkinter showerror

messagebox askquestion tkinter

El mensaje askquestion() va a realizar una pregunta de si o no al usuario.

#Función para mensaje informativo
def pregunta():
   askquestion("¿Sabías que...",
            "... el cielo lo vemos azul debido a la dispersión de la luz solar?")

#Botón para llamar a la función
boton = Button(root, text="Enviar", command=pregunta, width=75).pack()
messagebox tkinter askquestion

Con este código, no vamos a hacer nada tanto si se pulsa "Sí" como "No", no obstante, hoy verás como añadir condicionales para estos botones, así el programa podrá interactuar con estos mensajes.

messagebox askyesno tkinter

Podemos realizar el mismo mensaje que el anterior con un askyesno(). El resultado es el mismo que askquestion(), al menos de cara al usuario. Explicaré esto en la parte de los ejercicios.

def pregunta():
   askyesno("¿Sabías que...",
            "... el cielo lo vemos azul debido a la dispersión de la luz solar?")

#Botón para llamar a la función
boton = Button(root, text="Enviar", command=pregunta, width=75).pack()

messagebox askokcancel

Este mensaje va a mostrar una ventana de pregunta con los botones "Aceptar" y "Cancelar".

#Función para mensaje informativo
def pregunta():
   askokcancel("¿Seguro que desea continuar?",
            "La acción que va a realizar podría comprometer la integridad de la base de datos.")

#Botón para llamar a la función
boton = Button(root, text="Enviar", command=pregunta, width=75).pack()
messagebox tkinter askokcancel

messagebox askyesnocancel

Esta ventana mostrará una pregunta con las opciones de "Sí", "No" y "Cancelar".

#Función para mensaje informativo
def pregunta():
   askyesnocancel("¿Seguro que desea continuar?",
            "La acción que va a realizar podría comprometer la integridad de la base de datos.")

#Botón para llamar a la función
boton = Button(root, text="Enviar", command=pregunta, width=75).pack()
messagebox tkinter askyesnocancel

messagebox askretrycancel

Llegamos al último tipo de ventana. Esta mostrará una pregunta en forma de advertencia, con las opciones "Reintentar" y "Cancelar".

#Función para mensaje informativo
def pregunta():
   askretrycancel("¿Seguro que desea continuar?",
            "La acción que va a realizar podría comprometer la integridad de la base de datos.")

#Botón para llamar a la función
boton = Button(root, text="Enviar", command=pregunta, width=75).pack()

Pasemos al siguiente apartado del día, el centrado de las ventanas de Tkinter.


Centrar la ventana de Tkinter con el método PlaceWindow de Tkinter

El método PlaceWindow de Tkinter, es un método que nos sirve para centrar de manera fácil la ventana del programa. Es posible que de hecho, a esta altura del curso, ya hayas buscado en alguna otra página como centrar la ventana del programa, porque te moleste enormemente que aparezca de manera aleatoria en la pantalla.

Aquí tienes un ejemplo de como probarlo:

#Importaciones
import tkinter as tk

#---VENTANA PRINCIPAL----> root

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

root.geometry()

#Ajustes de ventana y pantalla
root.eval('tk::PlaceWindow . center')

#---WIDGETS----> root

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

#Botón de enviar
boton = tk.Button(root, text="Enviar").pack()

#Bucle de ejecución
root.mainloop()

Para usar este método un tanto especial, hay que utilizar el método eval() de Tkinter. El método eval() es un método muy peculiar, capaz de pasar una expresión entera de código como cadena de caracteres y ejecutarla.

¿De dónde sale PlaceWindow?

Te habrás fijado que PlaceWindow tiene una sintaxis un poco rara, que no se ha dado hasta ahora en el curso.

¿Te has fijado alguna vez en que se denomina a Tkinter como Tcl/Tk?

Esto es por que Tkinter no está escrito solo con código Python. También utiliza código TCL. Otro lenguaje de programación diseñado entre otras cosas, para el desarrollo de interfaces gráficas como la de Tkinter.

Así pues, hoy vamos a indagar un poco en el código que utiliza internamente Tkinter.

Extensión para programa en TCL en Visual Studio Code

Para ello, necesitas primero instalar una extensión con la que poder leer y editar archivos TCL correctamente en Visual Studio Code.

Extensión TCL para Visual Studio Code.

Una vez instalada, vamos a ver uno de los archivos más importantes de Tkinter, el cual, lleva ese método "PlaceWindow".

Ves a la siguiente ruta. Puede variar según la versión de Python que tengas. Recuerda cambiar "PF" por tu nombre de usuario.

D:\Users\PF\AppData\Local\Programs\Python\Python310\tcl\tk8.6

En esa carpeta, hay un montón de módulos para Tkinter. Nos interesa el archivo tk.tcl. Ábrelo con Visual Studio Code.

archivo tk.tcl de Tkinter

Al abrir este archivo, pulsa en VSCode las tecla CRTL + F para buscar el término "placewindow".

Llegarás al proc (procedimiento) que hace posible centrar la ventana. Por supuesto, no vamos a entrar en detalles sobre el lenguaje TCL, así que voy a hablar sobre esto muy por encima, para que se pueda entender fácilmente.

proc ::tk::PlaceWindow {w {place ""} {anchor ""}} {
    wm withdraw $w
    update idletasks
    set checkBounds 1
    if {$place eq ""} {
	set x [expr {([winfo screenwidth $w]-[winfo reqwidth $w])/2}]
	set y [expr {([winfo screenheight $w]-[winfo reqheight $w])/2}]
	set checkBounds 0
    } elseif {[string equal -length [string length $place] $place "pointer"]} {
	## place at POINTER (centered if $anchor == center)
	if {[string equal -length [string length $anchor] $anchor "center"]} {
	    set x [expr {[winfo pointerx $w]-[winfo reqwidth $w]/2}]
	    set y [expr {[winfo pointery $w]-[winfo reqheight $w]/2}]
	} else {
	    set x [winfo pointerx $w]
	    set y [winfo pointery $w]
	}
    } elseif {[string equal -length [string length $place] $place "widget"] && \
	    [winfo exists $anchor] && [winfo ismapped $anchor]} {
	## center about WIDGET $anchor, widget must be mapped
	set x [expr {[winfo rootx $anchor] + \
		([winfo width $anchor]-[winfo reqwidth $w])/2}]
	set y [expr {[winfo rooty $anchor] + \
		([winfo height $anchor]-[winfo reqheight $w])/2}]
    } else {
	set x [expr {([winfo screenwidth $w]-[winfo reqwidth $w])/2}]
	set y [expr {([winfo screenheight $w]-[winfo reqheight $w])/2}]
	set checkBounds 0
    }
    if {$checkBounds} {
	if {$x < [winfo vrootx $w]} {
	    set x [winfo vrootx $w]
	} elseif {$x > ([winfo vrootx $w]+[winfo vrootwidth $w]-[winfo reqwidth $w])} {
	    set x [expr {[winfo vrootx $w]+[winfo vrootwidth $w]-[winfo reqwidth $w]}]
	}
	if {$y < [winfo vrooty $w]} {
	    set y [winfo vrooty $w]
	} elseif {$y > ([winfo vrooty $w]+[winfo vrootheight $w]-[winfo reqheight $w])} {
	    set y [expr {[winfo vrooty $w]+[winfo vrootheight $w]-[winfo reqheight $w]}]
	}
	if {[tk windowingsystem] eq "aqua"} {
	    # Avoid the native menu bar which sits on top of everything.
	    if {$y < 22} {
		set y 22
	    }
	}
    }
    wm maxsize $w [winfo vrootwidth $w] [winfo vrootheight $w]
    wm geometry $w +$x+$y
    wm deiconify $w
}

Aquí está la línea que he utilizado antes, para que la tengas más a mano:

root.eval('tk::PlaceWindow . center')

Tenemos la llamada al proc de TCL y después del punto, le indico un argumento correspondiente al parámetro {place} del proc.

En la línea 11 tienes un if que evalúa que si el argumento es "center".

También tenemos pointer.

root.eval('tk::PlaceWindow . pointer')

En este caso, nos va a sacar la ventana en torno a donde esté el cursor.

Los cálculos los hace con métodos de obtención dinámica del tamaño de la pantalla y el tamaño de la ventana. obtiene el ancho de la pantalla "winfo screenwidth" en la primera expresión, sea la pantalla que sea, con eso tenemos unas coordenadas adaptadas a cualquier pantalla y le resta el ancho que ocupa la ventana "winfo reqwidth". Divide esto entre dos y saca la coordenada central en "x", ancho.

En la segunda expresión hace lo mismo con el alto, "y". Con eso, obtiene el centro dinámicamente sea la pantalla que sea y sin importar el tamaño de la ventana.

set x [expr {([winfo screenwidth $w]-[winfo reqwidth $w])/2}]
set y [expr {([winfo screenheight $w]-[winfo reqheight $w])/2}]

Con "pointer" es parecido, solo que en este caso, hace la expresión mediante las posiciones del puntero con los métodos winfo pointer que obtienen la posición de este.

Dejamos ya TCL, al menos por el momento. Lo tocaremos de nuevo si se da el caso, aunque puede que no.

Todo esto que hace TCL se puede hacer con nuestro propio algoritmo. Esto te ayudará a entender mejor lo que hace "PlaceWindow".

Las soluciones fáciles, no siempre son las mejores

Las soluciones fáciles, no siempre son las mejores. ¿A qué me refiero con esto? Quien sabe...

Fuera bromas, la solución anterior con PlaceWindow, es aceptable, pero no tiene en cuenta la barra de título de la ventana para hacer el centro. Si quieres precisar más aún, puedes crear tu propio algoritmo mejorando el de TCL.

Lo que vas a aprender aquí, no solo te servirá para centrar una ventana. Te sirve también de práctica para empezar a fortalecer tu lógica y matemáticas.

Creemos una ventana:

#Importaciones
from tkinter import *

#Ventana principal
root = Tk()
root.title("www.programacionfacil.org")
root.resizable(False, False)
ancho_ventana = 500
alto_ventana = 400

#Bucle de la ventana principal
root.mainloop()

¿Cómo funciona resizable?

El método "resizable" de Tkinter. Espera que se le pasen dos argumentos de tipo bool. Si el primero es "False", desactiva la opción de que el usuario pueda redimensionar el ancho de la ventana. El segundo, es para desactivar el alto. Por defecto, estos valores vienen en True si no se especifica el método resize(). Puedes poner uno en "False" y otro en "True". Mira un ejemplo:

root.resizable(False, True)

En este caso, la ventana no se podrá redimensionar en ancho, pero si en alto.

¿Cómo se captura el tamaño de la pantalla?

Ya tenemos el tamaño de la ventana, pero nos falta obtener el de la pantalla. El problema de las pantallas, es que hay de muchos tipos y diferentes resoluciones. Por lo tanto, necesitas unos métodos que obtengan dinámicamente el ancho y el alto de la pantalla en píxeles.

Los métodos winfo_screenwidth y winfo_screenheight

Los métodos "winfo_screenwidth" y "winfo_screenheight" sirven para capturar el tamaño de la pantalla dinamicamente. Esto quiere decir, que obtienen el valor de ancho y alto de la pantalla en píxeles, de la resolución que sea.

#Importaciones
from tkinter import *

#Ventana principal
root = Tk()
root.title("www.programacionfacil.org")
root.resizable(False, False)
ancho_ventana = 500
alto_ventana = 400

#Pantalla
ancho_pantalla = root.winfo_screenwidth()
alto_pantalla = root.winfo_screenheight()

#Ver valores de dimensiones de la pantalla
Label(text=f"Ancho pantalla: {ancho_pantalla} píxeles.").pack()
Label(text=f"Alto pantalla: {alto_pantalla} píxeles.").pack()

#Bucle de la ventana principal
root.mainloop()

Lo siguiente, es hacer el cálculo automático mediante estos valores obtenidos. Para este cálculo, no se tiene en cuenta la barra de inicio de Windows, por lo tanto, hay que restarle ese 37 para que se ajuste mejor al centro teniendo en cuenta esto.

Los cálculos son muy parecidos a los que estaban escritos en el "proc" del lenguaje TCL que utiliza "PlaceWindow".

#Importaciones
from tkinter import *

#Ventana principal
root = Tk()
root.title("www.programacionfacil.org")
root.resizable(False, False)
ancho_ventana = 500
alto_ventana = 400

#Pantalla
ancho_pantalla = root.winfo_screenwidth()
alto_pantalla = root.winfo_screenheight()

coordenadas_x = int((ancho_pantalla/2) - (ancho_ventana/2))
coordenadas_y = int((alto_pantalla/2) - (alto_ventana/2))- 37

root.geometry("{}x{}+{}+{}".format(ancho_ventana, alto_ventana, coordenadas_x, coordenadas_y))

#Ver valores de dimensiones de la pantalla
Label(text=f"Ancho pantalla: {ancho_pantalla} píxeles.").pack()
Label(text=f"Alto pantalla: {alto_pantalla} píxeles.").pack()

#Bucle de la ventana principal
root.mainloop()

El método geometry de Tkinter

Por último, para que te quede claro como funciona el geometry que hay en la línea 18, hay que pasarle cuatro argumentos:

  1. Ancho de la ventana.
  2. Alto de la ventana.
  3. Posición X (ancho) de la pantalla.
  4. Posición Y (alto) de la pantalla.

Ya expliqué anteriormente como iba, pero un pequeño repaso no viene mal, ya que hace ya unos cuantos capítulos de esto.

Por cierto, si no entiendes muy bien como funciona el "format()" te recomiendo que veas el capítulo 13 donde lo explico.

Veamos alguna cosa más con las ventanas.

Pantalla de Tkinter maximizada por defecto

Si quieres que la ventana esté maximizada por defecto al abrir el programa, solo tienes que utilizar el método state() con el valor "zoomed".

root.state("zoomed")

Además, si quieres ver qué más opciones admite state(), pon un valor erróneo como argumento:

root.state("minimized")

La consola te revelará las opciones disponibles:

Error en la consola

_tkinter.TclError: bad argument "viewable": must be normal, iconic, withdrawn, or zoomed

Opción "normal" del método state() de Tkinter

La opción "normal" deja la pantalla con el estado por defecto (aparición aleatoria en pantalla).

root.state("normal")

Opción "iconic" del método state() de Tkinter

Esta opción, deja la ventana "retirada" al ejecutar el programa. No la vemos.

root.state("withdrawn")

Para hacer que aparezca, tendrás que utilizar algo como el método deiconify()

root.state("withdrawn")
root.deiconify()

Recuerda que puedes utilizar todas estas cosas en funciones, condicionales, etc. De forma que tu programa pueda ir variando de múltiples formas. Solo tienes que echarle imaginación.

Vayamos al último apartado de este día 18. Un tema de seguridad informática.


Problemas de seguridad con eval()

El método eval(), al ejecutar código de las cadenas de texto, se puede utilizar malintencionadamente. No voy a escribir aquí un artículo entero sobre esto, así que, si quieres saber más sobre eval(), puedes leer este buen artículo (Eval really is dangerous) que explica con ejemplos los peligros de utilizar este método en tus programas. Es bastante técnico, de forma que si no tienes muchos conocimientos en ciberseguridad y Python, puede que no entiendas del todo o casi nada. No te preocupes si es tu caso, ya que no es requisito saber esto para continuar el curso. Por tu seguridad, no ejecutes nada de código que escriba a continuación o que haya en ese artículo que te acabo de poner. A no ser que lo hagas bajo tu propia responsabilidad y que tengas la seguridad de lo que estás haciendo.

Por otro lado, te recomendaría que no utilizaras eval() hasta que no tengas claro como funciona y como pueden atacar con ello. De momento no te tiene que preocupar, ya que solo estás haciendo pruebas, pero el día que crees y distribuyas programas, deberás tenerlo en cuenta si no quieres tener agujeros de seguridad.

Si haces esto, te dará un TypeError (error de tipo de dato). El input() devuelve un string:

numero = input('Introduzca un número: ')

print(type(numero))
cuadrado = numero * numero

Error en la consola

Introduzca un número: 10
<class 'str'>
TypeError: can't multiply sequence by non-int of type 'str'

El error dice, básicamente, que no se puede realizar la multiplicación de estos dos strings (numero * numero).

Aquí tienes un bloque try, except (lo explicaré otro día, aún no hemos dado este tema) que, básicamente, ejecuta el código del bloque try si no hay error. En cambio, si se produce algún error, se ejecuta el bloque Except. Es como un if else, especial para tratar errores.

try: 
    numero = eval(input('Introduce un número: '))
    cuadrado = numero * numero
    print(cuadrado)
except:
    print("Se produjo un error.")

El método eval(), a parte de ejecutar una cadena de texto como código, evalúa el valor introducido y lo transforma al tipo correcto.

Resultado en la consola

Introduce un número: 10
100

Si le pongo un float también funciona:

Resultado en la consola

Introduce un número: 10.5
110.25

Si le pongo un string, salta el error y se ejecuta el bloque except:

Resultado en la consola

Introduce un número: Hello, PCMaster!
Se produjo un error.

Hasta aquí todo perfecto, podemos controlar errores muy bien y dar salida alternativa al programa en caso de que ocurran, para evitar lo que pueda hacer mal el usuario. A este tipo de programación, se le denomina programación defensiva, seguramente haga un capítulo dedicado a eso.

Parece que lo tenemos todo bajo control, pero alguien con los conocimientos suficientes, podría encontrar un agujero de seguridad aquí.

Eliminar carpetas con archivos con Python

Antes de seguir con lo de eval(), es necesario que conozcas esto.

POR TU SEGURIDAD, NO INTENTES EJECUTAR NINGUNO DE LOS CÓDIGOS QUE VIENEN A CONTINUACIÓN.

Importar el módulo os de Python

Voy a importar el módulo "os" de Python.

import os

Ahora, voy a crear una carpeta en el explorador de Windows en la siguiente ruta y la voy a intentar eliminar con código Python.

D:\eliminar

Esta carpeta está vacía. No tiene ni sub carpetas.

seguridad informática con Python
import os

os.rmdir("D:\eliminar")

Esto ha hecho que se borre la carpeta por completo.

Ahora, la creo de nuevo, y le creo dentro una carpeta más y un archivo cualquiera. Que no esté vacía.

caerpetas y archivos
import os

os.rmdir("D:\eliminar")

Al intentar eliminar la carpeta principal, me da error:

Error en la consola

OSError: [WinError 145] El directorio no está vacío: 'D:\eliminar'

No se pueden eliminar carpetas que no estén vacías. Para ello, tenemos otro módulo de Python que nos puede ser útil. Sin embargo, se puede utilizar malintencionadamente.

Importar el módulo shutil de Python

Puesto que shutil es un módulo integrado en Python, no hay que instalar nada.

from shutil import *

Eliminar directorios con carpetas y archivos desde Python

ESTE CÓDIGO PUEDE HACER QUE PIERDAS TUS ARCHIVOS. LO REPITO DE NUEVO. NO EJECUTES NADA.

from shutil import *

rmtree("D:\eliminar")

Esto ha hecho que se elimine todo. Aunque hubiera 1000 archivos, se borran igualmente.

Ahora que ya sabes esto, volvamos al método evil()... digo eval().

Inyectar código en Python

En este supuesto programa, se esperaría un número. Sin embargo, como a eval() le puedo pasar cualquier código como string, le inyecto la línea de código malicioso y el ataque se hace efectivo.

from shutil import *

try: 
    numero = eval(input('Introduce un número: '))
    cuadrado = numero * numero
    print(cuadrado)
except:
    print("Se produjo un error.")

En el programa, en lugar de introducir el número que espera, le paso el rmtree() con la ruta.

Me dice que se produjo un error. Ha saltado el bloque except, ya que no se puede operar con ese string de código. Sin embargo, la carpeta, subcarpetas y archivos que pudiera haber en la ruta, han sido eliminados.

Error en la consola

Introduce un número: rmtree('D:\eliminar')
Se produjo un error.

Con este pequeño programa tan inocente, se ha creado un buen agujero de seguridad.

¿Qué pasaría si le vendes un programa a una empresa con eval() y alguien inyecta código?

Aquí solo te he dado un ejemplo, pero las mentes más retorcidas, encontrarán cualquier forma de atacar con comandos que quizás nadie esperaría.

Entonces, me dirás, no importes todo (*) del módulo "shutil" en tu programa. Error. Se puede hacer esto:

import shutil

try: 
    numero = eval(input('Introduce un número: '))
    cuadrado = numero * numero
    print(cuadrado)
except:
    print("Se produjo un error.")

Resultado en la consola

Introduce un número: shutil.rmtree('D:\eliminar')
Se produjo un error.

También, puede que pienses que si no utilizas el módulo "shutil" en tus programas, estarás a salvo. Error de nuevo.

Importar módulos de Python en tiempo de ejecución

Algo interesante que podemos hacer en Python, es realizar importaciones en tiempo de ejecución, si, así es. Importar módulos que no están en el código, mientras está en ejecución.

Quito el "import shutil":

try: 
    numero = eval(input('Introduce un número: '))
    cuadrado = numero * numero
    print(cuadrado)
except:
    print("Se produjo un error.")

Ejecuto. E inyecto este código:

Resultado en la consola

Introduce un número: __import__('shutil').rmtree('D:\eliminar')
Se produjo un error.

Y ya está. Todo se ha perdido...

Los peligros de la red de internet

Por lo tanto, ten en cuenta, que aunque estés realizando prácticas, ves con cuidado si copias código de la red de internet, ya que si utilizan "eval", te pueden "trolear", ya que podrían dejar un código preparado para que al ejecutarlo, destruya tus archivos o cualquier otra cosa que se le ocurra crear.

También ten en cuenta que hay algún que otro método peligroso que no voy a tratar aquí para no alargar más el capítulo.

Más que suficiente para este décimo octavo día del curso. Pasemos ya a la parte de los ejercicios de Python y Tkinter relacionados con el temario.

3 comentarios en «0»

  1. Hola. Cuando ejecuto msb.showinfo aparece una ventana con el mensaje, y otra ventana diferente de la principal que uso después. Cómo podría evitarlo? Gracias! Mu bueno el curso!!

Deja una respuesta

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

android studio logo Entrada anterior Tu primera App con Android Studio
curso de Python Entrada siguiente Ejercicios de Python y Tkinter