---
id: e55cbf5b5116abf3a5fc62c1af99e19b
author: Yunjin Lee
title: Implementacja przeglądarki obrazów opartej na wielofailowym Listboxie w Go i Tcl/Tk 9.0
description: Dodajemy powiązania poleceń Tcl listbox oraz obsługę zdarzeń wirtualnych do modernc.org/tk9.0, aby zaimplementować graficzną przeglądarkę obrazów umożliwiającą otwieranie wielu plików graficznych, przełączanie wyboru oraz usuwanie elementów z listy.
language: pl
date: 2024-11-19T11:51:05.279238471Z
path: /blog/posts/creating-a-image-viewer-with-tk-in-go-z279238471Z
category: gui
---

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

1. 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.
2. Jeśli wyświetlacz obrazów ma prezentować wiele obrazów na liście, utwórzmy listę.
3. Dobrze jest wczytywać wiele obrazów jednocześnie.
4. Zaimplementujmy także usuwanie z listy obrazów, których nie chcemy oglądać.
5. 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.

```bash
git 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ę.

```go
func Destroy(options ...Opt) {
	evalErr(fmt.Sprintf("destroy %s", collect(options...)))
}
```

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.

[insert 명령어](https://www.tcl.tk/man/tcl8.6/TkCmd/listbox.htm#M29)

Z opisu polecenia insert wynika, że insert wstawia wymienione elementy pod określonym indeksem.
Aby to zaimplementować, można napisać kod taki jak:

```go
func (l *ListboxWidget) AddItem(index int, items string) {
	evalErr(fmt.Sprintf("%s insert %d %s", l.fpath, index, items))
}
```

Skoro znamy już ogólną zasadę implementacji, omówmy dodatkowe funkcje Listbox.

### Lista: wstawianie i usuwanie

```go
package main
import . "modernc.org/tk9.0"

func main() {
    length := 3
    l := Listbox()
    l.AddItem(0, "item1 item2 item3")
    b1 := TButton(Txt("Delete Multiple Items, index (0-1)"), Command( func(){
        if length >= 2 {
            l.DeleteItems(0,1)
            length-=2
        }
    }))
    b2 := TButton(Txt("Delete One Item, index (0)"), Command( func () {
        if length > 0 {
            l.DeleteOne(0)
            length-=1
        }
    }))
    Pack(TExit(),l,b1,b2)
    App.Wait()
}

```

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.

```go
package main

import . "modernc.org/tk9.0"

func main() {
    l := Listbox()
    l.SelectMode("multiple")
    l.AddItem(0, "item1 item2 item3")
    btn := TButton(Txt("Print Selected"), Command( func() {
        sel := l.Selected()
        for _, i := range sel {
            println(l.GetOne(i))
        }
    }))

    Pack(TExit(), l, btn)
    App.Wait()
}
```

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.

```go
package main

import . "modernc.org/tk9.0"

func main() {
    l := Listbox()
    l.Background("blue")
    l.Foreground("yellow")
    l.SelectBackground("black")
    l.SelectForeground("white")
    l.Height(20)
    l.Width(6)
    l.AddItem(0, "item1 item2 item3")
    l.ItemForeground(0,"red")
    l.ItemBackground(0,"green")
    l.ItemSelectBackground(0,"white")
    l.ItemSelectForeground(0,"black")
    l.Relief("ridged")
    Pack(TExit(),l)
    App.Wait()
}
```

![색상 적용 결과](/assets/images/go-tk-imageviewer/listbox_color.png)

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.

```go
package main

import (
    "fmt"
    "log"
    "os"
    "runtime"
    "strings"
    "path"

    . "modernc.org/tk9.0"
)

var pbuttons []*TButtonWidget
var extensions []FileType
var pbutton *TButtonWidget = nil
var listbox, listbox2 *ListboxWidget
var cur *LabelWidget = nil
var imagesLoaded []*LabelWidget
func PhotoName(fileName string) string {
        fileName = path.Base(fileName)
        return fileName[:len(fileName)-len(path.Ext(fileName))]
}

func handleFileOpen() {
    res := GetOpenFile(Multiple(true),Filetypes(extensions)) //다중 선택을 활성화하고 필터를 켭니다.
    s := make([]string,0,1000)
    for _, itm := range res {
        if itm != "" {
            tmp := strings.Split(itm," ")
            s = append(s,tmp...)
        }
    }

    for _, photo := range s {
        formatCheck := strings.Split(photo, ".")
        format := formatCheck[len(formatCheck)-1]

        if (strings.Compare(format, "png") == 0) || (strings.Compare(format, "ico") == 0) {
            picFile, err := os.Open(photo)
            if err != nil {
                log.Println("Error while opening photo, error is: ", err)
            }

            pictureRawData := make([]byte, 10000*10000)
            picFile.Read(pictureRawData)

            imageLabel := Label(Image(NewPhoto(Data(pictureRawData))))
                        imagesLoaded = append(imagesLoaded,imageLabel)
            var deleteTestButton *TButtonWidget
            deleteTestButton = TButton(
                Txt("Unshow Image"),
            Command(func() {
                GridForget(imageLabel.Window)
                GridForget(deleteTestButton.Window)
            }))

            pbuttons = append(pbuttons,deleteTestButton)

                        listbox.AddItem(len(imagesLoaded)-1,PhotoName(photo))
                        listbox2.AddItem(len(imagesLoaded)-1,PhotoName(photo))
            picFile.Close()
        }
    }
}

