Mostrando entradas con la etiqueta Tkinter. Mostrar todas las entradas
Mostrando entradas con la etiqueta Tkinter. Mostrar todas las entradas

lunes, 4 de julio de 2022

Curso Guizero 4. Cambiando el Tamaño de los widgets y su Alineación.

aplicación procesador de texto



Curso Guizero 4. Cambiando el tamaño de los Widgets y su Alineación.

Un elemento importante a considerar a la hora de planificar una GUI es el tamaño de los elementos que la componen. En este capitulo, vamos a ver diferentes métodos para cambiar tanto la altura como el ancho de los widgets.

LOS GESTORES DE VENTANAS.

Antes de ponernos a cambiar el tamaño y diseño de los widgets de nuestra aplicación, es importante que tengamos en cuenta el papel que desempeña el administrador de ventanas, que es una parte del sistema operativo con el que estemos trabajando. 

El administrador de ventanas es el que finalmente establece como se van a ver nuestros widgets, donde se colocan en la ventana y que propiedades se pueden cambiar. 

Por ejemplo, el mismo código ejecutado en diferentes sistemas operativos da lugar a resultados diferentes:

el mismo widget en diferentes sistemas operativos.

De izquierda a derecha: macOS, windows10 y Rapsbian.

Aunque las diferencias entre ambos son pequeñas, no debes pensar que tu código dará lugar a los mismos resultados en diferentes sistemas operativos.


Parámetros de tamaño.

Los parámetros height (alto) y width (ancho) nos permiten cambiar fácilmente las dimensiones de los widgets.

Para especificar el alto y ancho de un widget podemos usar estos parámetros al crear el widget. Si no lo hacemos este se creará con un tamaño predeterminado. Pero también podemos especificarlos después de haberlos creado para cambiar su tamaño.

Si ejecutas el siguiente código puedes ver un ejemplo de un widget PushButton configurado con el tamaño predeterminado y otro con 10 caracteres de alto y 20 de ancho:


from guizero import App, PushButton
app = App()
button = PushButton(app, text="Tamaño por Defecto")
button_sized = PushButton(app, text="Nuevo tamaño", width=20, height=10)
app.display()

diferentes tamaños para los widgets.


Para algunos widgets la altura y anchura se miden en caracteres, es decir la cantidad de letras y símbolos que caben en el espacio. Para otros la altura y anchura se miden en pixeles. Es importante saber que widgets utilizan que medidas, ya que los caracteres no tienen el mismo tamaño que los pixeles.

En Guizero, los widgets Box, ListBox, Picture, Slider y Waffle se miden en píxeles, al igual que un PushButton que contiene una imagen.


Filling

Una forma alternativa para especificar el alto y el ancho de un widget es configurar sus parámetros height and width con "fill" (rellenar en castellano). Esto le indica al widget que utilice todo el espacio disponible. 

Un ejemplo de uso del valor "fill" con los parámetros height and width puede ser el siguiente:

from guizero import App, PushButton
app = App()
button = PushButton(app, height=10, width=20)
button_sized = PushButton(app, text="Usando todo el espacio", width="fill", height="fill")
app.display()

(No olvides poner las comillas alrededor de fill.)

uso del valor "fill"


No todos los widgets se pueden redimensionar de esta manera. Para ver cual la soportan y cual no, así como otras notas sobre el tamaño de los diferentes widgets consulta la documentación.

CONSEJO

Si tienes problemas para decidir cual debe ser el ancho y/o alto de un widget cuando estés diseñando la aplicación, empieza dibujando todo en un papel (si, si con un lápiz y un papel) e intenta que todos los elementos tengan el tamaño y las proporciones que te gustarían. Si lo haces de esta forma, posteriormente puedes medirlos y pasar esas medidas al programa. 

Ten en cuenta que un carácter mide aproximadamente 0.212 cm y que 600 caracteres tienen una longitud aproximada de 127 cms. Si lo quieres pasar a pixeles tienes que saber que 5.08 cms son unos 600 pixeles. Aunque si lo quieres usar de forma más sencilla, es posible que necesites utilizar un convertidor de unidades como este por ejemplo

Si no te decides por el lápiz y el papel y lo estás diseñando en una aplicación, por norma general el propio programa te facilita alguna opción para obtener el ancho y alto del elemento, pero ahí depende del programa que utilices.

HACIENDO QUE TODO SE ALINEE.

Cuando los widgets se colocan en un contenedor, como en una APP o un Box, en Guizero se pueden colocar de dos formas: automática o usando cuadrículas. De forma automática se utiliza el gestor de geometría PACK y si usamos cuadrículas el gestor de geometría GRID. Aunque por defecto no se contempla también podemos utilizar de forma mixta el gestor de geometría PLACE aunque no lo veremos en este capítulo lo puedes consultar en el correspondiente enlace. Esta elección de diseño es un parámetro inicial para cada contenedor y, por lo tanto, debe especificarse cuando se cree el mismo.

Empezaremos explicando como se colocan los widgets por defecto y en un capítulo posterior veremos como usar cuadrículas (GRID).

Diseño Automático.

Si no especificamos el otro gestor de geometría, por defecto los widgets se colocan de forma automática, en el orden en que se crean, comenzando en la parte superior del contenedor y colocándose en el centro del mismo. (Usan el gestor de geometría Pack)

Los widgets también se pueden alinear a la izquierda, a la derecha o en la parte inferior del contenedor usando el parámetro align. Intenta imaginar como se verá la siguiente aplicación antes de ejecutar el código:

from guizero import App, Text

# El diseño automatico está ya predeterminado.
app = App(title="align")

# Crea una serie de widgets cada uno con una alineación diferente.
top_text = Text(app, text="En la parte de arriba", align="top")
bottom_text = Text(app, text="En la parte de abajo", align="bottom")
left_text = Text(app, text="A la izquierda. ", align="left")
left_text_1 = Text(app, text="A la izquierda de nuevo", align="left")
right_text = Text(app, text="A la derecha", align="right")

app.display()


Cuando se colocan varios widgets en el mismo contenedor y con la misma alineación, aparecen por orden, por lo que left_text aparece primero que left_text_1 en este ejemplo.


colocación automática de los widgets en la ventana


Alineado de Widgtes dentro de un Box.

El alinear widgets dentro de una caja o Box nos va a permitir mejorar mucho el diseño. Un buen ejemplo es la forma de colocar algo en una esquina de la aplicación, en vez de en la parte superior, inferior, derecha o izquierda como hemos hecho hasta ahora.

Mira el código siguiente e intenta adivinar ¿Dónde aparecerá el widget PushButton?


from guizero import App, Box, PushButton
app = App()
box = Box(app, width="fill")
button = PushButton(box, align="left")
app.display()


