Barra de menú en Tcl/Tk (tkinter)

Barra de menú en Tcl/Tk (tkinter)

El control tk.Menu permite añadir una barra de menús a la ventana principal (tk.Tk) o a una ventana secundaria (tk.Toplevel) en una aplicación de escritorio de Tk. Los menús de una ventana contienen un texto y/o una imagen y pueden ser asociados a funciones para responder ante la presión del usuario.

Ventana con menú

En la imagen vemos una barra de menús en la ventana principal, un menú con el título «Archivo» y un botón dentro de este menú con el texto «Nuevo», el atajo «Ctrl+N» y una imagen a modo de ícono. Para implementar una funcionalidad como esta, lo primordial es crear una barra de menús vía la clase tk.Menu y configurarla en la ventana principal.

import tkinter as tk

ventana = tk.Tk()
ventana.title("Barra de menús en Tk")
ventana.config(width=400, height=300)
# Crear una barra de menús.
barra_menus = tk.Menu()
# Insertarla en la ventana principal.
ventana.config(menu=barra_menus)
ventana.mainloop()

Una barra de menús vacía no es de mucha utilidad: ni siquiera se ve en la ventana. Agregúemosle el primer menú de este artículo, con el título «Archivo», como suele aparecer en muchas aplicaciones de escritorio.

import tkinter as tk

ventana = tk.Tk()
ventana.title("Barra de menús en Tk")
ventana.config(width=400, height=300)
barra_menus = tk.Menu()
# Crear el primer menú.
menu_archivo = tk.Menu(barra_menus, tearoff=False)
# Agregarlo a la barra.
barra_menus.add_cascade(menu=menu_archivo, label="Archivo")
ventana.config(menu=barra_menus)
ventana.mainloop()

Cada menú que queramos ubicar en la barra de menús también se crea usando la clase tk.Menu. De modo que ahora tenemos dos instancias: barra_menus, el contenedor de todos los menús de la ventana, y menu_archivo, menú al cual en seguida agregaremos algunos botones. Para agregar menús a una barra de menús se utiliza el método add_cascade(), que recibe como argumento el menú (menu) para ser insertado y el texto (label) con que se quiere mostrar.

Nótese que en la creación de menu_archivo (línea 8) pasamos como primer argumento la barra de menús dentro de la cual queremos ubicarlo. El segundo argumento tearoff=False evita que Tk agregue una funcionalidad para desacoplar el menú de la ventana. Esta es una extraña opción muy rara vez necesaria y poco estimada por los usuarios, razón por la cual la desactivamos. Si te interesa ver de qué se trata, prueba iniciar el menú con el valor por defecto tearoff=True.

Ahora ya podemos visualizar nuestra barra de menús con el único menú Archivo, pero sin botón alguno. Para añadir un botón o una opción a un menú utilizamos el método add_command().

import tkinter as tk

def archivo_nuevo_presionado():
    print("¡Has presionado para crear un nuevo archivo!")

ventana = tk.Tk()
ventana.title("Barra de menús en Tk")
ventana.config(width=400, height=300)
barra_menus = tk.Menu()
menu_archivo = tk.Menu(barra_menus, tearoff=False)
menu_archivo.add_command(
    label="Nuevo",
    accelerator="Ctrl+N",
    command=archivo_nuevo_presionado
)
barra_menus.add_cascade(menu=menu_archivo, label="Archivo")
ventana.config(menu=barra_menus)
ventana.mainloop()

Aquí insertamos en nuestro menú un botón con texto «Nuevo» (argumento label) y un indicador de atajo del teclado «Ctrl+N» (accelerator). El argumento command funciona de la misma manera que el parámetro homónimo en los botones: recibe el nombre de una función que será invocada cuando el usuario presione sobre esa opción del menú.

El parámetro accelerator simplemente indica el atajo del teclado. Para que efectivamente el menú se active con el atajo, hay que asociar el evento correspondiente en la ventana y todos sus controles:

import tkinter as tk

def archivo_nuevo_presionado(event=None):
    print("¡Has presionado para crear un nuevo archivo!")

ventana = tk.Tk()
ventana.title("Barra de menús en Tk")
ventana.config(width=400, height=300)
barra_menus = tk.Menu()
menu_archivo = tk.Menu(barra_menus, tearoff=False)
menu_archivo.add_command(
    label="Nuevo",
    accelerator="Ctrl+N",
    command=archivo_nuevo_presionado
)
# Asociar el atajo del teclado del menú "Nuevo".
ventana.bind_all("<Control-n>", archivo_nuevo_presionado)
barra_menus.add_cascade(menu=menu_archivo, label="Archivo")
ventana.config(menu=barra_menus)
ventana.mainloop()

