---
id: e55cbf5b5116abf3a5fc62c1af99e19b
author: Yunjin Lee
title: Go- ja Tcl/Tk 9.0 -pohjainen monitiedostoinen listaruutuun perustuva kuvankatselin
description: Toteutamme graafisen kuvankatselimen lisäämällä modernc.org/tk9.0-kirjastoon Tcl-listbox-komentojen sidonnat ja virtuaalisten tapahtumien käsittelijät, jotka mahdollistavat useiden kuvatiedostojen avaamisen, valinnan vaihtamisen sekä luettelon kohteiden poistamisen.
language: fi
date: 2024-11-19T11:51:05.279238471Z
path: /blog/posts/creating-a-image-viewer-with-tk-in-go-z279238471Z
category: gui
---

Edellisessä kirjoituksessa tarkastelimme lyhyesti CGo-Free Tcl/Tk -kirjastoa.
Tällä kertaa sovellamme edellisen esimerkin pohjalta ja rakennamme kuvankatselimen.

## Kuvankatselimen suunnitelma

1. Edellisen kerran kuvan näyttäjässä ei ollut toimintoa kuvien poistamiseen, joten kun ladattiin useita kuvia, ikkuna ei riittänyt niille.
   Poistetaan käyttämättömät tarrat.
2. Jos kuvat listataan kuvan näyttäjässä, luodaan lista niille.
3. On parempi ladata useita kuvia kerralla.
4. Toteutetaan myös toiminto, jolla kuvia, joita ei haluta katsoa, voidaan poistaa listalta.
5. Luodaan toiminto, jolla voidaan valita ja tarkastella tiettyä kuvaa useiden kuvien joukosta.

## Muokattu Tcl/Tk 9.0 -kirjasto

Alkuperäisessä kirjastossa Listbox-toteutus on puutteellinen, joten kuvalistan näyttäminen on vaikeaa.
Ladataan muokattu kirjasto.
Jos Git CLI ei ole asennettuna, voit ladata sen myös tarball- tai zip-tiedostona.

```bash
git clone https://github.com/gg582/tk9.0
```

Tarkastellaan ensin joitakin lisättyjä toimintoja.

Ennen uusien funktioiden tarkastelua katsotaan, miten vanhat funktiot on toteutettu.
Tutkitaan rakennetta lyhyesti tk.go-tiedoston rivin 1017 Destroy-funktion avulla.

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

Tämä funktio on toteutettu siten, että toiminto välitetään evalErr-funktiolle Tcl-skriptin muodossa.
Toisin sanoen halutun toiminnon toteuttamiseksi riittää, että komento välitetään vastaavassa Tcl-skriptin muodossa.

Esimerkkinä toteutetaan menetelmä, jolla kohde lisätään listboxiin.
Tutkitaan ensin Tcl-skriptausta varten virallisen käsikirjan listbox-komennoista insert.

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

Insert-komennon kuvauksen mukaan insert lisää annetut kohteet määritettyyn indeksiin.
Tämän toteuttamiseksi voidaan kirjoittaa esimerkiksi seuraava koodi:

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

Kun perusperiaate on selvä, selitetään Listboxin lisätoiminnot.

### Lista: lisäys ja poisto

```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()
}

```

Edellisessä ohjelmassa AddItem lisää välilyönnillä erotetut eri kohteet indeksistä 0 alkaen järjestyksessä.
item1-item3 saavat indeksit 0, 1 ja 2.
Suoritetaan esimerkki ja katsotaan, miten kohteiden poisto toimii.

### Lista: valittujen kohteiden hakeminen

Haetaan seuraavaksi Listboxista valitut kohteet ja tarkistetaan ne.

```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()
}
```

Selected-menetelmä hakee indeksit kaikista Listboxissa tällä hetkellä valituista kohteista.
GetOne-menetelmä hakee kyseisen indeksin kohteen arvon.
Tulos näkyy konsolissa.
Samankaltainen Get-menetelmä hakee alku- ja loppuindeksin välillä olevien kohteiden arvot kaikki kerralla.

Seuraavaksi muutetaan listboxin väriä.

Tarkastellaan ensin seuraavaa esimerkkiä.

```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)

Kuten koodissa on määritelty, korkeus on kasvanut.
Lisäksi värit on sovellettu oikein.
Koska erityinen asetus koskee vain tiettyä kohdetta, vain ensimmäinen rivi on eri värinen.

Relief-menetelmällä voidaan muuttaa widgetin reunuksen tyyliä: flat, groove, raise, ridge, solid tai sunken.
Muutos ei ole kovin suuri.

## Kuvankatselimen esimerkki

Rakennetaan nyt kuvankatselin edellä opittujen widgetien avulla.
Esimerkkiohjelma on seuraava.

```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)

Tässä esimerkissä kaikki kuvawidgetit luodaan etukäteen, kun kuvat ladataan, jotta toteutus pysyy yksinkertaisena,
eikä päällekkäisiä tiedostoja tarkisteta.
Edellä mainittuja ongelmia voi parantaa,
ja kommentoitua DefaultTheme-menetelmää käyttämällä voi myös kokeilla teeman vaihtamista.
Harjoittele tekemällä ohjelmasta parannettu versio, jossa nämä asiat on huomioitu.

## Yhteenveto

Tässä kirjoituksessa tarkastelimme, miten Go-kielen Tcl/Tk-kirjaston komentokutsut toimivat,
ja rakensimme kuvankatselimen, johon lisättiin listbox.

1. Tcl/Tk-kirjaston komentokutsujen toteutustapa
2. Listboxin käyttö
3. Listbox-widgetin ominaisuuksien muuttaminen
4. Kuvankatselimen toteutus

Kokeile samaa lähestymistapaa muiden kirjastojen muokkaamiseen ja kirjoita lisättyjen toimintojen avulla valmis ohjelma.
