Implementacja przeglądarki obrazów opartej na wielofailowym Listboxie w Go i Tcl/Tk 9.0

W poprzednim wpisie pokrótce przyjrzeliśmy się bibliotece CGo-Free Tcl/Tk. W tym odcinku rozwiniemy poprzedni przykład i stworzymy przeglądarkę obrazów.
Plan przeglądarki obrazów
- Wyświetlacz obrazów z poprzedniej lekcji nie miał funkcji usuwania obrazów, więc im więcej obrazów wczytywaliśmy, tym bardziej brakowało miejsca w oknie. Usuńmy nieużywane etykiety.
- Jeśli wyświetlacz obrazów ma prezentować wiele obrazów na liście, utwórzmy listę.
- Dobrze jest wczytywać wiele obrazów jednocześnie.
- Zaimplementujmy także usuwanie z listy obrazów, których nie chcemy oglądać.
- Stwórzmy funkcję wyboru i wyświetlania konkretnego obrazu spośród wielu.
Zmodyfikowana biblioteka Tcl/Tk 9.0
Istniejąca biblioteka ma niedostateczną implementację Listbox, przez co trudno wyświetlić listę obrazów. Pobierzmy zmodyfikowaną bibliotekę. Jeśli Git CLI nie jest zainstalowany, można pobrać archiwum tarball lub zip.
1git clone https://github.com/gg582/tk9.0
Najpierw przyjrzyjmy się kilku dodanym funkcjom.
Zanim przyjrzymy się nowym funkcjom, sprawdźmy na przykładzie funkcji Destroy w pliku tk.go, w wierszu 1017, jak zbudowane są funkcje istniejące, aby w prosty sposób poznać ich strukturę.
1func Destroy(options ...Opt) {
2 evalErr(fmt.Sprintf("destroy %s", collect(options...)))
3}
Ta funkcja jest zaimplementowana przez przekazanie działania do funkcji evalErr w formie skryptu Tcl. Oznacza to, że aby zaimplementować żądaną funkcjonalność, wystarczy przekazać polecenie w formie odpowiedniego skryptu Tcl.
Jako przykład zaimplementujmy metodę dodającą element do listboxa. Najpierw, na potrzeby skryptowania w Tcl, zapoznajmy się w oficjalnym podręczniku z poleceniem insert, dostępnym dla listbox.
Z opisu polecenia insert wynika, że insert wstawia wymienione elementy pod określonym indeksem. Aby to zaimplementować, można napisać kod taki jak:
1func (l *ListboxWidget) AddItem(index int, items string) {
2 evalErr(fmt.Sprintf("%s insert %d %s", l.fpath, index, items))
3}
Skoro znamy już ogólną zasadę implementacji, omówmy dodatkowe funkcje Listbox.
Lista: wstawianie i usuwanie
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
W powyższym programie AddItem wstawia kolejno elementy oddzielone spacjami, zaczynając od indeksu 0. Elementy item1-item3 otrzymują kolejno indeksy 0, 1 i 2. Sprawdźmy, jak działa usuwanie elementów, uruchamiając przykład.
Lista: pobieranie wybranych elementów
Teraz pobierzmy elementy wybrane w Listbox i je sprawdźmy.
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}
Metoda Selected pobiera indeksy wszystkich elementów wybranych w bieżącym Listbox. Metoda GetOne pobiera wartość elementu o danym indeksie. Wynik widzimy na konsoli. Warto wiedzieć, że podobna metoda Get przyjmuje indeks początkowy i końcowy oraz pobiera wartości wszystkich elementów z zakresu.
Następnie zmieńmy kolory listboxa.
Najpierw przyjrzyjmy się poniższemu przykładowi.
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}

Jak zapisano w kodzie powyżej, wysokość listboxa się zwiększyła. Widać też, że kolory zostały poprawnie zastosowane. Ponieważ opcja zmiany koloru została ustawiona dla konkretnego elementu, tylko pierwszy wiersz ma inne kolory.
Ponadto, choć różnica nie jest duża, metoda Relief pozwala zmienić styl obramowania widgetu spośród: flat, groove, raise, ridge, solid oraz sunken.
Przykład przeglądarki obrazów
Teraz, wykorzystując poznane widgety, stwórzmy przeglądarkę obrazów. Przykładowy program wygląda następująco.
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

W tym przykładzie, aby uprościć implementację, wszystkie widgety obrazów są tworzone z wyprzedzeniem, w momencie wczytywania, a duplikaty plików nie są sprawdzane. Można poprawić wspomniane wcześniej problemy, a także wypróbować zmianę motywu, korzystając z metody DefaultTheme, która znajduje się w zakomentowanym fragmencie. Zachęcam do ćwiczenia poprzez stworzenie nowego programu, w którym wprowadzisz te udoskonalenia.
Podsumowanie
W tym artykule przyjrzeliśmy się temu, jak działa wywoływanie poleceń w bibliotece Tcl/Tk dla języka Go, oraz stworzyliśmy przeglądarkę obrazów z listboxem.
- Sposób wywoływania poleceń Tcl/Tk
- Sposób korzystania z listboxa
- Zmiana właściwości widgetu listbox
- Tworzenie przeglądarki obrazów
Zachęcam do podjęcia podobnych prób z modyfikacją innych bibliotek oraz do napisania kompletnego programu z dodanymi funkcjami.