Implementarea unei interfețe grafice Tcl/Tk în Go fără CGo (modernc.org/tk9.0): împachetarea widget-urilor și randarea imaginilor

Python include în mod implicit biblioteci GUI precum Tkinter. Recent, a fost dezvoltată și CGo-Free, Cross Platform Tk library, care permite utilizarea Tcl/Tk și în limbajul Go. Astăzi vom analiza utilizarea sa de bază.
Crearea unui Hello, Tk
Vom începe cu un exemplu simplu '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}

Vom analiza mai detaliat exemplul de cod de mai sus și rezultatul său de execuție.
Dacă ați utilizat anterior Tk în Python, veți înțelege structura în care widgeturile sunt împachetate într-o fereastră sau direct în fereastra de sus. În funcție de tipul widgetului, etichetele și alte elemente sunt incluse în acesta.
Ipadx și Ipady sunt abrevieri ale Internal padding (padding intern), care ajustează spațiile interioare ale widgeturilor. În acest exemplu, se ajustează spațiul interior al butonului.
Această bibliotecă dispune de structura Window, iar variabila App gestionează fereastra de nivel superior. Aceasta este definită în prealabil în interiorul bibliotecii. Astfel, funcția tk.App.Destroy(), care încheie tk.App.Wait(), are rolul de a închide fereastra de nivel superior.
Acum vom analiza câteva exemple din folderul _examples de pe GitLab.
Procesarea fișierelor SVG
Următorul exemplu afișează un fișier SVG într-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}

Modul în care această bibliotecă procesează SVG este următorul:
- Citiți conținutul fișierul SVG ca șir de caractere (sau includeți-l direct, ca în exemplul de mai sus).
- Transmiteți acest conținut funcției Data, pentru a-l converti într-un șir de caractere care include opțiuni (opțiunea -data).
- Octeții convertiți sunt transmiși funcției NewPhoto, care returnează un pointer către structura Img, reprezentând o imagine Tcl/Tk.
- Prin funcția Image, pointerul către structura Img este convertit într-un șir de caractere la care se adaugă opțiunea -Image.
- Valoarea RAW a structurii este convertită într-un șir de caractere, motivul fiind crearea widgetului Label.
Fișierele ICO și PNG sunt procesate într-un mod similar.
Procesarea fișierelor 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}

Procesul de tratare a fișierelor PNG este următorul:
- Convertiți fișierul gopher.png inclus (embedded) într-un tip de șir de caractere cu opțiuni.
- Convertiți-l în tipul *Img prin funcția NewPhoto.
- Prin funcția Image, se obține un șir de caractere RAW, după care se creează widgetul Label.
Fișierele ICO sunt procesate în aceeași manieră, iar diferența față de formatul SVG constă doar în modul de procesare din interiorul funcției Data.
Să analizăm acum natura „șirului de caractere cu opțiuni”:
1type rawOption string
Șirul de caractere cu opțiuni menționat anterior este, pur și simplu, un șir formatat.
1func (w *Window) optionString(_ *Window) string {
2 return w.String()
3}
Metoda optionString este o metodă a pointerului Window și returnează un șir de caractere.
În final, să analizăm pe scurt structura internă a funcției 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}
Din cod reiese că fișierele ICO sau PNG necesită un proces de codificare/decodificare. În celelalte cazuri, nu se efectuează nicio transformare specială; se adaugă doar opțiunea -data la șirul convertit în octeți, pentru a indica faptul că acesta este rezultatul funcției Data.
Încărcarea imaginilor cu ajutorul unui widget de meniu
Adăugând un widget de meniu la exemplul de încărcare a fișierelor PNG și ICO exersat anterior, se poate crea o aplicație care afișează imaginea necesară.
Mai întâi, să analizăm un exemplu simplu de widget de meniu.
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}

În acest exemplu, am exersat bara de meniu și crearea meniurilor, evidențierea literelor, opțiunea Command, legarea tastelor de scurtătură și stabilirea dimensiunii inițiale a ferestrei aplicației.
Acum vom modifica funcția Command atribuită lui GetOpenFile, pentru a crea un program care încarcă și afișează imagini.
Planul de realizare a programului este următorul:
- Restricționarea deschiderii doar a fișierelor PNG și ICO
- Procesarea fișierului selectat din dialogul de fișiere
- Implementarea unui widget pentru afișarea imaginii
Următorul cod reflectă acest 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}

Codul de mai sus funcționează în felul următor:
- Se verifică extensia fișierului prin funcția de comparare a șirurilor din pachetul strings
- Se deschide, se citește și se închide fișierul folosind pachetul os
- Imaginea citită este afișată într-un widget Label
Concluzie
În acest articol, am tratat următoarele subiecte folosind biblioteca Tcl/Tk din limbajul Go:
- Structura de bază a unei aplicații GUI
- Procesarea diverselor formate de imagini, precum SVG, PNG și ICO
- Împachetarea widgeturilor și gestionarea layout-ului
- Structura de procesare a datelor imaginilor
- Legarea tastelor de scurtătură și comenzile widgeturilor
Utilizând împreună limbajul Go și Tcl/Tk, se pot crea programe GUI simple, dar practice. Vă recomand să încercați dezvoltarea unor aplicații GUI mai complexe pornind de la aceste baze.