Implementación de una GUI en Go Tcl/Tk sin CGo (modernc.org/tk9.0): empaquetado de widgets y renderizado de imágenes

Python incluye de forma nativa bibliotecas GUI como Tkinter. Recientemente, también se ha desarrollado para Go la biblioteca Tk multiplataforma sin CGo, que permite utilizar Tcl/Tk. Hoy veremos su uso básico.
Crear Hello, Tk
Comencemos con un sencillo ejemplo 'Hello, TK!'.
1package main
2
3import tk "modernc.org/tk9.0"
4
5func main() {
6 tk.Pack(
7 tk.TButton(
8 tk.Txt("Hello, TK!"),
9 tk.Command(func() {
10 tk.Destroy(tk.App)
11 })),
12 tk.Ipadx(10), tk.Ipady(5), tk.Padx(15), tk.Pady(10),
13 )
14 tk.App.Wait()
15}

Examinemos en detalle el código de ejemplo anterior y su resultado de ejecución.
Si tiene experiencia con Tk en Python, comprenderá la estructura en la que los widgets se empaquetan dentro de una ventana o directamente bajo ella. Según el tipo de widget, las etiquetas y otros elementos se incluyen dentro de él.
Ipadx e Ipady son abreviaturas de Internal padding (relleno interno), y controlan el espaciado interno de los widgets. En este ejemplo, se ajusta el espaciado del botón.
Esta biblioteca dispone de una estructura Window, y la variable App gestiona la ventana de nivel superior. Esta variable está predefinida dentro de la biblioteca. Por lo tanto, la función tk.App.Destroy(), que finaliza tk.App.Wait(), cumple la función de cerrar la ventana de nivel superior.
Veamos ahora algunos ejemplos que se encuentran en la carpeta _examples de GitLab.
Procesar archivos SVG
A continuación se muestra un ejemplo que muestra un archivo SVG en un widget de etiqueta.
1package main
2
3import . "modernc.org/tk9.0"
4
5// https://en.wikipedia.org/wiki/SVG
6const svg = `<?xml version="1.0" encoding="UTF-8" standalone="no"?>
7<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
8<svg width="391" height="391" viewBox="-70.5 -70.5 391 391" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
9<rect fill="#fff" stroke="#000" x="-70" y="-70" width="390" height="390"/>
10<g opacity="0.8">
11 <rect x="25" y="25" width="200" height="200" fill="lime" stroke-width="4" stroke="pink" />
12 <circle cx="125" cy="125" r="75" fill="orange" />
13 <polyline points="50,150 50,200 200,200 200,100" stroke="red" stroke-width="4" fill="none" />
14 <line x1="50" y1="50" x2="200" y2="200" stroke="blue" stroke-width="4" />
15</g>
16</svg>`
17
18func main() {
19 Pack(Label(Image(NewPhoto(Data(svg)))),
20 TExit(),
21 Padx("1m"), Pady("2m"), Ipadx("1m"), Ipady("1m"))
22 App.Center().Wait()
23}

El procedimiento que esta biblioteca utiliza para procesar SVG es el siguiente:
- Se lee el contenido del archivo SVG como una cadena (o se incluye directamente en el código, como en el ejemplo anterior).
- Este contenido se pasa a la función Data, que lo convierte en una cadena que incluye opciones (opción -data).
- Los bytes convertidos se pasan a la función NewPhoto, que devuelve un puntero a la estructura Img que representa una imagen de Tcl/Tk.
- Al pasar por la función Image, el puntero a la estructura Img se convierte en una cadena a la que se añade la opción -Image.
- Se convierte en una cadena que contiene el valor RAW de la estructura, con el fin de crear el widget Label.
Los archivos ICO y PNG se procesan de manera similar.
Procesar archivos PNG
1package main
2
3import _ "embed"
4import . "modernc.org/tk9.0"
5
6//go:embed gopher.png
7var gopher []byte
8
9func main() {
10 Pack(Label(Image(NewPhoto(Data(gopher)))),
11 TExit(),
12 Padx("1m"), Pady("2m"), Ipadx("1m"), Ipady("1m"))
13 App.Center().Wait()
14}

