Íconos en ventanas de Tk (tkinter)

Íconos en ventanas de Tk (tkinter)

Tanto las ventanas principales (creadas vía la clase tk.Tk) como las ventanas secundarias (tk.Toplevel) tienen por defecto un ícono con el logo de Tcl/Tk. Configurar las ventanas de una aplicación de escritorio con íconos propios le dará a nuestro producto un aire más profesional. El ícono de una ventana también suele ser mostrado por el sistema operativo en la barra de tareas o aplicaciones. En este artículo analizaremos las diversas formas de hacerlo en las distintas plataformas soportadas por Tk (Windows, Linux y Mac).

Las ventanas de Tk proveen tres métodos para configurar un ícono:

  • iconbitmap()
  • iconmask()
  • iconphoto()

La función iconmask() tiene el objetivo de permitir áreas transparentes en el ícono de nuestra ventana. Sin embargo, hace tiempo que los diversos formatos de imagen (PNG, GIF, JPG, etc.) y de íconos (especialmente .ico) soportan transparencia. Por lo tanto, en la mayoría de los casos iconmask() es innecesaria. Si tenemos el ícono de nuestra aplicación en un archivo de imagen, la forma más sencilla de cargarlo en la ventana es vía iconphoto():

import tkinter as tk

ventana = tk.Tk()
ventana.title("Ventana con ícono")
ventana.geometry("300x200")
# Cargar el archivo de imagen desde el disco.
icono = tk.PhotoImage(file="icon-16.png")
# Establecerlo como ícono de la ventana.
ventana.iconphoto(True, icono)
ventana.mainloop()

Puedes descargar nuestros íconos de prueba (clic aquí) para correr el código de este artículo. El archivo comprimido contiene:

Archivo Vista previa Observaciones
icon-16.png 16×16 px
icon-32.png 32×32 px
icon.ico   Contiene los dos anteriores. Solo Windows.

Los íconos deben estar en la misma carpeta que nuestro programa o, mejor dicho, en el directorio actual de trabajo. Si no sabes qué significa esto, tenemos un artículo para explicarlo: La línea de comandos (o terminal) para pythonistas.

Tras correr el código, obtenemos el siguiente resultado:

Ventana con ícono en Tk

El funcionamiento del código es sencillo: cargamos un archivo de imagen vía la clase tk.PhotoImage y luego la asignamos como ícono de la ventana a través del método iconphoto(). El primer argumento es un booleano que indica si ese mismo ícono debe aplicarse a ventanas secundarias. Este procedimiento de cargar el ícono de la ventana a partir de un archivo de imagen es soportado por todos los sistemas operativos.

Además de mostrarse en la ventana, el ícono también suele mostrarse en la barra de tareas o aplicaciones. Por lo general, el tamaño adecuado para el ícono de la ventana es 16×16 píxeles. Llamaremos a esta imagen el «ícono chico». En cambio, en la barra de tareas se muestra un «ícono grande», que en Windows suele ser una imagen de 32×32. Estos valores pueden cambiar, sin embargo, acorde a la configuración de pantalla de cada sistema. Puesto que hemos cargado un ícono chico, veremos que el sistema operativo agranda automáticamente la imagen de 16×16 en la barra de tareas, produciendo un efecto pixelado:

Ícono pixelado en la barra de tareas

Si cargamos un ícono grande en lugar de un ícono chico, se verá bien en la barra de tareas, pero igualmente pixelado en la ventana. Por fortuna, el método iconphoto() puede recibir dos íconos de tamaños distintos (uno chico y otro grande) para mostrar correctamente en un caso y otro:

import tkinter as tk

ventana = tk.Tk()
ventana.title("Ventana con ícono")
ventana.geometry("300x200")
icono_chico = tk.PhotoImage(file="icon-16.png")
icono_grande = tk.PhotoImage(file="icon-32.png")
ventana.iconphoto(False, icono_grande, icono_chico)
ventana.mainloop()

De este modo, el sistema operativo muestra el ícono grande en la barra de tareas y el ícono chico en la ventana:

Ícono en ventana y en barra de tareas

Nótese que hemos cambiado el primer argumento de iconphoto() de True a False, puesto que, por alguna razón que desconocemos (tal vez un error de Tk), si el primer argumento es verdadero, en Windows el ícono chico es desestimado y el grande se muestra tanto en la ventana como en la barra de tareas. Del mismo modo, algunos sistemas operativos toman en consideración solo el ícono grande (y realmente no hay nada que podamos hacer para cambiar esto).

Los tamaños de las imágenes pueden variar, siempre y cuando haya un ícono más grande que el otro. El ícono chico podría ser de 24×24 y el grande de 64×64, o cualquier otra combinación. Los tamaños ideales para cada contexto (ventana y barra de tareas o aplicaciones) dependen del sistema operativo y, en distribuciones de Linux, de cada distribución en particular. En Ubuntu, por ejemplo, el tamaño ideal para el ícono grande es 64×64. En Windows, por lo general los tamaños adecuados son 16×16 y 32×32, tal como los venimos usando.

Ahora bien, en Windows podemos recurrir al método iconbitmap() para establecer el ícono de la ventana a partir de un archivo de extensión .ico. Este formato de Microsoft es más práctico porque permite tener múltiples imágenes de diversos tamaños (16×16, 32×32, 64×64, etc.) incluidos en un solo archivo .ico. Algunos programas para crear y editar este tipo de archivos son Greenfish Icon Editor (gratuito) e icofx. Así, por ejemplo, el siguiente código establece el archivo icon.ico (disponible en la descarga más arriba) como ícono de la ventana:

import tkinter as tk

ventana = tk.Tk()
ventana.title("Ventana con ícono")
ventana.geometry("300x200")
ventana.iconbitmap("icon.ico")
ventana.mainloop()

Puesto que el archivo icon.ico contiene una imagen de 16×16 y otra de 32×32, el ícono chico se muestra automáticamente en la ventana y el grande en la barra de tareas.

Empaquetar íconos en un archivo ejecutable

Los códigos anteriores requieren que los íconos estén presentes en el sistema de archivos para cargarse en la ventana. Sin embargo, si convertimos nuestra aplicación de escritorio a un archivo ejecutable usando herramientas como PyInstaller o cx_Freeze, tal vez queramos que el ejecutable no tenga ninguna dependencia, de modo que pueda moverse de un lugar a otro sin tener que llevar consigo un puñado de archivos. O tal vez simplemente no queremos tener que distribuir nuestros íconos como archivos aparte. En estos casos, ¿de dónde cargamos el ícono? Hay dos soluciones para esto.

Si nuestra aplicación solo tiene como objetivo sistemas Windows, entonces empaquetar los íconos en el ejecutable es muy sencillo porque Tk soporta cargar íconos directamente desde archivos ejecutables (.exe) vía iconbitmap(). Supongamos que convertimos el programa a un ejecutable usando PyInstaller:

pyinstaller --icon=icon.ico --noconsole app.py

Aquí app.py es el nombre de nuestro archivo de Python, --noconsole indica que es una aplicación de escritorio, --icon pasa el nombre del archivo .ico que queremos incluir en el ejecutable final.

Hecho esto, podemos usar el siguiente código para cargar en la ventana el ícono empaquetado dentro del archivo ejecutable de Windows:

import sys
import tkinter as tk

ventana = tk.Tk()
ventana.title("Ventana con ícono")
ventana.geometry("300x200")
# Cargar en la ventana el ícono del ejecutable.
ventana.iconbitmap(sys.executable)
ventana.mainloop()

En Windows, el método iconbitmap() también acepta como argumento, además de un archivo .ico, un archivo .exe cuyo ícono quiera emplearse como ícono de la ventana. Puesto que sys.executable contiene la ruta de nuestro ejecutable, pasándolo como argumento logramos que siempre se cargue el ícono desde el ejecutable, sin necesidad de distribuir archivos .ico o .png aparte. Nótese que este procedimiento implica que cuando el código se ejecuta desde el archivo .py, la ventana recibe el ícono de Python, puesto que sys.executable contiene en ese caso la ruta del intérprete.

Una segunda opción, menos elegante, pero funcional para todos los sistemas operativos, consiste en codificar el contenido de los íconos con Base64 e incluirlos en nuestro archivo de código de fuente. Los datos de imagen pueden luego ser cargados directamente de una variable vía tk.PhotoImage. Para ello, necesitamos primero un código que lea el contenido de una imagen y lo codifique con Base64. Esto puede conseguirse fácilmente con un pequeño programa de Python:

# codificar_icono.py
from base64 import b64encode
import sys
import pyperclip

filename = sys.argv[1]
with open(filename, "rb") as f:
    pyperclip.copy(b64encode(f.read()).decode("ascii"))
    print("Copiado al portapapeles.")

Este programita recibe por consola el nombre de un ícono, lo codifica vía Base64 y lo copia al portapapeles. Requiere que el módulo pyperclip esté instalado (vía pip install pyperclip). Lista esta herramienta, empecemos por codificar el ícono pequeño ejecutando en la terminal:

python codificar_icono.py icon-16.png

Luego, peguemos en una variable los datos copiados al portapepeles y carguémoslos vía tk.PhotoImage:

from base64 import b64decode
import tkinter as tk

icono_chico_datos = "pegar-aquí-los-datos"

ventana = tk.Tk()
ventana.title("Ventana con ícono")
ventana.geometry("300x200")
icono_chico = tk.PhotoImage(data=b64decode(icono_chico_datos))
ventana.iconphoto(False, icono_chico)
ventana.mainloop()

Hagamos lo mismo para el ícono grande:

python codificar_icono.py icon-32.png

Y finalmente:

from base64 import b64decode
import tkinter as tk

icono_chico_datos = "pegar-aquí-los-datos-del-ícono-chico"
icono_grande_datos = "pegar-aquí-los-datos-del-ícono-grande"

ventana = tk.Tk()
ventana.title("Ventana con ícono")
ventana.geometry("300x200")
icono_chico = tk.PhotoImage(data=b64decode(icono_chico_datos))
icono_grande = tk.PhotoImage(data=b64decode(icono_grande_datos))
ventana.iconphoto(False, icono_grande, icono_chico)
ventana.mainloop()

¡Listo! Los datos de nuestros íconos están incrustados en el código, por lo cual no será necesario distribuir archivos .png adicionales.

Curso online 👨‍💻

¡Ya lanzamos el curso oficial de Recursos Python en Udemy! Un curso moderno para aprender Python desde cero con programación orientada a objetos, SQL y tkinter en 2024.

Consultoría 💡

Ofrecemos servicios profesionales de desarrollo y capacitación en Python a personas y empresas. Consultanos por tu proyecto.

Deja una respuesta