jueves, 26 de marzo de 2020

Tkinter - de marciano a humano - sección 4 - gestión del diseño


Sección 4 - Gestión del diseño

Más adelante discutiremos sobre los widgets, los componentes básicos de su aplicación GUI.

4.0 ¿Cómo se organizan los widgets en una ventana?

Aunque hay tres diferentes "administradores de geometría" en Tkinter (pack, grid y place), el autor prefiere el administrador de geometría .grid() para casi todo (aunque se verán todos, el que más utilizaremos será .grid()). Este administrador trata cada ventana o marco como una tabla, una cuadrícula de filas y columnas.
  • Una celda es el área en la intersección de una fila y una columna.
  • El ancho de cada columna es el ancho de la celda más ancha en esa columna.
  • La altura de cada fila es la altura de la celda más grande de esa fila.
  • Para los widgets que no llenan toda la celda, puede especificar qué sucede con el espacio extra, por ejemplo puede dejar el espacio extra fuera del widget o bién estirar el widget para que se ajuste, ya sea en la dimensión horizontal como en la vertical.
  • Puede combinar varias celdas en un área más grande, este es un proceso llamado expansión.
Cuando crea un widget, no aparece hasta que lo registra en un administrador de geometría. Por lo tanto, la construcción y colocación de un widget es un proceso de dos pasos que va más o menos así:

self.thing=tk.Constructor(parent, ...)
self.thing.grid(...)

Donde Constructor es una de las clases de widgets como "Button", "Frame", etc., y "parent" es el widget principal en el que se está construyendo este widget secundario. Todos los widgets tienen un método .grid() que puede usar para indicarle al administrador de geometría dónde colocarlo.

4.1. El método .grid()

Para mostrar un widget "w" en la pantalla de su aplicación:

w.grid(option=value, ...)

Este método registra un widget "w" con el administrador de geometría de cuadrícula, si no hace esto, el widget existirá internamente, pero no será visible en la pantalla.
Para explicar mejor lo anteriormente dicho, veamos este fragmento del código de nuestra aplicación de ejemplo:

46 self.botonSalida.grid(row=0, column=0, pady=5, padx=5)

Como obsevaremos a continuación, si comentamos esta línea, agregando un "#" en la parte izquierda al inicio del renglón, el botón estará en el programa, pero NO se mostrará en la ventana, el colocar un "hash" delante del renglón es lo que se llama "comentar una línea", es útil a la hora de testear y es una practica bastante común, veamos este renglón comentado y el resultado obtenido:

46 # self.botonSalida.grid(row=0, column=0, pady=5, padx=5)



Como podemos ver, ahora no aparece el botón, pero si "descomentamos" la línea quitando el "#" entonces volverá a verse la ventana y el botón como se muestra en la sección 2.

4.1.1 Argumentos del administrador de geometría .grid()

 

Tabla 1: Argumentos del administrador de geometría .grid()
column El número de columna donde desea que se cuadricule el widget, contando desde cero. El valor por defecto es cero.
columnspan Normalmente, un widget ocupa solo una celda en la cuadrícula, sin embargo, puede tomar varias celdas de una fila y fusionarlas en una celda más grande configurando la opción de la columna en el número de celdas, por ejemplo:

w.grid(row=0, column=2, columnspan=3)

colocaría el widget w en una celda que abarca las columnas 2, 3 y 4 de la fila 0.
_in Para registrar "w" como hijo de algún widget "w2", debemos usar in_=w2. El nuevo padre "w2" debe ser un descendiente del widget principal utilizado cuando se creó "w".
ipadx Espacio interno "x". Esta dimensión se agrega dentro del widget dentro de su lado izquierdo y derecho.
ipady Espacio interno "y". Esta dimensión se agrega dentro del widget en su parte superior e inferior.
padx Espacio externo "x". Esta dimensión se agrega a la izquierda y a la derecha "fuera del widget".
pady Espacio externo "y". Esta dimensión se agrega arriba y debajo "fuera del widget".
row El número de fila en el que desea insertar el widget, contando desde 0. El valor predeterminado es el de la siguiente fila desocupada con un número más alto.
rowspan Normalmente un widget ocupa solo una celda en la cuadrícula, pero puedes agarrar múltiples adyacentes. Sin embargo, las celdas de una columna establecen la opción de la "fila de filas" en el número de celdas que se desea capturar. Esta opción se puede usar en combinación con la opción de columnas para tomar un bloque de células. Por ejemplo:

w.grid(row=3, column=2, rowspan=4, columnspan=5)

lo anterior colocaría el widget "w" en un área formada al fusionar 20 celdas, con los números de fila 3–6 y números de columna 2–6.
sticky Esta opción determina cómo distribuir cualquier espacio extra dentro de la celda que no se tome arriba por el widget en su tamaño natural, lo que causa este efecto es el de "estirar" el widget en todo el espacio posible de acuerdo a como estiremos la ventana, por ejemplo, si estiramos la ventana hacia abajo el botón se estirará hacia abajo, si estiramos la ventana hacia un lado se estirará de lado, todo esto depende de las siguientes configuraciones:
  • Si no proporciona un atributo fijo, el comportamiento predeterminado es centrar el widget en la celda.
  • Puede colocar el widget en una esquina de la celda usando:
    • sticky=tk.NE (arriba a la derecha)
    • sticky=tk.SE (abajo a la derecha)
    • sticky=tk.SW (abajo a la izquierda)
    • sticky=tk.NW (arriba a la izquierda)
  • Puede colocar el widget centrado contra un lado de la celda usando:
    • sticky=tk.N (centro superior)
    • sticky=tk.E (centro derecho)
    • sticky=tk.S (centro inferior)
    • sticky=tk.W (centro izquierdo)

Podemos usar sticky=tk.N+tk.S para estirar el widget verticalmente pero déjelo centrado horizontalmente.

Podemos usar sticky=tk.E+tk.W para estirarlo horizontalmente pero déjelo centrado verticalmente.

podemos usar sticky=tk.N+tk.E+tk.S+tk.W para estirar el widget tanto horizontal como verticalmente para llenar la celda.

Las otras combinaciones también funcionarán. Por ejemplo, sticky=tk.N+tk.S+tk.W estirará el widget verticalmente y lo colocará contra la pared oeste (izquierda).

Si al script le agregamos un padx=0, pady=0 (que sería el default) no va a a notarse diferencia en el diseño, pero si colocamos, por ejemplo, padx=5, pady=5 entonces notamos una mejora ya que “despega” nuestro botón de los bordes en 5 pixeles, en este cuadro se pueden comparar ambas imágenes:

.grid(row=0, column=0, pady=0, padx=0)

.grid(row=0, column=0, pady=5, padx=5)

 

 

 4.2. Otros métodos para gestionar .grid()

Estos métodos relacionados con la cuadrícula se definen en todos los widgets:

 

w.grid_bbox(column=None, row=None, col2=None, row2=None)

Devuelve una tupla de 4 valores que describe el cuadro delimitador de algunos o todos los sistemas de cuadrícula en el widget w, en este caso column y row son las coordenadas x e y de la esquina superior izquierda del área, col2 y row2 son el ancho y la altura. Si pasa argumentos de columna y fila, el cuadro delimitador devuelto describe el área de la celda en esa columna y fila. Si también pasa los argumentos col2 y row2, el cuadro delimitador devuelto describe el área de la cuadrícula de columnas column a col2 inclusive, y de las filas row a row2 inclusive. Por ejemplo, w.grid_bbox(0, 0, 1, 1) devuelve el cuadro delimitador de cuatro celdas, no una.

w.grid_forget()

Este método hace que el widget w desaparezca de la pantalla. Todavía existe, simplemente no es visible. Puede usar .grid() para que aparezca de nuevo, pero no recordará sus opciones de cuadrícula.

w.grid_info()

Devuelve un diccionario cuyas claves son nombres de opciones de w, con los valores correspondientes de esas opciones.

w.grid_location(x, y)