Se puede usar una caja o Box para mantener los diferentes elementos alineados entre si. Vamos a profundizar un poco más en el concepto intentando usar lo que hemos aprendido para recrear la bandera de un país de Oceanía llamado Tonga. Utilizamos cajas para crear y colocar la cruz y rellenar el resto de la bandera.




Prueba a eliminar los elementos Box de este script (deberás cambiar los parámetros maestros de los otros widgets) y mira lo que se crea cuando se ejecuta, en comparación con la versión con las cajas.

from guizero import App, Text, Box, PushButton, TextBox

app = App(width=300, height=200)
app.bg= "white"

# Divide la bandera en dos secciones, una superior que contendrá la cruz y un bloque rojo inferior.

top = Box(app, align="top", width="fill")
bottom = Box(app, width="fill", height="fill")

# Crea un cuadro para contener la cruz, alineado a la izquierda en la sección superior.
cross = Box(top, align="left")

# Se necesitan dos cajas para construir la cruz.
flag = Box(cross)
# Establece el fondo a blanco.
flag.bg = "white"
# Haz las partes superiores de la cruz con cuatro bloques rojos del mismo tamaño.
first = Text(flag, bg="red", align="top", height=1, width=3)
second = Text(flag, bg="red", align="left", height=1, width=3)
third = Text(flag, bg="red", align="left", height=1, width=3)
fourth = Text(flag, bg="red", align="left", height=1, width=3)
# Crea el segundo cuadro directamente debajo del primero, de modo que la pieza final
# de la cruz se añadirá a la parte inferior de la misma. Intenta ejecutar el
# programa sin esta caja para ver dónde se colocaría automáticamente.
flag_cont = Box(cross)
# Establece el fondo a blanco
flag_cont.bg = "white"
# Completa la bandera
fifth = Text(flag_cont, height=1, width=3, align="bottom", bg="red")

# Rellena el resto de la bandera de color rojo.
fill = Text(top, align="left", height="fill", width="fill", bg="red")
fill_cont = Text(bottom, height="fill", width="fill", bg="red")

app.display()

Puedes encontrar el código superior en el siguiente enlace.


Construyendo un editor de texto.


Con lo que hemos visto hasta ahora vamos a intentar un desafío. Crear un procesador de textos sencillo.  Necesitarás seguramente consultar la documentación de Guizero.

Empecemos.

Primero debemos comenzar planificando que es lo que la aplicación debe hacer y que widgets se adaptan mejor a la tarea. Echa un vistazo a un editor de textos que tengas en tu ordenador para inspirarte (El block de notas puede ser un buen ejemplo, haz clic en el menú archivo para ver algunas opciones que te brinda el editor de texto)

Nuestro editor de texto va a ser muy básico pero tendrá que tener los siguientes elementos:

- Un cuadro de texto para escribir.

- Un cuadro de texto para especificar el nombre del archivo.

- Un PushButton para guardar el archivo.

- Un PushButton para leer los archivos guardados.

Tomate un momento para diseñar el editor de texto incorporando los cuatro elementos. ¿Pondrás el botón Guardar en la parte superior o inferior del GUI? ¿Lo colocarás junto al botón de abrir? ¿Qué tamaño debe tener el cuadro donde se escribirá el texto?

Iremos añadiendo cada uno de los componentes. Intenta modificar el código sobre la marcha para que los widgets se adapten a tu diseño. Recuerda que Guizero coloca los widgets en el orden en el que se crean comenzando desde la parte superior. Eso significa que a lo mejor debes reordenar el código para adaptarlo a tu diseño.

Comienza dibujando en un papel como quieres que sea tu aplicación y luego intenta crear el diseño de la misma. No te preocupes por el código ya que lo desarrollaremos a la vez que iremos explicándolo.


La forma más fácil de manejar los controles y mantenerlos juntos es utilizar una caja (Box).  Yo simplemente la llamaré caja porque vamos a meter ahí los controles de la aplicación pero realmente puedes llamarlo como quieras mientras te quede lo que representa.

from guizero import App, TextBox, PushButton, Box

app = App(title="Procesador de Textos")

# Crea una caja para meter los controles.
# Queremos que ocupe todo el ancho de la aplicación.
caja = Box(app, align="top", width="fill")

# crea un TextBox para el nombre del archivo
nombreArchivo = TextBox(caja, text="archivo.txt", width=38, align="left")

# Crea un botón Grabar, que se usará con la función para grabar posteriormente.
btn_save = PushButton(caja, text="Grabar",  align="right")

# Crea un botón Leer, que se usará con la función leer posteriormente.
btn_read = PushButton(caja, text="Leer",  align="right")

# Crea un TextBox, el cual no está en la caja y ocupa el resto de la GUI
escritorio = TextBox(app, multiline=True, height="fill", width="fill")

app.display()

Primer diseño del GUI

Por el momento los botones de Grabar y Leer no hacen nada. Para indicarles que guarden o lean un archivo hay que añadir dos funciones:

# función para leer archivos:
def push_leer():
    with open(nombreArchivo.value, "r") as f:
        escritorio.value = f.read()

# función para escribir archivos:
def push_grabar():
    with open(nombreArchivo.value, "w") as f:
        f.write(escritorio.value)

Ambas funciones usan la función "Open" de Python. Si no estás familiarizado con ella puedes encontrar más información aquí.

Antes de seguir leyendo, intenta averiguar cuales son los parámetros nombreArchivo.value y escritorio.value e imagina que hará la función cuando se la llame.

La función push_leer() abre un archivo donde el nombre del mismo coincide con la propiedad del valor del TextBox nombreArchivo. Luego establece que la propiedad del cuadro de texto "escritorio" sea el contenido del archivo, es decir carga el contenido de texto del archivo en el cuadro de texto del escritorio.

La función push_grabar() realiza lo contrario. ¿Puedes deducir cuales son los valores de nombreArchivo.value y escritorio.value en este caso?

Finalmente necesitamos programar a los botones de Grabar y Leer para que ejecuten las funciones respectivas cuando se activen. Esto se hace añadiendo el parámetro "command" a cada uno de los widgets.

btn_save = PushButton(caja, text="Grabar", align="right", command=push_grabar)

¿Puedes escribir el código equivalente para el widget btn_read?

Cuando el procesador esté más avanzado les daremos a los usuarios más opciones de estilo para el texto. Haremos esto en el próximo capítulo así que no olvides guardar el archivo.


Siguiente Capítulo.- 5. Controlando la GUI.


martes, 24 de mayo de 2022

¿Cómo incorporar un Gráfico de Matplotlib en Tkinter y Guizero?


ventana de tkinter con gráfico insertado

¿Cómo incorporar un Gráfico de Matplotlib en Tkinter y Guizero?