Además de un texto, el botón de un menú puede tener una imagen. La configuración opera de forma similar a la de una imagen en un botón. Se crea una instancia de tk.PhotoImage con el nombre del archivo y luego se pasa como argumento al método add_command(). El argumento compound especifica en qué lugar debe aparecer la imagen respecto del texto.

import tkinter as tk

def archivo_nuevo_presionado(event=None):
    print("¡Has presionado para crear un nuevo archivo!")

ventana = tk.Tk()
ventana.title("Barra de menús en Tk")
ventana.config(width=400, height=300)
barra_menus = tk.Menu()
menu_archivo = tk.Menu(barra_menus, tearoff=False)
img_menu_nuevo = tk.PhotoImage(file="nuevo_archivo.png")
menu_archivo.add_command(
    label="Nuevo",
    accelerator="Ctrl+N",
    command=archivo_nuevo_presionado,
    image=img_menu_nuevo,
    # Indicar que la imagen debe aparecer a la izquierda del texto.
    compound=tk.LEFT
)
ventana.bind_all("<Control-n>", archivo_nuevo_presionado)
barra_menus.add_cascade(menu=menu_archivo, label="Archivo")
ventana.config(menu=barra_menus)
ventana.mainloop()

Otros valores posibles para compound son tk.TOP (arriba), tk.BOTTOM (abajo) y tk.RIGHT (derecha). Puedes descargar la imagen nuevo_archivo.png desde este enlace. El archivo debe estar en la misma carpeta que la aplicación.

Ahora bien, de seguro un menú tendrá más de un botón, así que podemos invocar add_command() tantas veces como sea necesario. Las opciones de un menú aparecen en la interfaz en el mismo orden en que fueron añadidas en el código. Agreguemos un nuevo botón al menu_archivo para cerrar el programa.

import tkinter as tk

def archivo_nuevo_presionado(event=None):
    print("¡Has presionado para crear un nuevo archivo!")

ventana = tk.Tk()
ventana.title("Barra de menús en Tk")
ventana.config(width=400, height=300)
barra_menus = tk.Menu()
menu_archivo = tk.Menu(barra_menus, tearoff=False)
img_menu_nuevo = tk.PhotoImage(file="nuevo_archivo.png")
menu_archivo.add_command(
    label="Nuevo",
    accelerator="Ctrl+N",
    command=archivo_nuevo_presionado,
    image=img_menu_nuevo,
    compound=tk.LEFT
)
ventana.bind_all("<Control-n>", archivo_nuevo_presionado)
menu_archivo.add_separator()
menu_archivo.add_command(label="Salir", command=ventana.destroy)
barra_menus.add_cascade(menu=menu_archivo, label="Archivo")
ventana.config(menu=barra_menus)
ventana.mainloop()

El método add_separator() introduce una línea horizontal que es útil para separar grupos de botones de menús relacionados. El resultado es el siguiente.

Ventana con dos botones de menú

Del mismo modo, con sucesivas llamadas a add_cascade() podemos agregar otros menús a la barra de menús. El siguiente código agrega un segundo menú con el título «Opciones».

import tkinter as tk

def archivo_nuevo_presionado(event=None):
    print("¡Has presionado para crear un nuevo archivo!")

ventana = tk.Tk()
ventana.title("Barra de menús en Tk")
ventana.config(width=400, height=300)
barra_menus = tk.Menu()
menu_archivo = tk.Menu(barra_menus, tearoff=False)
img_menu_nuevo = tk.PhotoImage(file="nuevo_archivo.png")
menu_archivo.add_command(
    label="Nuevo",
    accelerator="Ctrl+N",
    command=archivo_nuevo_presionado,
    image=img_menu_nuevo,
    compound=tk.LEFT
)
ventana.bind_all("<Control-n>", archivo_nuevo_presionado)
menu_archivo.add_separator()
menu_archivo.add_command(label="Salir", command=ventana.destroy)
menu_opciones = tk.Menu(barra_menus, tearoff=False)
barra_menus.add_cascade(menu=menu_archivo, label="Archivo")
barra_menus.add_cascade(menu=menu_opciones, label="Opciones")
ventana.config(menu=barra_menus)
ventana.mainloop()

Ventana con dos menús

Un menú puede contener botones tradicionales, como los que acabamos de usar para las opciones «Nuevo» y «Salir», o botones con casillas de verificación. El funcionamiento de estos últimos es similar al de la clase ttk.Checkbutton. Los botones de menús con casilla de verificación se comportan igual que los botones normales, pero contienen además un valor booleano que es alterado cada vez que el usuario presiona sobre ellos. Este valor booleano está representado por la presencia de una marca a la izquierda del texto del botón.

