Implémentation d'une visionneuse d'images à liste multi-fichiers basée sur Go et Tcl/Tk 9.0

Dans le précédent article, nous avons brièvement examiné la bibliothèque CGo-Free Tcl/Tk. Dans cette séance, nous allons adapter l'exemple de la fois précédente pour créer une visionneuse d'images.
Plan de la visionneuse d'images
- L'afficheur d'images de la dernière fois ne disposait d'aucune fonction de suppression d'images ; plus on chargeait d'images, plus la fenêtre manquait de place. Supprimons les étiquettes inutilisées.
- Si l'afficheur d'images doit présenter plusieurs images sous forme de liste, créons une liste.
- Il est préférable de charger plusieurs images en une seule fois.
- Implémentons également la possibilité de retirer de la liste les images que l'on ne souhaite pas consulter.
- Créons une fonction permettant de sélectionner et d'afficher une image particulière parmi plusieurs images.
Bibliothèque Tcl/Tk 9.0 modifiée
La bibliothèque existante ne propose qu'une implémentation insuffisante du Listbox, ce qui rend difficile l'affichage d'une liste d'images. Téléchargeons la bibliothèque modifiée. Si Git CLI n'est pas installé, vous pouvez également télécharger l'archive tarball ou zip.
1git clone https://github.com/gg582/tk9.0
Examinons d'abord quelques-unes des fonctions ajoutées.
Avant d'étudier les nouvelles fonctions, voyons la structure des fonctions existantes en analysant brièvement la fonction Destroy située à la ligne 1017 de tk.go.
1func Destroy(options ...Opt) {
2 evalErr(fmt.Sprintf("destroy %s", collect(options...)))
3}
Cette fonction est implémentée en transmettant une action, sous forme de script Tcl, à la fonction evalErr. Autrement dit, pour réaliser la fonction souhaitée, il suffit de transmettre la commande au format du script Tcl correspondant.
À titre d'exemple, implémentons une méthode qui ajoute un élément à une listbox. Tout d'abord, pour le scripting Tcl, consultons la commande insert parmi les commandes utilisables sur un listbox dans le manuel officiel.
D'après la page de description de la commande insert, insert insère les éléments énumérés à un index spécifié. Pour l'implémenter, on peut donc écrire un code tel que :
1func (l *ListboxWidget) AddItem(index int, items string) {
2 evalErr(fmt.Sprintf("%s insert %d %s", l.fpath, index, items))
3}
Maintenant que nous avons compris le principe général de l'implémentation, présentons les fonctions supplémentaires du Listbox.
Liste : insertion et suppression
1package main
2import . "modernc.org/tk9.0"
3
4func main() {
5 length := 3
6 l := Listbox()
7 l.AddItem(0, "item1 item2 item3")
8 b1 := TButton(Txt("Delete Multiple Items, index (0-1)"), Command( func(){
9 if length >= 2 {
10 l.DeleteItems(0,1)
11 length-=2
12 }
13 }))
14 b2 := TButton(Txt("Delete One Item, index (0)"), Command( func () {
15 if length > 0 {
16 l.DeleteOne(0)
17 length-=1
18 }
19 }))
20 Pack(TExit(),l,b1,b2)
21 App.Wait()
22}
23
Dans le programme ci-dessus, AddItem insère, à partir de l'index 0, les différents éléments séparés par des espaces. Les éléments item1 à item3 reçoivent respectivement les index 0, 1 et 2. Exécutons l'exemple pour observer le fonctionnement de la suppression d'éléments.
Liste : obtention des éléments sélectionnés
Récupérons maintenant les éléments sélectionnés dans le Listbox et vérifions-les.
1package main
2
3import . "modernc.org/tk9.0"
4
5func main() {
6 l := Listbox()
7 l.SelectMode("multiple")
8 l.AddItem(0, "item1 item2 item3")
9 btn := TButton(Txt("Print Selected"), Command( func() {
10 sel := l.Selected()
11 for _, i := range sel {
12 println(l.GetOne(i))
13 }
14 }))
15
16 Pack(TExit(), l, btn)
17 App.Wait()
18}
La méthode Selected récupère les index de tous les éléments actuellement sélectionnés dans le Listbox. La méthode GetOne récupère la valeur de l'élément correspondant à l'index donné. Le résultat affiché dans la console permet de le vérifier. À noter que la méthode analogue Get reçoit un index de début et un index de fin, et récupère les valeurs de tous les éléments de la plage.
Modifions maintenant les couleurs du listbox.
Examinons d'abord l'exemple suivant.
1package main
2
3import . "modernc.org/tk9.0"
4
5func main() {
6 l := Listbox()
7 l.Background("blue")
8 l.Foreground("yellow")
9 l.SelectBackground("black")
10 l.SelectForeground("white")
11 l.Height(20)
12 l.Width(6)
13 l.AddItem(0, "item1 item2 item3")
14 l.ItemForeground(0,"red")
15 l.ItemBackground(0,"green")
16 l.ItemSelectBackground(0,"white")
17 l.ItemSelectForeground(0,"black")
18 l.Relief("ridged")
19 Pack(TExit(),l)
20 App.Wait()
21}