A veces necesitamos incorporar gráficos de Matplotlib dentro de nuestras aplicaciones. Se puede enfocar este tema desde dos vertientes. (Lo primero no obstante es instalar esta librería, claro está.)

La primera es hacer un gráfico con la librería  Matplotlib, por ejemplo, guardarlo en un archivo en el mismo directorio y como hemos visto en un tema anterior cargarlo como cualquier otro gráfico en un widget.

Por ejemplo podemos construir un gráfico como el que se muestra a continuación utilizando la librería Matplotlib y guardándolo con la instrucción savefig() de esta librería. Puedes encontrar más información sobre como elaborar un gráfico en el siguiente enlace.

gráfico a insertar en una aplicación.


Y luego, una vez que tenemos el archivo lo podemos usar en nuestra GUI. En Guizero esto se realizaría usando PICTURE(). Matizar que Guizero solo permite usar imágenes en formato GIF o PNG.

El código puede ser parecido a este:

from guizero import *
'''Una forma de insertar un grafico es construyendolo con matplotlib,
guardandolo con la instrucion savefig()
y luego importarlo en guizero con Picture()'''

raiz=App()
Picture(raiz, image="grafico1.png", width=500, height=500)
raiz.display()

Lo que nos crea esta ventana con el gráfico dentro.

Gráfico insertado en una ventana


Código de esta aplicación en Github.


La segunda opción es incrustar ese gráfico en el widget directamente, aunque también es algo más compleja.

Podemos usar por ejemplo  este código que ya utilizamos en el capítulo de matplotlib para crear un simple gráfico de barras y trataremos de insertarlo en un widget.

import matplotlib.pyplot as plt
mascotas = [12, 9, 3, 1]
nombres = ["perros", "gatos", "peces", "tortugas"]
plt.bar(nombres, mascotas)
plt.plot()
plt.show()

Daría lugar al siguiente gráfico:

gráfico generado con matplotlib


Comenzamos importando las librerías que vamos a necesitar para crear una ventana y luego dibujar dentro el gráfico anterior.

# librerías de gráficos normales
import tkinter as tk
# librerías para insertar el gráfico
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
# Librería para crear el gráfico
import matplotlib.pyplot as plt

Luego programamos la ventana de la aplicación y un contenedor tipo "frame" que será el que aloje el gráfico. 

# Creamos la ventana y el contenedor "frame" que contendrá el gráfico.
ventana = tk.Tk()
ventana.geometry('300x250')
ventana.title("Gráfica insertada en tkinter")
frame = tk.Frame(ventana, bg='yellow')
frame.grid(column=0, row=0, sticky='nsew')

Usando las listas con los datos que tenemos que son el nombre de las mascotas y su número o frecuencia procedemos a crear un gráfico.

mascotas = [12, 9, 3, 1]
nombres = ["perros", "gatos", "peces", "tortugas"]

fig, axs = plt.subplots(dpi=80, figsize=(4, 3))
fig.suptitle("Grafico Principal")

axs.bar(nombres, mascotas)

En este ejemplo creamos una figura que solamente tendrá un gráfico principal, es por ello que usamos la función fig, axs = plt.subplots().  Con dpi y figsize establecemos el tamaño del gráfico. También le añadimos un título y finalmente al ser solamente un gráfico usamos axs y el tipo de gráfico a crear, que en este caso es un gráfico de barras. 

Si queremos dibujar varios gráficos, habría que definirlos dentro de plt.subplots() y accederíamos a ellos con axs[0], ax[1] etc.

La novedad es que vamos a usar FigureCanvasTkAgg para dibujar el gráfico (fig) dentro del frame anteriormente definido, usando un canvas de tkinter:

# Dibujamos el gráfico dentro del frame definido usando
# un Canvas de tkinter.
canvas = FigureCanvasTkAgg(fig, master=frame)
canvas.draw()
canvas.get_tk_widget().grid(column=0, row=0)

ventana.mainloop()

Si todo ha ido bien el resultado será algo parecido a esto:

ventana de tkinter con gráfico insertado


Puedes encontrar el código en el siguiente enlace.



domingo, 27 de marzo de 2022

Tipos de Ventanas en Tkinter y Guizero.

 

ventana de un programa











Tipos de Ventanas en Tkinter y Guizero.

Ahora que ya hemos visto los  diferentes tipos de widgets y de gestores de geometría existentes vamos a centrarnos en las ventanas.

Existen diferentes ventanas. Por una parte tenemos las ventanas de aplicación y por otra las de diálogos.

Ventanas de aplicación.

Suelen ser las que inician y finalizan las aplicaciones gráficas.

Ventanas de diálogos.

Surgen de las anteriores.

El conjunto de ambas, junto con los widgets que contienen, configuran la interfaz del usuario.

En el siguiente ejemplo desarrollaremos una ventana de aplicación con un botón abrir, que cada vez que se presione abra una ventana hija de dialogo diferente y en una posición distinta de la pantalla. 

Estas ventanas que se van generando se sitúan en un plano superior con respecto a las generadas anteriormente. Además podemos interactuar indistintamente con cada una de las ventanas creadas. Podemos cambiar sus posiciones, cerrarlas etc.

Para el ejemplo, usaremos el gestor de geometría pack, aunque podríamos haber utilizado cualquiera de los anteriormente vistos. 

Tkinter.

ventana de aplicación y de dialogo


from tkinter import *
from tkinter import ttk