Menú con casilla de verificación

La imagen muestra el botón con casilla de verificación «Iniciar con sistema» que indica, en una hipotética aplicación, si el programa debe iniciarse con el sistema (para una implementación real de esta funcionalidad en Windows véase este artículo). El usuario puede habilitar o deshabilitar esa opción a través del menú de opciones. La función asociada con este botón es invocada cada vez que el usuario cambia la opción. El código es el siguiente.

import tkinter as tk

def archivo_nuevo_presionado(event=None):
    print("¡Has presionado para crear un nuevo archivo!")

def menu_iniciar_con_sistema_presionado():
    if iniciar_con_sistema.get():
        print("Opción establecida (iniciar con el sistema).")
    else:
        print("Opción deshabilitada (no iniciar con el sistema).")

ventana = tk.Tk()
ventana.title("Barra de menús en Tk")
ventana.config(width=400, height=300)
barra_menus = tk.Menu()
menu_archivo = tk.Menu(barra_menus, tearoff=False)
img_menu_nuevo = tk.PhotoImage(file="nuevo_archivo.png")
menu_archivo.add_command(
    label="Nuevo",
    accelerator="Ctrl+N",
    command=archivo_nuevo_presionado,
    image=img_menu_nuevo,
    compound=tk.LEFT
)
ventana.bind_all("<Control-n>", archivo_nuevo_presionado)
menu_archivo.add_separator()
menu_archivo.add_command(label="Salir", command=ventana.destroy)
menu_opciones = tk.Menu(barra_menus, tearoff=False)
iniciar_con_sistema = tk.BooleanVar()
menu_opciones.add_checkbutton(
    label="Iniciar con sistema",
    command=menu_iniciar_con_sistema_presionado,
    variable=iniciar_con_sistema
)
barra_menus.add_cascade(menu=menu_archivo, label="Archivo")
barra_menus.add_cascade(menu=menu_opciones, label="Opciones")
ventana.config(menu=barra_menus)
ventana.mainloop()

Como se observa en la línea 30, para crear un botón con casilla de verificación dentro de un menú se utiliza add_checkbutton() en lugar de add_command(). A este método es necesario pasarle una variable booleana de Tk creada vía la clase tk.BooleanVar() (líneas 29 y 33). Cada vez que el usuario presiona el botón, Tk cambia el valor booleano de la instancia iniciar_con_sistema y además invoca la función menu_iniciar_con_sistema_presionado(). El valor booleano de iniciar_con_sistema puede ser leído o establecido vía código a través de los método get() (tal como ocurre en la línea 7) y set().

Esta misma lógica tiene el método add_radiobutton(), empleado para añadir varios botones con casilla de verificación dentro de un menú, pero que tienen una relación entre sí de tal modo que cuando uno de ellos está activado, el resto se desactiva. Por ejemplo, si queremos permitirle al usuario elegir a través de los menús de nuestra aplicación el color del tema de la interfaz entre las opciones «Claro» y «Oscuro», sería conveniente emplear un par de botones de este estilo.

import tkinter as tk

def archivo_nuevo_presionado(event=None):
    print("¡Has presionado para crear un nuevo archivo!")

def menu_iniciar_con_sistema_presionado():
    if iniciar_con_sistema.get():
        print("Opción establecida (iniciar con el sistema).")
    else:
        print("Opción deshabilitada (no iniciar con el sistema).")

def menu_tema_presionado():
    valor_tema = tema_elegido.get()
    if valor_tema == 1:
        print("Tema claro establecido.")
    elif valor_tema == 2:
        print("Tema oscuro establecido.")

ventana = tk.Tk()
ventana.title("Barra de menús en Tk")
ventana.config(width=400, height=300)
barra_menus = tk.Menu()
menu_archivo = tk.Menu(barra_menus, tearoff=False)
img_menu_nuevo = tk.PhotoImage(file="nuevo_archivo.png")
menu_archivo.add_command(
    label="Nuevo",
    accelerator="Ctrl+N",
    command=archivo_nuevo_presionado,
    image=img_menu_nuevo,
    compound=tk.LEFT
)
ventana.bind_all("<Control-n>", archivo_nuevo_presionado)
menu_archivo.add_separator()
menu_archivo.add_command(label="Salir", command=ventana.destroy)
menu_opciones = tk.Menu(barra_menus, tearoff=False)
iniciar_con_sistema = tk.BooleanVar()
menu_opciones.add_checkbutton(
    label="Iniciar con sistema",
    command=menu_iniciar_con_sistema_presionado,
    variable=iniciar_con_sistema
)
menu_tema = tk.Menu(barra_menus, tearoff=False)
tema_elegido = tk.IntVar()
tema_elegido.set(1)  # Opción seleccionada por defecto ("Claro").
menu_tema.add_radiobutton(
    label="Claro",
    variable=tema_elegido,
    value=1,
    command=menu_tema_presionado
)
menu_tema.add_radiobutton(
    label="Oscuro",
    value=2,
    variable=tema_elegido,
    command=menu_tema_presionado
)
menu_opciones.add_cascade(menu=menu_tema, label="Tema")
barra_menus.add_cascade(menu=menu_archivo, label="Archivo")
barra_menus.add_cascade(menu=menu_opciones, label="Opciones")
ventana.config(menu=barra_menus)
ventana.mainloop()