Dadas las coordenadas (x, y) relativas al widget que contiene, este método devuelve una tupla (column, row) que describe qué celda del sistema de cuadrícula de w contiene esa coordenada de pantalla.

w.grid_propagate()

Normalmente, todos los widgets propagan sus dimensiones, lo que significa que se ajustan para adaptarse a los contenidos. Sin embargo, a veces desea forzar que un widget tenga un tamaño determinado, independientemente del tamaño de su contenido. Para hacer esto se llama a w.grid_propagate(0) donde w es el widget cuyo tamaño desea forzar.

w.grid_remove()

Este método es como .grid_forget(), pero sus opciones de cuadrícula se recuerdan, por lo que si .grid() lo vuelve a usar, usará las mismas opciones de configuración de cuadrícula.

w.grid_size()

Devuelve una tupla de 2 valores que contiene el número de columnas y el número de filas respectivamente, en el sistema de cuadrícula de w.

w.grid_slaves (row=None, column=None)

Devuelve una lista de los widgets administrados por el widget w. Si no se proporcionan argumentos, obtendrá una lista de todos los widgets administrados. Use el argumento row= para seleccionar solo los widgets en una fila, o el argumento column= para seleccionar solo los widgets en una columna.

4.3. Configurar tamaños de columna y fila

A menos que tome ciertas medidas, el ancho de una columna de cuadrícula dentro de un widget dado será igual al ancho de su celda más ancha, y el alto de una fila de cuadrícula será el alto de su celda más alta. El atributo adhesivo (sticky) en un widget controla solo dónde se colocará si no llena completamente la celda. Si desea anular este tamaño automático de columnas y filas, use estos métodos en el widget principal w que contiene el diseño de cuadrícula:

w.columnconfigure(N, option=value, ...)

En el diseño de cuadrícula dentro del widget w, configure la columna N para que la opción dada tenga el valor dado.

w.rowconfigure(N, option=value, ...)

En el diseño de cuadrícula dentro del widget w, configure la fila N para que la opción dada tenga el valor dado.


Tabla 2: Opciones de configuración de columna y fila para el administrador de geometría .grid()
minimize El tamaño mínimo de la columna o fila en píxeles. Si no hay nada en la columna o fila dada, no aparecerá, incluso si usa esta opción.
pad Una cantidad de píxeles que se agregarán a la columna o fila dada, más allá de la celda más grande en la columna o fila.
weight para hacer que una columna o fila se pueda estirar, use esta opción y proporcione un valor que le proporcione “peso relativo” a esta columna o fila al distribuir el espacio extra. Por ejemplo, si un widget w contiene un diseño de cuadrícula, estas líneas distribuirán tres cuartas partes del espacio extra para la primera columna y un cuarto para la segunda columna, si no se usa esta opción, la columna o fila no se estirará.

w.columnconfigure (0, weight=3) w.columnconfigure (1, weight=1)

4.4. Hacer que la ventana raíz sea redimensionable

¿Desea que el usuario cambie el tamaño de toda la ventana de su aplicación y distribuya el espacio adicional entre sus widgets internos? Esto requiere algunas operaciones que no son obvias.
Es necesario utilizar las técnicas para la gestión del tamaño de fila y columna, descritas en la Sección 4.3 para hacer que la cuadrícula del widget de la Aplicación se pueda estirar, pero eso por sí solo no es suficiente.
Considere la aplicación básica discutida en la Sección 2, que contiene solo un botón SALIR. Si ejecuta esta aplicación y cambia el tamaño de la ventana, el botón permanece del mismo tamaño y en el mismo lugar (fila 0 columna 0, o sea arriba a la izquierda).
Aquí hay una nueva versión del ejemplo 001 que reemplaza al método .__createWidgets(), en esta versión, el botón "SALIR" siempre llena todo el espacio disponible.

#!/user/bin/env python
#-------------------------------------------------------------------------------
# Name: tkinter ejemplo 002
# Purpose: El proposito de este script es demostrar características básicas
# de tkinter, es parte de la sección 4.4 de la guía de tkinter 8.5
#
# Author: Mauricio José Tobares
#
# Created: 25/01/2020
# Copyright: (c) SplinterStudio 2020
# Licence:
#-------------------------------------------------------------------------------

