Creación de GUI con Tk en Go
Python incorpora bibliotecas GUI como Tkinter de forma predeterminada. Recientemente, una biblioteca Tk multiplataforma CGo-Free ha sido desarrollada para permitir el uso de Tcl/Tk en el lenguaje Go. Hoy examinaremos su uso fundamental.
Creación de "Hello, Tk"
Comenzaremos con un ejemplo sencillo de "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 y los resultados de la ejecución.
Quienes tengan experiencia con Tk en Python comprenderán la estructura en la que los widgets se empaquetan dentro de una ventana o directamente debajo de ella. Dependiendo del tipo de widget, se incluyen elementos como etiquetas.
Ipadx e Ipady son abreviaturas de "Internal padding", y ajustan el margen de los widgets internos. En este ejemplo, se ajusta el margen del botón.
Esta biblioteca posee una estructura Window, y la variable App gestiona la ventana superior. Esta ya está predefinida dentro de la biblioteca. Por lo tanto, la función tk.App.Destroy(), que finaliza tk.App.Wait(), se encarga de cerrar la ventana superior.
Ahora, examinaremos algunos ejemplos ubicados en la carpeta _examples de GitLab.
Procesamiento de archivos SVG
A continuación, se presenta un ejemplo que muestra un archivo SVG en un widget Label.
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 método para procesar SVG en esta biblioteca es el siguiente:
- Se lee el contenido del archivo SVG como una cadena de texto (o se incluye directamente como en el ejemplo anterior).
- Este contenido se pasa a la función Data para convertirlo en una cadena con opciones (opción -data).
- El valor de bytes convertido se pasa a la función NewPhoto, la cual retorna un puntero a una estructura Img que representa una imagen Tcl/Tk.
- Al pasar por la función Image, el puntero a la estructura Img se convierte en una cadena a la que se le añade la opción -Image.
- La razón para convertirlo en una cadena que contiene el valor RAW de la estructura es para la creación del widget Label.
Los archivos ICO y PNG se procesan de manera similar.
Procesamiento de 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 proceso de procesamiento de archivos PNG es el siguiente:
- El archivo gopher.png incrustado se convierte a un tipo de cadena que contiene opciones.
- Se convierte a un tipo *Img a través de la función NewPhoto.
- Tras pasar por la función Image y ser convertido a una cadena RAW, se genera como un widget de etiqueta.
Los archivos ICO se procesan de la misma manera, y la única diferencia con el formato SVG reside en el método de procesamiento interno de la función Data.
Examinemos ahora la naturaleza de la "cadena con opciones":
1type rawOption string
La cadena con opciones mencionada anteriormente no es más que una cadena formateada.
1func (w *Window) optionString(_ *Window) string {
2 return w.String()
3}
El método optionString es un método para un puntero Window que devuelve una cadena.
Finalmente, examinaremos 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 // ok
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}
Observando el código, los archivos ICO o PNG requieren un proceso de codificación/decodificación. En otros casos, la cadena convertida a tipo byte simplemente se le añade la opción -data sin ninguna conversión especial para indicar que es el resultado de la función Data.
Carga de imágenes con el widget de menú
Si agregamos un widget de menú a los ejemplos anteriores de carga de PNG e ICO, podemos crear una aplicación que muestre las imágenes necesarias.
Primero, examinemos un ejemplo sencillo 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, configuramos una barra de menú y un menú, resaltamos texto, utilizamos la opción Command, vinculamos atajos de teclado y ajustamos el tamaño inicial de la ventana de la aplicación.
Ahora, modificaremos la función Command designada como GetOpenFile para crear un programa que cargue y muestre imágenes.
El plan para desarrollar el programa es el siguiente:
- Restringir la apertura únicamente a archivos PNG e ICO.
- Procesar el archivo seleccionado desde el diálogo de archivo.
- Implementar un widget para la visualización de imágenes.
A continuación, se presenta el código que refleja este plan:
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 opera de la siguiente manera:
- Se verifica la extensión del archivo utilizando la función de comparación de cadenas del paquete strings.
- Se abre, lee y cierra el archivo utilizando el paquete os.
- La imagen leída se muestra como un widget de etiqueta.
Resumen
En este artículo, hemos abordado los siguientes temas utilizando la biblioteca Tcl/Tk del lenguaje 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 de diseño.
- Estructura de procesamiento de datos de imagen.
- Vinculación de atajos de teclado y comandos de widgets.
La combinación de Go y Tcl/Tk permite crear programas GUI sencillos pero prácticos. Con base en esto, le animamos a que se atreva a desarrollar aplicaciones GUI más complejas.