Implementazione di una GUI Tcl/Tk in Go senza CGo (modernc.org/tk9.0): packing dei widget e rendering delle immagini

Python include di default librerie GUI come Tkinter. Di recente è stata sviluppata anche per il linguaggio Go una libreria Tk CGo-Free e multipiattaforma CGo-Free, Cross Platform Tk 라이브러리 che permette di utilizzare Tcl/Tk. Oggi esamineremo il suo utilizzo di base.
Creare un Hello, Tk
Iniziamo con un semplice esempio '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}

Esaminiamo in dettaglio il codice di esempio precedente e il suo risultato.
Chi ha esperienza con Tk in Python comprenderà la struttura in cui i widget vengono impacchettati all'interno di una finestra o direttamente nella finestra principale. A seconda del tipo di widget, etichette e altri elementi vi sono inclusi.
Ipadx e Ipady sono l'abbreviazione di Internal padding e regolano lo spazio interno dei widget. In questo esempio viene regolato il margine del pulsante.
In questa libreria è presente la struttura Window, e la variabile App gestisce la finestra di primo livello. Essa è predefinita all'interno della libreria. Pertanto, la funzione tk.App.Destroy(), che termina tk.App.Wait(), svolge il ruolo di chiudere la finestra di primo livello.
Ora esaminiamo alcuni esempi presenti nella cartella _examples di GitLab.
Gestire file SVG
Di seguito è riportato un esempio che visualizza un file SVG in 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}

Il modo in cui questa libreria gestisce gli SVG è il seguente:
- Si legge il contenuto del file SVG come stringa (oppure lo si include direttamente, come nell'esempio sopra).
- Questo contenuto viene passato alla funzione Data per essere convertito in una stringa contenente le opzioni (opzione -data).
- I byte convertiti vengono passati alla funzione NewPhoto, che restituisce un puntatore alla struttura Img che rappresenta un'immagine Tcl/Tk.
- Attraverso la funzione Image, il puntatore alla struttura Img viene convertito in una stringa a cui viene aggiunta l'opzione -Image.
- La conversione in una stringa contenente il valore RAW della struttura serve alla creazione del widget Label.
I file ICO e PNG vengono gestiti in modo analogo.
Gestire file 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}

Il processo di gestione dei file PNG è il seguente:
- Il file gopher.png incorporato viene convertito in un tipo stringa contenente le opzioni.
- Tramite la funzione NewPhoto viene convertito in un tipo *Img.
- Dopo essere passato attraverso la funzione Image, viene convertito in una stringa RAW e poi creato come widget Label.
I file ICO vengono elaborati con lo stesso metodo, e l'unica differenza rispetto al formato SVG riguarda il modo in cui la funzione Data elabora il contenuto.
Esaminiamo ora la natura della "stringa contenente le opzioni":
1type rawOption string
La stringa contenente le opzioni menzionata in precedenza è semplicemente una stringa formattata.
1func (w *Window) optionString(_ *Window) string {
2 return w.String()
3}
Il metodo optionString è un metodo del puntatore Window e restituisce una stringa.
Infine, esaminiamo brevemente la struttura interna della funzione 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}
Dal codice si evince che per i file ICO e PNG sono necessari i processi di codifica e decodifica. Negli altri casi, senza alcuna conversione particolare, si aggiunge soltanto l'opzione -data alla stringa convertita in byte, per indicare che si tratta del risultato della funzione Data.
Caricare immagini con un widget di menu
Aggiungendo un widget di menu agli esempi di caricamento di PNG e ICO appena esercitati, si può creare un'applicazione che visualizza l'immagine desiderata.
Iniziamo con un semplice esempio di widget di menu.
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}

In questo esempio abbiamo sperimentato la barra dei menu e la creazione dei menu, l'evidenziazione dei caratteri, l'opzione Command, l'associazione delle scorciatoie da tastiera e l'impostazione della dimensione iniziale della finestra dell'applicazione.
Ora modificheremo la funzione Command assegnata a GetOpenFile per creare un programma che carica e visualizza un'immagine.
Il piano di sviluppo del programma è il seguente:
- Limitare l'apertura ai soli file PNG e ICO
- Elaborare il file selezionato nella finestra di dialogo del file
- Implementare un widget per la visualizzazione dell'immagine
Di seguito il codice che riflette questo piano:
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}

Il codice sopra funziona nel modo seguente:
- Si verifica l'estensione del file tramite le funzioni di confronto di stringhe del pacchetto strings
- Si apre, legge e chiude il file utilizzando il pacchetto os
- L'immagine letta viene visualizzata in un widget Label
Riepilogo
In questo articolo abbiamo trattato, utilizzando la libreria Tcl/Tk per Go, i seguenti contenuti:
- Struttura di base di un'applicazione GUI
- Gestione di diversi formati di immagine come SVG, PNG e ICO
- Impacchettamento dei widget e gestione del layout
- Struttura di elaborazione dei dati delle immagini
- Associazione di scorciatoie da tastiera e comandi dei widget
Utilizzando insieme Go e Tcl/Tk è possibile creare programmi GUI semplici ma pratici. Vi invitiamo a cimentarvi con applicazioni GUI più complesse partendo da queste basi.