---
id: e55cbf5b5116abf3a5fc62c1af99e19b
author: Yunjin Lee
title: Реализация просмотрщика изображений на основе многофайлового списка (listbox) с использованием Go и Tcl/Tk 9.0
description: Для реализации графического просмотрщика изображений, поддерживающего открытие нескольких файлов изображений, переключение выбора и удаление элементов списка, в modernc.org/tk9.0 добавляются привязки команд Tcl listbox и обработчики виртуальных событий.
language: ru
date: 2024-11-19T11:51:05.279238471Z
path: /blog/posts/creating-a-image-viewer-with-tk-in-go-z279238471Z
category: gui
---

В прошлой статье мы вкратце рассмотрели библиотеку CGo-Free Tcl/Tk.
В этот раз мы возьмём пример из прошлой статьи и на его основе создадим просмотрщик изображений.

## План работы над просмотрщиком изображений

1. В предыдущем средстве отображения изображений не было функции удаления изображений, поэтому по мере загрузки нескольких изображений
   размер окна становился недостаточным.
   Давайте удалим неиспользуемые метки.
2. Если изображения будут перечисляться в средстве отображения, создадим для них список.
3. Желательно загружать несколько изображений за один раз.
4. Реализуем также возможность удалять из списка изображения, которые не нужно просматривать.
5. Создадим функцию выбора и просмотра отдельного изображения из нескольких.

## Измененная библиотека Tcl/Tk 9.0

В существующей библиотеке реализация Listbox недостаточна, поэтому трудно отображать список изображений.
Давайте загрузим измененную библиотеку.
Если Git CLI не установлен, можно загрузить архив tarball или zip.

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

Сначала рассмотрим некоторые из добавленных функций.

Прежде чем изучать новые функции, давайте кратко разберём структуру существующих функций на примере функции Destroy из строки 1017 файла tk.go.

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

Эта функция реализована путём передачи действия в функцию evalErr в виде скрипта Tcl.
Другими словами, чтобы реализовать нужную функцию, достаточно передать команду в формате соответствующего скрипта Tcl.

В качестве примера реализуем метод добавления элемента в список. Сначала, для написания скриптов Tcl, посмотрим в официальном руководстве команду insert из доступных для listbox команд.

[команда insert](https://www.tcl.tk/man/tcl8.6/TkCmd/listbox.htm#M29)

Из описания команды insert видно, что insert вставляет перечисленные элементы в указанную позицию индекса.
Тогда для её реализации можно написать код следующего вида:

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

Теперь, когда мы примерно понимаем принцип реализации, опишем дополнительные функции для Listbox.

### Список: вставка и удаление

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

```

В приведённой выше программе AddItem последовательно помещает элементы, разделённые пробелами, начиная с индекса 0.
item1-item3 получают индексы 0, 1 и 2 соответственно.
Чтобы понять, как работает удаление элементов, запустим пример и посмотрим.

### Список: получение выбранных элементов

Теперь получим выбранные в Listbox элементы и проверим результат.

```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 получает индексы всех элементов, выбранных в текущем Listbox.
Метод GetOne получает значение элемента по соответствующему индексу.
Результат виден в выводе консоли.
Для справки: похожий метод Get принимает начальный и конечный индексы и возвращает значения всех элементов в этом диапазоне.

Далее изменим цвета списка.

Сначала рассмотрим следующий пример.

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

Как и было задано в коде, высота увеличилась.
Кроме того, видно, что цвета применились корректно.
Здесь заданы параметры, меняющие цвет только для отдельного элемента, поэтому видно, что отличается только первая строка.

Также, хотя разница небольшая, с помощью метода Relief можно изменить стиль рамки виджета: flat, groove, raise, ridge, solid или sunken.

## Пример просмотрщика изображений

Теперь создадим просмотрщик изображений с использованием изученных виджетов.
Пример программы выглядит так:

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

В этом примере, чтобы упростить реализацию, все виджеts изображений создаются заранее при загрузке, и проверка на дубликаты файлов не выполняется.
Вы можете устранить упомянутые выше недостатки, а также попробовать изменить тему с помощью метода DefaultTheme, который сейчас закомментирован.
Рекомендуем попрактиковаться, написав новую программу с такими улучшениями.

## Заключение

В этой статье мы разобрали, как работают вызовы команд в библиотеке Tcl/Tk для Go, и создали просмотрщик изображений с добавленным списком Listbox.

1. Способ вызова команд Tcl/Tk
2. Использование Listbox
3. Изменение свойств виджета Listbox
4. Создание просмотрщика изображений

Попробуйте таким же образом поработать над модификацией других библиотек и создайте законченную программу с добавленными функциями.