'de entrada esta importación no es correcta en python 3.x'
# import Tkinter as tk
'la forma correcta de importar tkinter es esta:'
import tkinter as tk

class Prototipo2(tk.Frame):
    def __init__(self, parent):
        super(Application, self).__init__(parent)
        'se trae el contenido de la función crearboton'
        self.crearboton()
    def crearboton(self):
        'ventana de nivel superior.'
        top=self.winfo_toplevel()
        'hace que la fila 0 de la ventana de nivel superior pueda expandirse.'
        top.rowconfigure (0, weight=1)
        'hace que la columna 0 de la ventana de nivel superior pueda expandirse.'
        top.columnconfigure (0, weight=1)
        'hace que la fila 0 del widget pueda expandirse'
        self.rowconfigure (0, weight=1)
        'hace que la columna 0 del widget pueda expandirse'
        self.columnconfigure (0, weight=1)
        'esto es así porque se destruye a root que es el padre o raiz.'
        self.botonSalida=tk.Button(self, text='SALIR', command=root.destroy)
        self.botonSalida.grid(row=0, column=0, sticky=tk.N+tk.S+tk.E+tk.W)

if __name__ == "__main__":
    'ventana root o raiz'
    root=tk.Tk()
    'con esto ejecutamos la aplicación'
    main=Prototipo2(root)
    main.pack(fill="both", expand=True)
    root.mainloop()


La "ventana de nivel superior" es la ventana más externa de la pantalla, sin embargo, esta no es la ventana de su aplicación, es la principal de la instancia de la aplicación.
Para obtener la ventana de nivel superior deberá llamar al método .winfo_toplevel() en cualquier widget de su aplicación (no quiero apurarme mucho pero puede consultar la Sección 26).
La línea top.rowconfigure(0, weight=1) hace que la fila "0" de la cuadrícula de la ventana del nivel superior se pueda estirar, la línea que sigue, top.columnconfigure(0, weight=1) hace lo mismo que la anterior pero con la columna "0".
Los siguientes dos renglones, self.rowconfigure(0, weight=1) y self.columnconfigure(0, weight=1) hacen que la fila "0" y la columna "0" de la cuadrícula del widget Prototipo2 se puedan estirar.
El argumento sticky=tk.N+tk.S+tk.E+tk.W hace que el botón se expanda para llenar su celda de la cuadrícula.
Si notamos en este ejemplo quitamos la parte en que la ventana inicia maximizada para que el ejemplo sea más claro.

Hasta el próximo jueves, salud y saludos!
Mauricio José Tobares

jueves, 19 de marzo de 2020

Tkinter - de marciano a humano - sección 3 - Definiciones


Sección 3 - Definiciones

Antes de continuar, definamos algunos de los términos comunes (y algunos ya utilizados en la revisión de código del primer ejemplo).

window:

Este término tiene diferentes significados en diferentes contextos, pero en general se refiere a un área rectangular en algún lugar de la pantalla.

top-level window:

Una ventana que existe independientemente en su pantalla. Estará decorado con el marco estándar y los controles para el administrador de escritorio de su sistema. Puede moverlo en su escritorio. En general, puede cambiar su tamaño, aunque su aplicación puede evitar esto.

Widget:

El término genérico para cualquiera de los componentes básicos que componen una aplicación en una interfaz gráfica de usuario. Ejemplos de widgets:

Botones (Button)
Botones de radio (Radiobutton)
Campos de texto (text field)
Marcos (Frame)
Etiquetas de texto (text label)

NOTA: Para facilitar la lectura de este y otras publicaciones posteriores muchas veces me voy a referir a un radiobutton como "radiobutton y no como botón de radio, ya que es más simple entender que un radiobutton es un radiobutton y no que es un "botón de radio", lo tenemos más incorporado como terminología que como su traducción literal y no me refiero solo al radiobutton, en general hay muchas palabras que es mejor NO traducir literalmente aunque tienen una traducción.