func DeleteSelected () {
    s:=listbox.Selected()
    if len(s) == 0 {
        return
        }
    for _, i := range s {
        listbox.DeleteOne(i)
        listbox2.DeleteOne(i)
        if len(imagesLoaded)-1>i {
            continue
        }
        if cur == imagesLoaded[i] {
            pbutton = nil
            cur = nil
        }
        Destroy(imagesLoaded[i])
        Destroy(pbuttons[i])
                imagesLoaded = append(imagesLoaded[:i],imagesLoaded[i+1:]...)
        pbuttons = append(pbuttons[:i], pbuttons[i+1:]...)
    }
}

func SelectImage() {
        s:=listbox2.Selected()
        if len(s) == 0 {
                return
        }

    if len(imagesLoaded) -1 < s[0] {
        return
    }
    if imagesLoaded[s[0]] == nil {
        return
    }
    if cur != nil {
            GridForget(cur.Window)
    }
    if pbutton != nil {
        GridForget(pbutton.Window)
    }

        Grid(imagesLoaded[s[0]], Row(0), Column(2))
    Grid(pbuttons[s[0]], Row(0), Column(3))
    cur = imagesLoaded[s[0]]
    pbutton = pbuttons[s[0]]
}

func SelectIndex(index int) {

    if len(imagesLoaded) -1 <index {
        return
    }
    if imagesLoaded[index] == nil {
        return
    }
    if cur != nil {
            GridForget(cur.Window)
    }
    if pbutton != nil {
        GridForget(pbutton.Window)
    }

        Grid(imagesLoaded[index], Row(0), Column(2))
    Grid(pbuttons[index], Row(0), Column(3))
    cur = imagesLoaded[index]
    pbutton = pbuttons[index]
}

func main() {
    menubar := Menu()
    //DefaultTheme("awdark","themes/awthemes-10.4.0")
    //테마를 사용하고 싶을 때에는 테마 명과 경로를 지정해 줍니다.
    fileMenu := menubar.Menu()
    extensions = make([]FileType,0,1)
    extensions = append(extensions, FileType{ "Supported Images", []string {".png",".ico"}, "" } )
    //필터에 png와 ico를 넣어 줍니다.
    fileMenu.AddCommand(Lbl("Open..."), Underline(0), Accelerator("Ctrl+O"), Command(func () {
        handleFileOpen()
        SelectIndex(len(imagesLoaded)-1)
    } ))
    Bind(App, "<Control-o>", Command(func() { fileMenu.Invoke(0) }))
    fileMenu.AddCommand(Lbl("Exit"), Underline(1), Accelerator("Ctrl+Q"), ExitHandler())
    Bind(App, "<Control-q>", Command(func() { fileMenu.Invoke(1) }))
    menubar.AddCascade(Lbl("File"), Underline(0), Mnu(fileMenu))
        imagesLoaded = make([]*LabelWidget, 0, 10000)
    pbuttons = make([]*TButtonWidget,0,10000)
    var scrollx, scroll, scroll2, scrollx2 *TScrollbarWidget
        listbox = Listbox(Yscrollcommand(func(e *Event) { e.ScrollSet(scroll)}) , Xscrollcommand( func(e *Event) { e.ScrollSet(scrollx)}))
        listbox2 = Listbox(Yscrollcommand(func(e *Event) { e.ScrollSet(scroll2)}), Xscrollcommand(func(e *Event) { e.ScrollSet(scrollx2)}))
        listbox.SelectMode("multiple")
        listbox2 = Listbox()
        listbox.Background("white")
        listbox.SelectBackground("blue")
        listbox.SelectForeground("yellow")
        listbox2.Background("grey")
        listbox2.SelectBackground("green")
    listbox2.SelectForeground("blue")
    listbox2.SelectBackground("brown")
        listbox.Height(5)
        listbox.Width(4)
        listbox2.Height(5)
        listbox2.Width(4)
        delBtn := Button(Txt("Delete"), Command(func () { DeleteSelected() }))
        selBtn := Button(Txt("Select"), Command(func () { SelectImage() }))
        scroll = TScrollbar(Command(func(e *Event) { e.Yview(listbox) }))
        scrollx = TScrollbar(Orient("horizontal"),Command(func(e *Event) { e.Xview(listbox) }))
    scroll2 = TScrollbar(Command(func(e *Event) { e.Yview(listbox2) }))
        scrollx2 = TScrollbar(Orient("horizontal"),Command(func(e *Event) { e.Xview(listbox2) }))
        Grid(listbox,Row(1),Column(0), Sticky("nes"))
        Grid(scroll,Row(1),Column(1), Sticky("nes"))
    Grid(scrollx,Row(2),Column(0),  Sticky("nes"))
        Grid(delBtn,Row(3),Column(0), Sticky("nes"))
        Grid(listbox2,Row(1),Column(2), Sticky("nes"))
        Grid(scroll2,Row(1),Column(3), Sticky("nes"))
    Grid(scrollx2,Row(2),Column(2), Sticky("nes"))
        Grid(selBtn,Row(3),Column(2), Sticky("nes"))
    App.WmTitle(fmt.Sprintf("%s on %s", App.WmTitle(""), runtime.GOOS))
    App.Configure(Mnu(menubar), Width("80c"), Height("60c")).Wait()
}

```

![이미지 뷰어 실행 결과](/assets/images/go-tk-imageviewer/imageviewer.png)

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.

1. Sposób wywoływania poleceń Tcl/Tk
2. Sposób korzystania z listboxa
3. Zmiana właściwości widgetu listbox
4. 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.