class Aplicacion():
    
    # Declara una variable de clase para contar ventanas
    
    ventana = 0
    
    # Declara una variable de clase para usar en el
    # calculo de la posición de la ventana
    
    posx_y = 0
    
    # Declara una ventana de aplicación
    
    def __init__(self):
        
        self.raiz=Tk()
        
        # Define la dimensión de la ventana 300x200
        # que se situara en la coordenada x=500, y=50
        
        self.raiz.geometry('300x200+500+50')
        
        self.raiz.resizable(0,0)
        self.raiz.title("Ventana de aplicación")
        
        # Define botón 'Abrir' que se utilizará para
        # abrir las ventanas de diálogo. El botón
        # está vinculado con el método 'self.abrir'
        
        boton = ttk.Button(self.raiz, text="Abrir", command=self.abrir)
        boton.pack(side=BOTTOM, padx=20, pady=20)
        self.raiz.mainloop()
        
    def abrir(self):
        ''' Construye una ventana de dialogo '''
        
        # Define una nueva ventana de diálogo '''
        
        # Las ventanas de dialogo hijas se definen con TopLevel()
        self.dialogo = Toplevel()
        
        # incrementa en 1 el contador de ventanas
        # en la variable de clase a la que se puede acceder
        # desde cualquier método de la clase con la notación
        # del punto.
        Aplicacion.ventana+=1
        
        # Recalcula la posición de la ventana


        Aplicacion.posx_y += 50
        # el \ al final de la línea era para hacer un salto y que siga leyendo el código
        tamypos ='200x100+'+str(Aplicacion.posx_y)+ \
                  '+'+ str(Aplicacion.posx_y)
        print(tamypos)
        self.dialogo.geometry(tamypos)
        self.dialogo.resizable(0,0)
        
        # obtiene el identificador de la nueva ventana
        
        ident = self.dialogo.winfo_id()
        
        # Construye el mensaje de la barra de titulo
        
        titulo = str(Aplicacion.ventana)+": "+str(ident)
        print(titulo)
        self.dialogo.title(titulo)
        
        # Define el botón 'Cerrar' que cuando sea presionado
        # cerrara (destruirá) la ventana 'self.dialogo' llamando
        # al método 'self.dialogo.destroy'
        
        boton = ttk.Button(self.dialogo, text='Cerrar', command=self.dialogo.destroy)
        boton.pack(side=BOTTOM, padx=20,pady=20)
        
        # Cuando la ejecución del programa llega a este
        # punto se utiliza el método wait.window() para
        # esperar que la ventana 'self.dialogo' sea
        # destruida.
        
        # Mientras tanto se atiende a los eventos locales
        # que se produzcan, por lo que otras partes de la
        # aplicación seguirán funcionando con normalidad.
        # Si hay código después de esta línea se ejecutará
        # cuando la ventana 'self.dialogo' sea cerrada.
        
        self.raiz.wait_window(self.dialogo)
        
def main():
    mi_app = Aplicacion()
    return 0

if __name__=='__main__':
    main()   

Puedes obtener el código de la aplicación en el siguiente enlace.


Guizero

ventana principal y de dialogo en Guizero

'''ejemplo de ventanas no modales porque mientras existen es posible interactuar libremente con ellas,
con ellas, sin ningún límite, excepto que si cerramos la ventana principal se cerrarán todas
las ventanas hijas abiertas.'''

from guizero import *

def dialogo():
    ''' Crea una nueva ventana de dialogo. Cada vez que se ejecuta se dibuja una
nueva ventana, con una nueva posición y un nuevo titulo de ventana'''
    global ventana, posx_y
    # Incrementa en 1 el numero de ventanas abiertas
    ventana +=1
    # Incrementa en 50 los pixeles de la variable q controla la posición
    posx_y +=50
    # variable de texto que contendrá el tamaño y posición de la ventana en geometry
    #tama_y_posc = "200x100"+"+"+str(posx_y)+"+"+str(posx_y)
    tama_y_posc = f"200x100+{posx_y}+{posx_y}"
    print(tama_y_posc)
        
    # En guizero se usa Window() para definir una ventana de dialogo.
    window = Window(raiz)
    # obtiene el identificador de la nueva ventana
    ident = window.tk.winfo_id()
    titulo = f"{ventana}:{ident}"
    print(ident)
    window.title=titulo
    window.hide()
    window.tk.geometry(tama_y_posc)
    window.tk.resizable(0,0)
    PushButton(window, text="Cancelar", align="bottom", command=window.destroy)
    window.show()

# Variable genérica para contar las ventanas.
ventana = 0
# Variable genérica para el calculo de la posición de la ventana
posx_y = 0

#---------------------------- Ventana Principal------------------------------------    
raiz = App(title="Ventana de Aplicación")
# La geometría del objeto raiz.tk es "(tamaño_x)x(tamaño_y)+posc_x+posc_y"
raiz.tk.geometry("300x200+500+50")
raiz.tk.resizable(0,0)

# En guizero el parámetro padding afecta a la distancia del texto del botón
# respecto al borde del mismo, hace mas grande o mas pequeño al boton
boton = PushButton(raiz, text="Abrir", align="bottom", padx=20, pady=5, command=dialogo)

# Sin embargo si lo pones en el método tk afecta a la distancia al siguiente widget.
# En este caso a la distancia a la parte inferior de la ventana
boton.tk.pack(padx=20, pady=20)
#-----------------------------------------------------------------------------------


raiz.display()

Puedes encontrar el código del ejemplo en el siguiente enlace.


Ventanas Modales y No modales.

A parte de que las ventanas pueden ser principales o de dialogo, también como ya hemos visto en el ejemplo anterior aunque no lo hayamos dicho, pueden ser modales y no modales. 

Las ventanas hijas creadas en los ejemplos anteriores son "no modales" porque mientras que existan es posible interactuar independientemente con cada una de ellas, sin limite alguno, excepto que si cerramos la ventana principal se cerrarán todas ellas.

El ejemplo típico es el de las ventanas de cualquier aplicación ofimática que te permiten trabajar con varios documentos mientras están abiertas.

En las ventanas con modales esto no pasa, ya que la ventana activa, bloquea a las demás hasta que se cierre el diálogo.

El ejemplo típico es el de algunas ventanas de dialogo en la que se establecen preferencias de las aplicaciones que tienen que ser cerradas antes de poder acceder a otras.


Tkinter. Ventana Modal.

ventana modal en tkinter


Aquí la peculiaridad es que se emplea el método grab_set() para crear la ventana modal y transiet() para convertir la ventana de dialogo en una ventana transitoria, haciendo que esta se oculte cuando la ventana de la aplicación se minimice.

'''Ejemplo de ventana modal. El siguiente ejemplo sólo es posible
mantener abierta sólo una ventana hija, aunque si la cerramos podremos abrir otra.

El método grab_set() se utiliza para crear la ventana modal y el método transiet()
se emplea para convertir la ventana de diálogo en ventana transitoria,
haciendo que se oculte cuando la ventana de aplicación sea minimizada.'''

#!/usr/bin/env python
# -*- coding: utf-8 -*-

from tkinter import *
from tkinter import ttk

