---
id: e55cbf5b5116abf3a5fc62c1af99e19b
author: Yunjin Lee
title: Implementación de un visor de imágenes basado en un cuadro de lista para múltiples archivos con Go y Tcl/Tk 9.0
description: Se implementa un visor de imágenes con interfaz gráfica que permite abrir múltiples archivos de imagen, alternar selecciones y eliminar elementos de la lista, para lo cual se añaden enlaces de comandos Tcl listbox y controladores de eventos virtuales en modernc.org/tk9.0.
language: es
date: 2024-11-19T11:51:05.279238471Z
path: /blog/posts/creating-a-image-viewer-with-tk-in-go-z279238471Z
category: gui
---

En la publicación anterior, examinamos brevemente la biblioteca CGo-Free Tcl/Tk.
En esta ocasión, aplicaremos el ejemplo de la vez pasada para crear un visor de imágenes.

## Plan del visor de imágenes

1. El indicador de imágenes de la vez anterior carecía de una función para eliminar imágenes, por lo que, a medida que se cargaban varias imágenes, el tamaño de la ventana resultaba insuficiente.
   Eliminemos las etiquetas que no se utilizan.
2. Si vamos a enumerar varias imágenes en el indicador de imágenes, creemos una lista.
3. Conviene cargar varias imágenes a la vez.
4. También implementemos la función de quitar de la lista las imágenes que no se van a ver.
5. Creemos una función que permita seleccionar y ver una imagen específica entre varias imágenes.

## Biblioteca Tcl/Tk 9.0 modificada

La biblioteca existente tiene una implementación insuficiente de Listbox, por lo que resulta difícil mostrar la lista de imágenes.
Descarguemos la biblioteca modificada.
Si Git CLI no está instalado, también puede descargar el archivo tarball o zip.

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

Primero, veamos algunas de las funciones añadidas.

Antes de examinar las nuevas funciones, comprendamos brevemente la estructura de las funciones existentes a través de la función Destroy, en la línea 1017 de tk.go.

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

Esta función se implementa pasando la acción a la función evalErr en forma de script de Tcl.
Esto significa que, para implementar la funcionalidad deseada, basta con pasar el comando en el formato del script de Tcl correspondiente.

A modo de ejemplo, implementemos un método que agregue elementos a un listbox.
Primero, para el scripting de Tcl, veamos insert, uno de los comandos disponibles para listbox en el manual oficial.

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

Si consultamos la página de descripción del comando insert, insert inserta los elementos enumerados en un índice específico.
Entonces, para implementarlo, podemos escribir código como el siguiente:

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

Ahora que conocemos el principio general de implementación, explicaremos las funciones adicionales para Listbox.

### Lista: inserción/eliminación

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

```

En el programa anterior, AddItem inserta, desde el índice 0 y en orden, los distintos elementos separados por espacios.
item1-item3 ocupan, en orden, los índices 0, 1 y 2.
Ejecutemos el ejemplo para ver cómo funciona la eliminación de elementos.

### Lista: obtener los elementos seleccionados

Ahora obtendremos los elementos seleccionados en Listbox y los verificaremos.

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

El método Selected obtiene los índices de todos los elementos seleccionados en el Listbox actual.
El método GetOne obtiene el valor del elemento correspondiente a ese índice.
Podemos verificarlo mediante el resultado que se imprime en la consola.
Como referencia, el método similar Get recibe un índice inicial y uno final y obtiene los valores de todos los elementos dentro de ese rango.

A continuación, cambiemos los colores del listbox.

Primero, veamos el siguiente ejemplo.

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

Tal como se escribió en el código anterior, la altura ha aumentado.
Además, se puede ver que los colores se han aplicado correctamente.
Aquí se especificó una opción que asigna colores distintos solo a determinados elementos,
por lo que se puede observar que únicamente la primera línea tiene un color diferente.

Asimismo, aunque la diferencia no es grande, con el método Relief se puede cambiar el estilo del borde del widget entre flat, groove, raise, ridge, solid y sunken.

## Ejemplo del visor de imágenes

Ahora, utilizando los widgets que acabamos de aprender, creemos un visor de imágenes.
El programa de ejemplo es el siguiente.

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

En este ejemplo, para simplificar la implementación, todos los widgets de imagen se crean de antemano al cargarlas,
y no se verifica si hay archivos duplicados.
Se pueden mejorar los problemas mencionados anteriormente,
y también es posible cambiar el tema utilizando el método DefaultTheme, que está en la parte comentada.
Le recomendamos practicar creando un programa nuevo que incorpore estas mejoras.

## Conclusión

En esta publicación, examinamos cómo funciona la invocación de comandos de la biblioteca Tcl/Tk en Go, y creamos un visor de imágenes con un listbox añadido.

1. Forma de invocación de comandos de la biblioteca Tcl/Tk
2. Uso del listbox
3. Modificación de las propiedades del widget listbox
4. Creación de un visor de imágenes

Le recomendamos que, de manera similar, intente modificar otras bibliotecas y que escriba un programa completo con las funciones añadidas.