Menú de opciones con radiobutton

Hay varias cosas para notar en este código. En primer lugar, creamos un nuevo menú llamado menu_tema que fue a su vez añadido al menú de opciones vía add_cascade() (líneas 42 y 57). Como el mismo nombre lo indica (cascade, «cascada»), puede haber menús dentro de otros menús que se despliegan en forma de cascada tal como se observa en la imagen. Segundo, dentro del menu_tema insertamos dos botones vía los métodos add_radiobutton(), pero a diferencia de la opción anterior insertada con add_checkbutton(), aquí ambas opciones hacen referencia a la misma variable entera tema_elegido. El hecho de que refieran a la misma variable le indica a Tk que esas dos opciones (aunque podrían ser más de dos) son incompatibles: cuando el usuario presiona el botón «Claro», se remueve la selección del tema «Oscuro» y viceversa. Por último, nótese que cada llamada a add_radiobutton() incluye un argumento value que indica el valor numérico (pues la variable creada es tk.IntVar) que representa la opción que se está agregando al menú. Este valor será retornado por menu_tema.get() cuando la opción esté seleccionada, como vemos en la función menu_tema_presionado().

Estados

El botón de un menú puede estar habilitado o deshabilitado, igual que un botón. Cuando está desactivado, el texto y la imagen se muestran con un color diferente y el usuario no puede presionar sobre él. Los métodos add_command(), add_checkbutton() y add_radiobutton() soportan el argumento state, que denota el estado del botón.

import tkinter as tk

ventana = tk.Tk()
ventana.title("Barra de menús en Tk")
ventana.config(width=400, height=300)
barra_menus = tk.Menu()
menu_archivo = tk.Menu(barra_menus, tearoff=False)
menu_archivo.add_command(
    label="Nuevo",
    accelerator="Ctrl+N",
    state=tk.DISABLED
)
barra_menus.add_cascade(menu=menu_archivo, label="Archivo")
ventana.config(menu=barra_menus)
ventana.mainloop()

Ventana con menú deshabilitado

La constante tk.DISABLED indica que el menú está deshabilitado. El menú puede ser rehabilitado cambiando el valor de state por tk.NORMAL.

# En algún otro lugar del código o en respuesta a un evento.
menu_archivo.entryconfig(0, state=tk.NORMAL)

El método entryconfig() es empleado para alterar cualquiera de las opciones pasadas como argumentos a las tres funciones disponibles para agregar botones a un menú. Cada botón es identificado por un índice, que representa su posición en el menú y que se pasa como primer argumento. Aquí el menú «Nuevo» tiene el índice 0. Otras opciones, tales como label, accelerator, command, etc., pueden asimismo alterarse vía entryconfig() en cualquier parte del código.

Estilos

La apariencia de los botones de los menús se personaliza pasando argumentos a las funciones add_command(), add_checkbutton() y add_radiobutton(). Las tres soportan cinco propiedades que establecen el tipo de letra y los colores de los botones.

import tkinter as tk
from tkinter import font

ventana = tk.Tk()
ventana.title("Barra de menús en Tk")
ventana.config(width=400, height=300)
barra_menus = tk.Menu()
menu_archivo = tk.Menu(barra_menus, tearoff=False)
menu_archivo.add_command(
    label="Nuevo",
    accelerator="Ctrl+N",
    # Tipo de fuente.
    font=font.Font(family="Times", size=14),
    # Color de fondo.
    background="#ADD8E6",
    # Color del texto.
    foreground="#FF0000",
    # Color de fondo cuanto el botón tiene el foco.
    activebackground="#32CDFF",
    # Color del texto cuando el botón tiene el foco.
    activeforeground="#FFFF00"
)
barra_menus.add_cascade(menu=menu_archivo, label="Archivo")
ventana.config(menu=barra_menus)
ventana.mainloop()

Menú con estilos

Para una explicación completa de la clase Font y las opciones que acepta, véase el apartado Tipo de letra en nuestro artículo sobre las cajas de texto.



Deja una respuesta