El procedimiento para procesar archivos PNG es el siguiente:
- Se convierte el archivo gopher.png incrustado en un tipo de cadena que incluye opciones.
- Mediante la función NewPhoto, se convierte en un tipo *Img.
- Tras pasar por la función Image, se convierte en una cadena RAW y se crea como widget de etiqueta.
Los archivos ICO se procesan del mismo modo, y la única diferencia con el formato SVG está en el modo en que la función Data realiza el procesamiento interno.
Aquí veremos qué es realmente la "cadena con opciones":
1type rawOption string
La cadena con opciones mencionada anteriormente no es más que una cadena con formato.
1func (w *Window) optionString(_ *Window) string {
2 return w.String()
3}
El método optionString es un método del puntero Window que devuelve una cadena.
Por último, veamos brevemente la estructura interna de la función Data:
1func Data(val any) Opt {
2 switch x := val.(type) {
3 case []byte:
4 switch {
5 case bytes.HasPrefix(x, pngSig):
6 // correcto
7 case bytes.HasPrefix(x, icoSig):
8 b := bytes.NewBuffer(x)
9 img, err := ico.Decode(bytes.NewReader(x))
10 if err != nil {
11 fail(err)
12 return rawOption("")
13 }
14
15 b.Reset()
16 if err := png.Encode(b, img); err != nil {
17 fail(err)
18 return rawOption("")
19 }
20
21 val = b.Bytes()
22 }
23 }
24 return rawOption(fmt.Sprintf(`-data %s`, optionString(val)))
25}
Como se observa en el código, los archivos ICO y PNG requieren un proceso de codificación y decodificación. En los demás casos, no se realiza ninguna conversión especial: simplemente se añade la opción -data a la cadena convertida a bytes, para indicar que es el resultado de la función Data.
Cargar imágenes con un widget de menú
Si añadimos un widget de menú al ejemplo de carga de PNG e ICO que acabamos de practicar, podremos crear una aplicación que muestre la imagen que necesitemos.
Primero, veamos un sencillo ejemplo de widget de menú.
1package main
2
3import (
4 "fmt"
5 . "modernc.org/tk9.0"
6 "runtime"
7)
8
9func main() {
10 menubar := Menu()
11
12 fileMenu := menubar.Menu()
13 fileMenu.AddCommand(Lbl("New"), Underline(0), Accelerator("Ctrl+N"))
14 fileMenu.AddCommand(Lbl("Open..."), Underline(0), Accelerator("Ctrl+O"), Command(func() { GetOpenFile() }))
15 Bind(App, "<Control-o>", Command(func() { fileMenu.Invoke(1) }))
16 fileMenu.AddCommand(Lbl("Save As..."), Underline(5))
17 fileMenu.AddSeparator()
18 fileMenu.AddCommand(Lbl("Exit"), Underline(1), Accelerator("Ctrl+Q"), ExitHandler())
19 Bind(App, "<Control-q>", Command(func() { fileMenu.Invoke(4) }))
20 menubar.AddCascade(Lbl("File"), Underline(0), Mnu(fileMenu))
21
22 App.WmTitle(fmt.Sprintf("%s on %s", App.WmTitle(""), runtime.GOOS))
23 App.Configure(Mnu(menubar), Width("8c"), Height("6c")).Wait()
24}

En este ejemplo hemos practicado la barra de menús y la creación de menús, el subrayado de letras, la opción Command, la asignación de atajos de teclado y la configuración del tamaño inicial de la ventana de la aplicación.
Ahora modificaremos la función Command asociada a GetOpenFile para crear un programa que cargue y muestre imágenes.
El plan de desarrollo del programa es el siguiente:
- Permitir abrir únicamente archivos PNG e ICO
- Procesar el archivo seleccionado en el cuadro de diálogo de archivos
- Implementar un widget para mostrar la imagen
El código que refleja este plan es el siguiente:
1package main
2
3import (
4 "fmt"
5 "log"
6 "os"
7 "runtime"
8 "strings"
9
10 . "modernc.org/tk9.0"
11)
12
13func handleFileOpen() {
14 s := GetOpenFile()
15 for _, photo := range s {
16 formatCheck := strings.Split(photo, ".")
17 format := formatCheck[len(formatCheck)-1]
18
19 if (strings.Compare(format, "png") == 0) || (strings.Compare(format, "ico") == 0) {
20 picFile, err := os.Open(photo)
21 if err != nil {
22 log.Println("Error while opening photo, error is: ", err)
23 }
24
25 pictureRawData := make([]byte, 10000*10000)
26 picFile.Read(pictureRawData)
27
28 imageLabel := Label(Image(NewPhoto(Data(pictureRawData))))
29 Pack(imageLabel,
30 TExit(),
31 Padx("1m"), Pady("2m"), Ipadx("1m"), Ipady("1m"))
32 }
33 picFile.Close()
34 }
35}
36
37func main() {
38 menubar := Menu()
39
40 fileMenu := menubar.Menu()
41 fileMenu.AddCommand(Lbl("Open..."), Underline(0), Accelerator("Ctrl+O"), Command(handleFileOpen))
42 Bind(App, "<Control-o>", Command(func() { fileMenu.Invoke(0) }))
43 fileMenu.AddCommand(Lbl("Exit"), Underline(1), Accelerator("Ctrl+Q"), ExitHandler())
44 Bind(App, "<Control-q>", Command(func() { fileMenu.Invoke(1) }))
45 menubar.AddCascade(Lbl("File"), Underline(0), Mnu(fileMenu))
46
47 App.WmTitle(fmt.Sprintf("%s on %s", App.WmTitle(""), runtime.GOOS))
48 App.Configure(Mnu(menubar), Width("10c"), Height("10c")).Wait()
49}

El código anterior funciona de la siguiente manera:
- Verifica la extensión del archivo mediante las funciones de comparación de cadenas del paquete strings
- Abre, lee y cierra el archivo utilizando el paquete os
- La imagen leída se muestra mediante un widget de etiqueta
Resumen
En este artículo hemos tratado los siguientes contenidos utilizando la biblioteca Tcl/Tk para Go:
- Estructura básica de una aplicación GUI
- Procesamiento de diversos formatos de imagen, como SVG, PNG e ICO
- Empaquetado de widgets y gestión del diseño
- Estructura de procesamiento de datos de imagen
- Asignación de atajos de teclado y comandos de widgets
Al usar Go junto con Tcl/Tk se pueden crear programas GUI sencillos y prácticos. Le animamos a partir de estos conocimientos para afrontar el desarrollo de aplicaciones GUI más complejas.