class Aplicacion():
    
    #variables de la clase
    ventana = 0
    posx_y = 0
    
    def __init__(self):
        self.raiz=Tk()
        self.raiz.geometry('300x200+500+50')
        self.raiz.resizable(0,0)
        self.raiz.title("Ventana de aplicación")
        boton = ttk.Button(self.raiz, text="Abrir", command=self.abrir)
        boton.pack(side=BOTTOM, padx=20, pady=20)
        self.raiz.mainloop()
        
    def abrir(self):
        '''Construye una ventana de dialogo'''
        
        self.dialogo = Toplevel() #equivale a Window en guizero
        Aplicacion.ventana+=1
        Aplicacion.posx_y+=50
        tamypos = f'200x100+{Aplicacion.posx_y}+{Aplicacion.posx_y}'
        self.dialogo.geometry(tamypos)
        self.dialogo.resizable(0,0)
        ident = self.dialogo.winfo_id()
        titulo = str(Aplicacion.ventana)+": "+str(ident)
        self.dialogo.title(titulo)
        boton=ttk.Button(self.dialogo,text='Cerrar', command=self.dialogo.destroy)
        boton.pack(side=BOTTOM, padx=20, pady=20)
        
        # Convierte la ventana ''self.dialogo' en
        # transitoria con respecto a su ventana maestra
        # 'self.raiz'.
        # Una ventana transitoria siempre se dibuja sobre
        # su maestra y se ocultará cuando la maestra sea
        # minimizada. Si el argumento 'master' es
        # omitido el valor, por defecto, será la ventana
        # madre
        
        self.dialogo.transient(master=self.raiz)
        
        # El método grab_set() asegura que no haya eventos
        # de ratón o teclado que se envien a otra ventana
        # diferente a 'self.dialogo'. Se utilizará para
        # crear una ventana de tipo modal que será
        # necesario cerrar para poder trabajar con otra
        # diferente. Con ello, también se impide que la
        # misma ventana se abra varias veces.
        
        self.dialogo.grab_set()

        # Para esperar a que la ventana self.dialogo sea cerrada.
        self.raiz.wait_window(self.dialogo)
        
def main():
    mi_app = Aplicacion()
    return 0

if __name__=='__main__':
    main()        

El código de este ejemplo se encuentra en el siguiente enlace.

Guizero. Ventana Modal.

ventana modal en guizero



Por defecto en Guizero las ventanas añadidas de dialogo que se creen son no modales, funcionan de forma independiente, así que para transformarlas en modales hay que usar componentes de Tkinter.

from guizero import *
# Las listas de un solo elemento tienen que tener una coma ,
# después del elemento.
contador=[0,]
posx_y=[0,]

def dialogo():
    # En vez de variables globales (enteros, tuplas) vamos a utilizar listas que se modifican
    # por si solas a nivel global de la aplicación.
    contador[0]=contador[0]+1
    posx_y[0]=posx_y[0]+50
    ventana=Window(raiz)
    ident = ventana.tk.winfo_id()
    ventana.title=f"{contador[0]} :{ident}"
    ventana.hide()
    ventana.tk.resizable(0,0)
    dimension = f"200x100+{posx_y[0]}+{posx_y[0]}"
    ventana.tk.geometry(dimension)
    #-----------------------------------------------------------------------
    PushButton(ventana, text="Cancelar", align="bottom", command=ventana.destroy)
        # Convierte la ventana ''self.dialogo' en
        # transitoria con respecto a su ventana maestra
        # 'self.raiz'.
        # Una ventana transitoria siempre se dibuja sobre
        # su maestra y se ocultará cuando la maestra sea
        # minimizada. Si el argumento 'master' es
        # omitido el valor, por defecto, será la ventana
        # madre
    ventana.tk.transient(master=raiz.tk)
        # El método grab_set() asegura que no haya eventos
        # de ratón o teclado que se envíen a otra ventana
        # diferente a 'self.dialogo'. Se utilizará para
        # crear una ventana de tipo modal que será
        # necesario cerrar para poder trabajar con otra
        # diferente. Con ello, también se impide que la
        # misma ventana se abra varias veces.
    ventana.tk.grab_set()
    ventana.show()

raiz=App(title="Ventana de Aplicación")
raiz.tk.geometry("300x200+500+50")
raiz.tk.resizable(0,0)

boton=PushButton(raiz, text="Abrir", padx=20, pady=5, align="bottom", command=dialogo)
boton.tk.pack(pady=20)

raiz.display()

Puedes encontrar en este enlace el código del ejemplo anterior.


viernes, 25 de marzo de 2022

El gestor de Geometría Place.

Imagen de entrada


El gestor de Geometría Place.

Este gestor se basa en colocar los diferentes widgets en base a sus coordenadas en pixeles dentro de la ventana. Es como si la ventana fuese un papel reglado y colocásemos los diferentes elementos poniendo las coordenadas de cada uno en el papel. Aunque colocar los elementos es sencillo, el tema lleva bastante trabajo porque hay que tener en cuenta que cada elemento tiene un alto y ancho determinado en pixeles y tenemos que colocarlos en la ventana especificando una coordenada (x e y) en pixeles.

Como punto de referencia tenemos que sabe que la posición (0, 0) esta en la parte superior izquierda de la ventana. 

También tenemos que saber, que como estamos especificando las posiciones en pixeles dentro de la ventana,  la posición es absoluta, por lo que si posteriormente modificamos el tamaño de la misma los widgets no se adaptarán a esta modificación, y se quedarán en el mismo sitio.

Para trabajar con este gestor nos van a venir bien dos aplicaciones.

La primera es una regla de pantalla: Kruler o Screen Ruler están bien. Kruler se puede instalar directamente desde la tienda de aplicaciones de Ubuntu por ejemplo o también desde el terminal con

$ sudo apt-get install kruler

Pero tiene la desventaja que te si utilizas Gnome como yo en Ubuntu, te instala el gestor de ventanas de KDE que son casi 500gb.

mientras que screenruler solo aparece para instalar desde terminal aunque es bastante, bastante más ligera:

$ sudo apt-get install screenruler

Como su nombre indica se tratan de eso, de una regla de pantalla con la que podemos medir distancias o tamaños para posicionar los widgets que queramos utilizar.

aplicación kruler


La segunda aunque no se necesita estrictamente para diseñar los widgets de las ventanas si que nos puede venir bien para saber cual es un color concreto de un elemento que ya está creado y que nosotros queramos replicar. El programa se llama Gpick y es muy fácil de utilizar simplemente se pasa el puntero del ratón por la zona que nos interesa saber su color y ya está. Además tiene muchas más opciones para la combinación de colores.



Dicho lo anterior vamos a la materia.

Tkinter.

Como en los casos anteriores la aplicación es básicamente igual en cuanto al diseño de los widgets.

from tkinter import *
from tkinter import ttk, font
import getpass

#GESTOR DE GEOMETRIA (PLACE)