Frame:

En Tkinter, el widget Frame es la unidad básica de organización para diseños complejos. Un marco (en adelante "Frame") es un área rectangular que puede contener otros widgets.

parent-child:

Cuando se crea cualquier widget, se crea una relación padre-hijo, por ejemplo, si coloca una etiqueta de texto (en adelante Label) dentro de un Frame, el Frame es el padre del  Label.

Hasta el próximo jueves, salud y saludos!
Mauricio José Tobares

jueves, 12 de marzo de 2020

Tkinter - de marciano a humano - sección 2 - Una aplicación mínima


Sección 2 - Una aplicación mínima

La semana anterior en la sección 1 vimos la generador de interfaz gráfica de usuario multiplataforma para Python, en esta ocación veamos la forma de hacer una ventana básica, este pequeño programa solamente contiene un botón para salir:

Crearemos un archivo prototipo001.py y dentro del mismo colocaremos el siguiente código:
(cambienle el autor si quieren...)

01 #!/user/bin/env python
02 #-------------------------------------------------------------------------------
03 # Name: tkinter ejemplo 001
04 # Purpose: El proposito de este script es demostrar las características
05 # básicas de tkinter y llegar a crear interfaces profesionales,
06 # es en parte inspirado en la guía oficial de tkinter 8.5 y con
07 # conocimientos ampliados que luego fueron incorporados a la guía
08 #
09 # Author: Mauricio José Tobares
10 #
11 # Created: 24/01/2020
12 # Copyright: (c) SplinterStudio 2020
13 # Licence: <lo que? licencia? que es eso?>
14 #-------------------------------------------------------------------------------
15
16 from tkinter import *
17 from tkinter import ttk
18
19 # Crea una clase Python para definir el interfaz de usuario de la aplicación.
20 # Cuando se cree un objeto del tipo 'Prototipo1' se ejecutará automáticamente
21 # el método __init__() que construye y muestra la ventana con todos sus widgets
22 class Prototipo1():
23 def __init__(self):
24 'ventana root o raiz'
25 self.root=Tk()
26 'título de la ventana root'
27 tituloVentanaRoot="Prototipo 001"
28 self.root.title(tituloVentanaRoot)
29 ' se coloca el boton de salida'
30 'esto es así porque se destruye a root que es el padre o raiz.'
31 self.botonSalida=ttk.Button(self.root, text='SALIR', command=self.root.destroy)
32 self.botonSalida.grid(row=0, column=0)
33 'se ejecuta el programa'
34 self.root.mainloop()
35
36 # Define la función main() que es en realidad la que indica
37 # el comienzo del programa. Dentro de ella se crea el objeto
38 # aplicación 'mi_app' basado en la clase 'Prototipo1':
39 def main():
40 mi_app=Prototipo1()
41 return 0
42
43 # Mediante el atributo __name__ tenemos acceso al nombre de un
44 # un módulo. Python utiliza este atributo cuando se ejecuta
45 # un programa para conocer si el módulo es ejecutado de forma
46 # independiente (en ese caso __name__ = '__main__') o es
47 # importado:
48 if __name__ == '__main__':
49 main()

ACLARACIÓN: yo se que se ve feo el poner el número de línea, pero este manual va de principiante que no sabe absolutamente nada a contenidos avanzados, por tanto al inicio será muy conveniente colocar el número de la línea!

nuestra pequeña aplicación se ve así:
Por ahora se ve bastante fea... la típica interfaz marciana tan cuestionada por muchos...
Como se puede apreciar hay unas cuantas diferencias entre Python 2.7 y Pythn 3.8.1, principalmente la forma en que se trabajan (con clases), el documento original NO FUNCIONARÁ en las versiones de python 3.x (este ejemplo fue testeado en Python 3.8.1), a lo largo de todo este artículo se trabajaremos solo con Python 3.8.1, seguramente funcionará de igual modo en las versiones anteriores de Python 3.x.

Agregando unos extras

