Implémentation d'une interface graphique Tcl/Tk en Go sans CGo (modernc.org/tk9.0) : gestion du placement des widgets et rendu d'images

Python est livré avec des bibliothèques GUI intégrées par défaut, comme Tkinter. Récemment, la bibliothèque CGo-Free, Cross Platform Tk a été développée pour permettre d'utiliser Tcl/Tk depuis le langage Go. Aujourd'hui, nous allons examiner ses bases d'utilisation.
Créer un Hello, Tk
Commençons par un exemple simple « 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}

Examinons en détail le code d'exemple ci-dessus et son résultat d'exécution.
Si vous avez déjà utilisé Tk avec Python, vous comprendrez la structure où les widgets sont placés (packed) dans une fenêtre ou directement sous la fenêtre. Selon le type de widget, des étiquettes (labels) et autres éléments y sont inclus.
Ipadx et Ipady sont les abréviations de Internal padding (marge interne) ; ils ajustent les marges internes des widgets. Dans cet exemple, ce sont les marges du bouton qui sont ajustées.
Cette bibliothèque possède une structure Window, et la variable App gère la fenêtre de niveau supérieur. Celle-ci est prédéfinie à l'intérieur de la bibliothèque. Ainsi, la fonction tk.App.Destroy(), qui met fin à tk.App.Wait(), joue le rôle de fermer la fenêtre de niveau supérieur.
Examinons maintenant quelques exemples présents dans le dossier _examples de GitLab.
Traiter des fichiers SVG
Voici un exemple qui affiche un fichier SVG dans 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}

La manière dont cette bibliothèque traite le SVG est la suivante :
- Lire le contenu du fichier SVG sous forme de chaîne de caractères (ou l'inclure directement, comme dans l'exemple ci-dessus).
- Transmettre ce contenu à la fonction Data pour le convertir en chaîne de caractères contenant les options (option -data).
- Les octets convertis sont transmis à la fonction NewPhoto, qui renvoie un pointeur vers la structure Img représentant une image Tcl/Tk.
- En passant par la fonction Image, le pointeur de la structure Img est converti en chaîne de caractères à laquelle est ajoutée l'option -Image.
- La conversion en chaîne contenant la valeur RAW de la structure a pour but la création du widget Label.
Les fichiers ICO et PNG sont traités de manière similaire.
Traiter des fichiers 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}

Le processus de traitement d'un fichier PNG est le suivant :
- Convertir le fichier gopher.png intégré en un type chaîne contenant les options.
- Convertir en type *Img via la fonction NewPhoto.
- Passer par la fonction Image, puis convertir en chaîne RAW avant de créer le widget Label.
Les fichiers ICO sont traités de la même manière ; la seule différence avec le format SVG réside dans le traitement interne de la fonction Data.
Examinons maintenant la nature de la « chaîne contenant les options » :
1type rawOption string
La chaîne contenant les options mentionnée précédemment n'est qu'une simple chaîne formatée.
1func (w *Window) optionString(_ *Window) string {
2 return w.String()
3}
La méthode optionString est une méthode du pointeur Window qui renvoie une chaîne de caractères.
Enfin, examinons brièvement la structure interne de la fonction 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}
Comme le montre le code, les fichiers ICO et PNG nécessitent un processus d'encodage/décodage. Dans les autres cas, aucune conversion particulière n'est effectuée : seule l'option -data est ajoutée à la chaîne convertie en octets, afin d'indiquer qu'il s'agit du résultat de la fonction Data.
Charger une image avec un widget de menu
En ajoutant un widget de menu aux exemples de chargement de PNG et d'ICO vus précédemment, on peut créer une application qui affiche l'image souhaitée.
Commençons par un exemple simple de widget de 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}

Dans cet exemple, nous avons essayé la barre de menus et la création de menus, la mise en évidence de caractères, l'option Command, la liaison de raccourcis clavier, ainsi que la définition de la taille initiale de la fenêtre de l'application.
Modifions maintenant la fonction Command spécifiée pour GetOpenFile afin de créer un programme qui charge et affiche une image.
Le plan de réalisation du programme est le suivant :
- Limiter l'ouverture aux seuls fichiers PNG et ICO
- Traiter le fichier sélectionné dans la boîte de dialogue de fichier
- Implémenter un widget d'affichage de l'image
Voici le code qui met en œuvre ce 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}

Le code ci-dessus fonctionne de la manière suivante :
- Vérifier l'extension du fichier à l'aide des fonctions de comparaison de chaînes du package strings
- Ouvrir, lire, puis fermer le fichier à l'aide du package os
- L'image lue est affichée dans un widget Label
Conclusion
Dans cet article, nous avons traité, à l'aide de la bibliothèque Tcl/Tk pour Go, les points suivants :
- La structure de base d'une application GUI
- Le traitement de différents formats d'image tels que SVG, PNG et ICO
- Le placement des widgets (packing) et la gestion de la mise en page
- La structure de traitement des données d'image
- La liaison de raccourcis clavier et les commandes des widgets
Utiliser ensemble le langage Go et Tcl/Tk permet de créer des programmes GUI simples mais utiles. Nous vous encourageons à vous lancer ensuite dans le développement d'applications GUI plus complexes à partir de ces bases.