class Aplicacion():
    def __init__(self):
        self.raiz=Tk()
        
        # Define la dimensión de la ventana
        
        self.raiz.geometry("430x200")
        
        # Establece que no se puede cambiar el tamaño de la
        # ventana.
        
        self.raiz.resizable(0,0)
        self.raiz.title("Acceso")
        self.fuente = font.Font(weight='bold')
        
        self.etiq1 = ttk.Label(self.raiz, text="Usuario:", font=self.fuente)
        self.etiq2 = ttk.Label(self.raiz, text="Contraseña:", font=self.fuente)
        
        # Declara una variable de cadena que se asigna a
        # la opción 'textvariable' de un widget 'Label' para
        # mostrar mensajes en la ventana. Se asigna el color
        # azul a la opción 'foreground' para el mensaje.
        
        self.mensa = StringVar()
        self.etiq3 = ttk.Label(self.raiz, textvariable=self.mensa, font=self.fuente, foreground='blue')
        
        self.usuario = StringVar()
        self.clave = StringVar()
        self.usuario.set(getpass.getuser())
        self.ctext1 = ttk.Entry(self.raiz, textvariable=self.usuario, width=30)
        self.ctext2 = ttk.Entry(self.raiz, textvariable=self.clave, width=30, show="*")
        self.separ1 = ttk.Separator(self.raiz,orient=HORIZONTAL)
        self.boton1 = ttk.Button(self.raiz,text="Aceptar",padding=(5,5), command=self.aceptar)
        self.boton2 = ttk.Button(self.raiz, text="Cancelar", padding=(5,5), command=quit)       


Lo que varía esta ahora cuando colocamos esos elementos dentro de la ventana. Veamos el código.

              self.etiq1.place(x=30, y=40)
        self.etiq2.place(x=30, y=80)
        
        # Nueva etiqueta para mostrar contraseña correcta o incorrecta.
        self.etiq3.place(x=150, y=120)
        
        self.ctext1.place(x=150, y=42)
        self.ctext2.place(x=150, y=82)
        self.separ1.place(x=5, y=145, bordermode=OUTSIDE, height=10, width=420)
        self.boton1.place(x=170, y=160)
        self.boton2.place(x=290, y=160)
        self.ctext2.focus_set()


Como se ve, los widgets se colocan en la ventana poniendo los valores de x e y en pixeles. Por ejemplo la self.etiq1.place(x=30, y=40) quiere decir que colocamos esa etiqueta a 30 pixeles a la derecha del borde superior izquierdo de la ventana y 40 pixeles por debajo. Y así con el resto de los elementos.

El bordermode=outside que figura en el separador significa la forma en que se cuenta las coordenadas. Por ejemplo, si como es el caso se utiliza "outside" las coordenadas x e y se cuentan desde la esquina superior izquierda del contenedor pero incluyendo el borde de ese elemento, mientras que "inside" cuenta las coordenadas sin incluirlo. 


parámetro bordermode

Ahora vamos a pararnos un poco más a explicar el concepto de evento.  Un evento es algo que sucede en el programa y que desencadena una reacción. Por ejemplo, vamos a  preparar el programa para que cuando hagamos clic con el botón izquierdo del ratón en la caja de la contraseña (evento) se borre un mensaje informativo que crearemos, se borre el contenido de esta caja y se ponga de nuevo el foco en la misma.  

Esto se consigue con el método bind() que asociará el evento (hacer clic con el botón izquierdo del ratón en la caja de entrada de la contraseña) expresado como <Button-1> con el método 'self.borrar_mensa' que borra el mensaje y la contraseña y devuelve el control al mismo. 

Otros ejemplos de acciones que se pueden capturar:
        <double-button-1>, <buttonrelease-1>, <enter>, <leave>,
        <focusin>, <focusout>, <return>, <shift-up>, <key-f10>, 
        <key-space>, <key-print>, <keypress-h> etc.


              self.ctext2.bind('<Button-1>', self.borrar_mensa)
        self.raiz.mainloop()

        # Declara método para validar la contraseña y mostrar
        # un mensaje en la propia ventana, utilizando la etiqueta
        # 'self.mensa'. Cuando la contraseña es correcta se
        # asigna el color azul a la etiqueta 'self.etiq3' y
        # cuando es incorrecta el color rojo. Para ello. se emplea
        # el método 'configure()' que permite cambiar los valores
        # de las opciones de los widgets.
    
    def aceptar(self):
        if self.clave.get() == 'herodoto':
            self.etiq3.configure(foreground='blue')
            self.mensa.set("Acceso permitido")
        else:
            self.etiq3.configure(foreground='red')
            self.mensa.set("Acceso denegado")
            
    # Declara un método para borrar el mensaje anterior y
    # la caja de entrada de la contraseña.
    
    def borrar_mensa(self, evento):
        self.clave.set("")
        self.mensa.set("")
        
def main():
    mi_app = Aplicacion()
    return 0

if __name__=='__main__':
    main()

Puede encontrar el código aquí el código de gestor de geometría Place con Tkinter.

Guizero. 

En Guizero se usa por defecto el método Pack y también podemos usar el gestor Grid. Sin embargo no existe un método de Guizero puro para posicionar las ventanas usando el gestor Place. Por eso, si lo queremos utilizar con widgets propios de Guizero, tendremos que hacer dos cosas.

- SUPER IMPORTANTE: para que luego nos funcione la geometría Place, al diseñar el widget hay que poner el parámetro visible=False en todos los elementos de Guizero que queramos posicionar. OBLIGATORIO.

- Debemos posicionar posteriormente cada widget, al igual que hacemos en Tkinter. Esto es lógico puesto que estamos usando el posicionamiento de Tkinter para colocar elementos de Guizero. Pero solo debemos hacerlo con esta instrucción y de la siguiente forma, el resto igual:

<nombre_asignado_al_widget>.tk.place(x,y)

from guizero import *
''' SUPER IMPORTANTE: PARA QUE FUNCIONE LA GEOMETRIA PLACE HAY QUE PONER visible=False en todos
los WIDGETS DE GUIZERO QUE QUERAMOS POSICIONAR. ES COMO SI DIJERAN OCULTALOS QUE YA LOS PONGO YO'''
''' LOS WIDGET IMPORTADOS DE TKINTER.TTK EL METODO SE PONE COMO EN TKINTER EJ SEPAR1.PLACE() Y NO SEPAR1.TK.PLACE'''
from tkinter import HORIZONTAL, OUTSIDE
from tkinter.ttk import Separator
from getpass import getuser

def aceptar():
    if ctext2.value == 'herodoto':
        print('Acceso Permitido')
        print('Usuario:    ', ctext1.value)
        print('Contraseña: ', ctext2.value)
        ctext2.value="" #Borra el contenido del 2º cuadro de texto
        etiq3.text_color="blue"
        etiq3.value="Acceso Permitido"
        #quit() #Sale del programa
    else:
        print('Acceso denegado')
        etiq3.text_color="red"
        etiq3.value="Acceso Denegado"
                
def haz_esto():
    print('haz esto')
    ctext2.value=""
    etiq3.value=""

usuario = getuser()


raiz = App(title='Acceso', width=430, height=200)
raiz.tk.resizable(0,0) # Para inmovilizar la ventana equivale a resizable(width=False,height=False)