Para comenzar vamos a colocar un logo al proyecto (un pequeño .ico de 128x128 que encontré en una web de iconos gratuitos), lo que se debe hacer es (y como es lógico) pensar a futuro, para ello vamos a crear una carpera llamada “modulos” en la carpeta donde se tiene el script prototipo001.py, dentro de esta carpeta colocaremos una carpeta llamada “img”, y dentro de esta carpeta pondremos nuestro logo.ico. Una ruta posible hacia nuestro pequeño logo sería ./modulos/img/logo.ico

Ahora bien, vamos a los códigos, en primer lugar colocaremos a continuación de la línea n° 28 el siguiente código:

28           self.root.title(tituloVentanaRoot) # dejo este renglon solo para referencia
29           'con esto colocamos el logo personalizado de la ventana root'
30           try:
31               'el icono DEBE ser .ico de lo contrario no funcionara'
32               self.root.iconbitmap('./modulos/img/logo.ico') # ATENCION: Cuidado con la ruta del ícono!!
33           except TclError:
34               'imprime este mensaje si el icono no se encuentra o no pudo ser cargado'
35               print("ERROR: El icono de la ventana no pudo ser cargado")
36               pass
El resultado se verá así:
Pueden poner cualquier logo... yo puse ese porque es lo que encontré!!

Ahora le agrego una forma de maximizar la ventana al tamaño que tenga el usuario, aporte que se agradece pues la mayoría de los programas comienzan maximizados (o al menos esa debería ser la regla general)
NOTA IMPORTANTE:
Es siempre recomendable que a la ventana padre se la llame “root”, ya que la gran mayoría de los programadores así le llama a la raiz de sus aplicaciones, en nuestro caso la ventana padre.

Siguiendo el mismo procedimiento de agregar código colocaremos a partir de la línea n° 25 lo siguiente:

25        self.root=Tk()
26        'lo siguiente hace que la ventana root inicie maximizada'
27        toplevel=self.root.winfo_toplevel()
28        toplevel.wm_state('zoomed')

No voy a profundizar mucho en detalles porque seguidamente se van a explicar algunos conceptos.

Hasta el próximo jueves, salud y saludos!
Mauricio José Tobares

jueves, 5 de marzo de 2020

TkInter - de marciano a humano - sección 1 - Generador de interfaz gráfica de usuario multiplataforma para Python

TKINTER DE MARCIANO A HUMANO

Sección 1 - Generador de interfaz gráfica de usuario multiplataforma para Python

Tkinter es un widget de interfaz gráfica de usuario (GUI) configurado para Python.

El documento original fue escrito para Python 2.7 y Tkinter 8.5 ejecutándose en el sistema X Window bajo Linux, pero como bien se aclaró en el prefacio esta versión del documento fue adaptada a la versión Python 3.8.1, su versión puede variar mucho, poquito o nada desde Python 2.7 a la 3.8.1 (o superiores) por lo que hay que tener muy en cuenta, todo el tiempo, que este documento fue probado en un sistema windows 10 (es lo que hay!!), python 3.8.1 y tkinter 8.5.

Comenzaremos mirando la parte visible de Tkinter creando los widgets y organizándolos en la pantalla. Más adelante hablaremos sobre cómo conectar la cara (el "panel frontal") de la aplicación a la lógica detrás de ella.

Refefrencias pertinentes (en perfecto ingles):

  • Fredrik Lundh, who wrote Tkinter, has two versions of his An Introduction to Tkinter: a more complete 1999 version 3 and a 2005 version 4 that presents a few newer features.

  • Python 2.7 quick reference 5: general information about the Python language.

Para ver un ejemplo de una aplicación de trabajo considerable (alrededor de 1000 líneas de código), vea una herramienta de selección de color y fuente. El diseño de esta aplicación demuestra cómo construir widgets compuestos.

TkInter - de marciano a humano - sección 13 - El Widget LabelFrame

  13 El widget LabelFrame El widget LabelFrame al igual que el widget Frame es un contenedor espacial, un área rectangular que puede...