Comme indiqué dans le code ci-dessus, la hauteur a augmenté. On constate également que les couleurs ont bien été appliquées. Ici, une option permettant de définir une couleur différente pour un élément particulier a été spécifiée, c'est pourquoi seule la première ligne présente une couleur différente.
Par ailleurs, bien que la différence ne soit pas importante, la méthode Relief permet de modifier le style de la bordure du widget parmi flat, groove, raise, ridge, solid et sunken.
Exemple de visionneuse d'images
Créons maintenant une visionneuse d'images à l'aide des widgets que nous venons d'étudier. Voici le programme d'exemple :
1package main
2
3import (
4 "fmt"
5 "log"
6 "os"
7 "runtime"
8 "strings"
9 "path"
10
11 . "modernc.org/tk9.0"
12)
13
14var pbuttons []*TButtonWidget
15var extensions []FileType
16var pbutton *TButtonWidget = nil
17var listbox, listbox2 *ListboxWidget
18var cur *LabelWidget = nil
19var imagesLoaded []*LabelWidget
20func PhotoName(fileName string) string {
21 fileName = path.Base(fileName)
22 return fileName[:len(fileName)-len(path.Ext(fileName))]
23}
24
25func handleFileOpen() {
26 res := GetOpenFile(Multiple(true),Filetypes(extensions)) //다중 선택을 활성화하고 필터를 켭니다.
27 s := make([]string,0,1000)
28 for _, itm := range res {
29 if itm != "" {
30 tmp := strings.Split(itm," ")
31 s = append(s,tmp...)
32 }
33 }
34
35 for _, photo := range s {
36 formatCheck := strings.Split(photo, ".")
37 format := formatCheck[len(formatCheck)-1]
38
39 if (strings.Compare(format, "png") == 0) || (strings.Compare(format, "ico") == 0) {
40 picFile, err := os.Open(photo)
41 if err != nil {
42 log.Println("Error while opening photo, error is: ", err)
43 }
44
45 pictureRawData := make([]byte, 10000*10000)
46 picFile.Read(pictureRawData)
47
48 imageLabel := Label(Image(NewPhoto(Data(pictureRawData))))
49 imagesLoaded = append(imagesLoaded,imageLabel)
50 var deleteTestButton *TButtonWidget
51 deleteTestButton = TButton(
52 Txt("Unshow Image"),
53 Command(func() {
54 GridForget(imageLabel.Window)
55 GridForget(deleteTestButton.Window)
56 }))
57
58 pbuttons = append(pbuttons,deleteTestButton)
59
60 listbox.AddItem(len(imagesLoaded)-1,PhotoName(photo))
61 listbox2.AddItem(len(imagesLoaded)-1,PhotoName(photo))
62 picFile.Close()
63 }
64 }
65}
66
67func DeleteSelected () {
68 s:=listbox.Selected()
69 if len(s) == 0 {
70 return
71 }
72 for _, i := range s {
73 listbox.DeleteOne(i)
74 listbox2.DeleteOne(i)
75 if len(imagesLoaded)-1>i {
76 continue
77 }
78 if cur == imagesLoaded[i] {
79 pbutton = nil
80 cur = nil
81 }
82 Destroy(imagesLoaded[i])
83 Destroy(pbuttons[i])
84 imagesLoaded = append(imagesLoaded[:i],imagesLoaded[i+1:]...)
85 pbuttons = append(pbuttons[:i], pbuttons[i+1:]...)
86 }
87}
88
89func SelectImage() {
90 s:=listbox2.Selected()
91 if len(s) == 0 {
92 return
93 }
94
95 if len(imagesLoaded) -1 < s[0] {
96 return
97 }
98 if imagesLoaded[s[0]] == nil {
99 return
100 }
101 if cur != nil {
102 GridForget(cur.Window)
103 }
104 if pbutton != nil {
105 GridForget(pbutton.Window)
106 }
107
108 Grid(imagesLoaded[s[0]], Row(0), Column(2))
109 Grid(pbuttons[s[0]], Row(0), Column(3))
110 cur = imagesLoaded[s[0]]
111 pbutton = pbuttons[s[0]]
112}
113
114func SelectIndex(index int) {
115
116 if len(imagesLoaded) -1 <index {
117 return
118 }
119 if imagesLoaded[index] == nil {
120 return
121 }
122 if cur != nil {
123 GridForget(cur.Window)
124 }
125 if pbutton != nil {
126 GridForget(pbutton.Window)
127 }
128
129 Grid(imagesLoaded[index], Row(0), Column(2))
130 Grid(pbuttons[index], Row(0), Column(3))
131 cur = imagesLoaded[index]
132 pbutton = pbuttons[index]
133}
134
135func main() {
136 menubar := Menu()
137 //DefaultTheme("awdark","themes/awthemes-10.4.0")
138 //테마를 사용하고 싶을 때에는 테마 명과 경로를 지정해 줍니다.
139 fileMenu := menubar.Menu()
140 extensions = make([]FileType,0,1)
141 extensions = append(extensions, FileType{ "Supported Images", []string {".png",".ico"}, "" } )
142 //필터에 png와 ico를 넣어 줍니다.
143 fileMenu.AddCommand(Lbl("Open..."), Underline(0), Accelerator("Ctrl+O"), Command(func () {
144 handleFileOpen()
145 SelectIndex(len(imagesLoaded)-1)
146 } ))
147 Bind(App, "<Control-o>", Command(func() { fileMenu.Invoke(0) }))
148 fileMenu.AddCommand(Lbl("Exit"), Underline(1), Accelerator("Ctrl+Q"), ExitHandler())
149 Bind(App, "<Control-q>", Command(func() { fileMenu.Invoke(1) }))
150 menubar.AddCascade(Lbl("File"), Underline(0), Mnu(fileMenu))
151 imagesLoaded = make([]*LabelWidget, 0, 10000)
152 pbuttons = make([]*TButtonWidget,0,10000)
153 var scrollx, scroll, scroll2, scrollx2 *TScrollbarWidget
154 listbox = Listbox(Yscrollcommand(func(e *Event) { e.ScrollSet(scroll)}) , Xscrollcommand( func(e *Event) { e.ScrollSet(scrollx)}))
155 listbox2 = Listbox(Yscrollcommand(func(e *Event) { e.ScrollSet(scroll2)}), Xscrollcommand(func(e *Event) { e.ScrollSet(scrollx2)}))
156 listbox.SelectMode("multiple")
157 listbox2 = Listbox()
158 listbox.Background("white")
159 listbox.SelectBackground("blue")
160 listbox.SelectForeground("yellow")
161 listbox2.Background("grey")
162 listbox2.SelectBackground("green")
163 listbox2.SelectForeground("blue")
164 listbox2.SelectBackground("brown")
165 listbox.Height(5)
166 listbox.Width(4)
167 listbox2.Height(5)
168 listbox2.Width(4)
169 delBtn := Button(Txt("Delete"), Command(func () { DeleteSelected() }))
170 selBtn := Button(Txt("Select"), Command(func () { SelectImage() }))
171 scroll = TScrollbar(Command(func(e *Event) { e.Yview(listbox) }))
172 scrollx = TScrollbar(Orient("horizontal"),Command(func(e *Event) { e.Xview(listbox) }))
173 scroll2 = TScrollbar(Command(func(e *Event) { e.Yview(listbox2) }))
174 scrollx2 = TScrollbar(Orient("horizontal"),Command(func(e *Event) { e.Xview(listbox2) }))
175 Grid(listbox,Row(1),Column(0), Sticky("nes"))
176 Grid(scroll,Row(1),Column(1), Sticky("nes"))
177 Grid(scrollx,Row(2),Column(0), Sticky("nes"))
178 Grid(delBtn,Row(3),Column(0), Sticky("nes"))
179 Grid(listbox2,Row(1),Column(2), Sticky("nes"))
180 Grid(scroll2,Row(1),Column(3), Sticky("nes"))
181 Grid(scrollx2,Row(2),Column(2), Sticky("nes"))
182 Grid(selBtn,Row(3),Column(2), Sticky("nes"))
183 App.WmTitle(fmt.Sprintf("%s on %s", App.WmTitle(""), runtime.GOOS))
184 App.Configure(Mnu(menubar), Width("80c"), Height("60c")).Wait()
185}
186

Dans cet exemple, afin de simplifier l'implémentation, tous les widgets d'image sont créés à l'avance lors du chargement, et les fichiers en double ne sont pas vérifiés. Vous pouvez améliorer les problèmes évoqués précédemment, et essayer de modifier le thème en utilisant la méthode DefaultTheme, dont la partie est commentée. Nous vous encourageons à vous exercer en créant un programme amélioré intégrant ces points.
Synthèse
Dans cet article, nous avons vu comment fonctionne l'appel de commandes de la bibliothèque Tcl/Tk en Go, et nous avons créé une visionneuse d'images dotée d'un listbox.
- Méthode d'appel de commandes de la bibliothèque Tcl/Tk
- Utilisation du listbox
- Modification des propriétés du widget listbox
- Création d'une visionneuse d'images
Nous vous invitons à appliquer la même démarche pour modifier d'autres bibliothèques, et à réaliser un programme complet à partir des fonctions que vous aurez ajoutées.