etiq1 = Text(raiz, text="Usuario:", visible=False)
etiq1.tk.place(x=30, y=40)
ctext1 = TextBox(raiz, width=30, visible=False)
ctext1.tk.place(x=150, y=42)
ctext1.value = usuario
etiq2 = Text(raiz, text="Contraseña:", visible=False)
etiq2.tk.place(x=30, y=80)
ctext2 = TextBox(raiz, width=30, visible=False)
ctext2.tk.place(x=150, y=80)
# Permite ocultar el texto tecleado True=* otro carácter se pone y ya está.
ctext2.hide_text="#"
ctext2.when_clicked= haz_esto


etiq3=Text(raiz, text="", visible=False)
etiq3.tk.place(x=150, y=120)

separ1 = Separator(raiz.tk, orient=HORIZONTAL)
separ1.place(x=5, y=145, bordermode=OUTSIDE, height=10, width=420)
boton1 = PushButton(raiz, text="Aceptar", visible=False, padx=10, pady=5, command=aceptar)
boton1.tk.place(x=170, y=160)
boton2 = PushButton(raiz, text="Cancelar", visible=False, padx=10, pady=5, command=quit)
boton2.tk.place(x=290, y=160)

ctext2.focus()
raiz.display()

Puede encontrar aquí el código de gestor de geometría Place en Guizero.

Sin embargo lo que hemos visto anteriormente con este gestor tiene un PROBLEMA. Si permitimos redimensionar la ventana y por tanto cambiamos su tamaño (ancho y alto), los elementos quedarán descolocados ya que están definidos con respecto a la esquina superior izquierda.


ventana redimensionada con método PLACE


Otra forma de utilizar el gestor de ventanas place es, en vez de especificar su posición en pixeles respecto a la esquina superior izquierda, indicar donde se sitúan los elementos en base a un porcentaje sobre el ancho y alto de la ventana que los contiene.

Veámoslo en detalle con la siguiente aplicación.

La aplicación nos permite descargar desde una dirección web que nosotros queramos un texto plano y contar el número de veces que sale cada palabra en el texto. Como ejemplo vamos a usar el Quijote e iremos paso por paso...

progama usando place pero con referencias relativas.


Comenzaremos con el diseño de la aplicación para luego implementar su parte lógica.

Para que la aplicación funcione correctamente necesitamos tener instaladas estas librerías:

- Guizero para el diseño.  (pip3 install guizero)
- request para obtener los datos de la página web donde obtengamos la información.
(pip3 install request)
- html2txt para transformar los datos recibidos como html en un archivo de texto plano.
(pip3 install html2txt)

Aunque aparece en la imagen de arriba, el link donde se encuentra alojado "EL Quijote" dentro de la página del proyecto Gutenberg es:

https://www.gutenberg.org/cache/epub/2000/pg2000.txt

aunque puedes probar con cualquier otro libro o texto siempre que este en formato txt.

Comenzamos con el diseño de la ventana principal.

Tiene que tener el siguiente aspecto:

ventana principal de la aplicación

from guizero import *

raiz = App(title='Contador de palabras en la red')
# Tamaño y posición de la ventana.
raiz.tk.geometry("700x400+350+150")
# Ventana no modificable.
raiz.tk.resizable(0, 0)

raiz.display()


Comenzamos importando el módulo Guizero con todo lo que contiene. Después creamos la ventana principal a la que llamamos app y le ponemos el título usando el parámetro "title". Una vez hecho esto especificamos  el tamaño y posición a través de un objeto interno de Tkinter.  Esto como ya vimos se realiza añadiendo el sufijo ".tk" al objeto creado en Guizero lo que nos hace posible usar las propiedades y métodos de Tkinter. Ya que no todo lo que se puede hacer con Tkinter esta implementado en Guizero, pues este esta pensado para hacer aplicaciones de forma más fácil y rápida. 

Con raiz.tk.geometry() definimos la geometría de la ventana, la cual tendrá unas dimensiones de 700 pixeles de ancho (eje X) por 400 pixeles de alto (eje Y). Estará separada de la parte izquierda de la pantalla en 350 pixeles (+x) y de la parte superior en 150 px (+Y)

root.geometry

Como no quiero que el usuario modifique el tamaño de la ventana uso raiz.tk.resizable(0,0) para no permitir cambiar ni el ancho, ni el largo. Vemos que los dos valores son iguales a 0. Si en lugar de 0 pusiésemos un 1 permitiríamos modificar el parámetro en cuestión, ya sea el ancho, el largo o ambos.

Para crear el cuadro de texto donde el usuario introducirá la url desde donde descargar el texto usamos:

raiz.tk.resizable(

# Primer cuadro de texto (titulo + cuadro)
t1 = Text(raiz, text = "Dirección:", visible=False)
t1.tk.place(relx=.03, rely=0.05)
url = TextBox(raiz, width = 50, text = '', visible=False)
url.tk.place(relx=.15, rely=0.05)

raiz.display()

A t1 la asignamos un texto donde irá el título del cuadro "Dirección:". Acordaos de poner visible=False porque luego usaremos una propiedad de tkinter y sino, no funciona. La novedad es que usaremos relx y rely para poner la posición relativa del elemento con respecto al alto y ancho de la ventana.

Con t1.tk.place(relx=.03, rely=.05) estamos diciendo al interprete que coloque este elemento a un 3% del tamaño de la ventana desde la izquierda y a un 5% desde arriba. Es decir si la ventana de ancho tiene 700 pixeles lo estamos colocando a 21 pixeles desde la izquierda (700 * 0.03) y a 20 pixeles desde arriba (400 * 5%). 

Para definir la caja donde pondremos la url usamos un cuadro de texto con una anchura de 50 y vacío, sin ningún texto asignado.

Debe quedar algo así:

insertando cuadro de url

Vamos con el botón que usaremos para descargar los datos de la url que hayamos seleccionado.  Como al pulsar el botón se ejecutará la función que descargue los datos, vamos a crear una función llamada pasar_datos(), de momento sin contenido, que será la que llame el botón al ser pulsado. La parte lógica la haremos después.

from guizero import *

def pasar_datos():
    pass

raiz = App(title='Contador de palabras en la red')
# Tamaño y posición de la ventana.
raiz.tk.geometry("700x400+350+150")
# Ventana no modificable.
raiz.tk.resizable(0, 0)

# Primer cuadro de texto (titulo + cuadro)
t1 = Text(raiz, text = "Dirección:", visible=False)
t1.tk.place(relx=.03, rely=0.05)
url = TextBox(raiz, width = 50, text = '', visible=False)
url.tk.place(relx=.15, rely=0.05)
# Botón descargar.
but1 = PushButton(raiz, text='Descargar', visible=False,
                  pady=3, command=pasar_datos)
but1.tk.place(relx=0.75, rely=0.045)

raiz.display()

Como haremos en todos los widgets hemos usado también posiciones relativas respecto al tamaño de la ventana (raiz). Con "pady=3" establecemos un poco de separación entre el texto y el borde del botón. En la práctica esto hace que el botón sea un poco más alto (eje de las y)

añadiendo botón descargar


El texto que aparecerá debajo de lo anterior para describir lo que irá en las cajas donde se mostrará el texto descargado y la tabla de frecuencias será "Texto: " y "Tabla de Frecuencias: ". Su código es el que sigue:

but1.tk.place(relx=0.75, rely=0.045)

# Texto encima de las cajas.
t2 = Text(raiz, text='Texto: ', visible=False)
t2.tk.place(relx=.03, rely=0.15)
t3 = Text(raiz, text='Tabla de frecuencias:', visible=False)
t3.tk.place(relx=.65, rely=0.15)

raiz.display()

Y para finalizar con el diseño ponemos unas cajas de texto donde aparecerá a la izquierda el texto que hayamos descargado y a la derecha se mostrará el resultado con las palabras que más se repiten:

t3.tk.place(relx=.65, rely=0.15)
t4 = TextBox(raiz, text='Prueba', height=15,
             width=50, multiline=True, visible=False, scrollbar=True)
t4.tk.place(relx=.03, rely=0.25)
t5 = TextBox(raiz, text='Palabra    |   Frecuencia', height=15,
             width=25, multiline=True, visible=False)
t5.tk.place(relx=.65, rely=0.25)

raiz.display()

Con el parámetro "multiline=True" permitimos que la caja que contendrá el texto pueda albergar múltiples líneas.

El resultado final será el diseño completo de la ventana de la aplicación.

diseño aplicación terminada


Vamos ahora con la parte lógica. Para no mezclar el código con el del archivo de diseño creamos un nuevo archivo. Yo lo llamaré "modquijote.py". Empezamos importando todas las bibliotecas necesarias que comentamos la empezar el capítulo.  Creamos también la función que descargará la página web y la convertirá en texto plano.

modquijote.py: Parte lógica del programa

import requests
import html2text
import re

def descargar_pagina(url):
    '''
    Lee una pagina web y la convierte en texto plano
    '''
    try:
        page = requests.get(url)
        if page.status_code != 200:
            return "Algo ha salido mal"
        content = html2text.html2text(page.text)
        return content
    except Exception:
        print("error")

A la función descarga_pagina(url) le pasaremos la dirección de una página web que en nuestro caso puede ser "https://www.gutenberg.org/cache/epub/2000/pg2000.txt".

El objeto page recogerá la página web a través de "request.get(url)". Pero claro, este objeto tiene muchas propiedades. La propiedad text de este objeto (page.text) lo que hace básicamente es devolver el contenido de page en formato unicode. Esto luego se lo pasamos como argumento a html2text.html2text() para terminar de obtener todo el contenido en texto plano. Si todo ha ido como debiera el page.status_code debe ser igual a 200 y devolverá el texto con el que trabajaremos, pero si no lo es, la función devolverá el mensaje "Algo ha salido mal".

En el archivo de diseño, cuando escribimos el botón "Descargar", le asignamos la función pasar_datos como parámetro.  Como te puedes imaginar será la encargada de pasar este texto dentro de Textbox (t4) que definimos anteriormente. 

Ya que estamos con la parte lógica vamos ya a dejar hecha la función que contará las palabras. Se llamará contar_palabras y le tendremos que pasar el texto plano como argumento. No voy a entrar en detalles de lo que hace pero básicamente usa una expresión regular para quitar los signo de puntuación (.!¡?¿ etc) y luego pasar todo a minúsculas y vamos guardando la frecuencia con la que aparecen las palabras en un diccionario (frec). Terminamos ordenándolo para ver las palabras más frecuentes.

El módulo final quedaría tal que así:

modquijote.py: Parte lógica del programa

import requests
import html2text
import re

def descargar_pagina(url):
    '''
    Lee una pagina web y la convierte en texto plano
    '''
    try:
        page = requests.get(url)
        if page.status_code != 200:
            return "Algo ha salido mal"
        content = html2text.html2text(page.text)
        return content
    except Exception:
        print("error")

def contar_palabras (texto) :
    '''
    Calcula la frecuencia de aparicion de cada palabra en un texto y genera una
    lista de pares (palabra, frecuencia) ordenada de mayor a menor frecuencia.
    '''
    frec={}
    texto = re.sub('[^\w\s]+', '', texto) # eliminamos signos de puntuación
    for w in texto.lower().split():
        if len(w)>3:
            frec[w]=frec.get(w, 0)+1
    # Ordenamos el diccionario por sus valores, no por sus claves. Y de mayor a menor.
    frec_sorted = sorted(frec.items(), key=lambda x: x [1] , reverse=True)   
    return frec_sorted

Solo nos queda encajar todo esto en nuestra ventana. Para ello importamos el módulo "modquijote" en nuestro archivo principal:

from guizero import *
# Importamos todo el módulo con las funciones auxiliares creadas.
import modquijote as qj

raiz = App(title='Contador de palabras en la red')
# Tamaño y posición de la ventana.

La jugada es la siguiente. El usuario introducirá la dirección de la página web a descargar en el Cuadro de texto creado para tal fin, que es el cuadro de texto que llamamos url. Al pulsar en el botón "Descargar" se lanzará un evento y se llamará a la función pasar_datos() que ya teniamos definida. Vamos a completarla:

from guizero import *
import modquijote as qj

max_frec = 20  # numero máximo de palabras a mostrar en la tabla frecuencias


def pasar_datos():
    texto = qj.descargar_pagina(url.value)
    t4.value = texto
    frec_items = qj.contar_palabras(texto)
    for k, v in frec_items[:max_frec]:
        show=f"{k:17} {v}"
        t5.append(show)


raiz = App(title='Contador de palabras en la red')

En la variable texto guardamos el texto plano que nos retorna la función descarga_página() al que hemos pasado como argumento el valor que había en la caja de texto "url" usando la propiedad "value" (url.value). Posteriormente este texto lo ponemos dentro de la caja de texto multilinea t4 usando su propiedad "value". 

t4.value = texto

Para finalizar desempaquetamos el diccionario que contiene las palabras junto con sus frecuencias ya ordenadas de mayor a  menor. Pero como no nos interesan todas, solo recorremos las 20 primeras (max_frecuc). Para terminar las añadimos a la caja de texto "t5", una por una usando append()  y formateando la palabra metiéndola dentro de una caja de 17 posiciones {k:17} para que quede bien.

Puedes encontrar los archivos en el enlace Contar_Palabras.