---
id: e55cbf5b5116abf3a5fc62c1af99e19b
author: Yunjin Lee
title: 使用 Go 与 Tcl/Tk 9.0 实现基于多文件列表框的图像查看器
description: 在 modernc.org/tk9.0 中添加 Tcl listbox 命令绑定和虚拟事件处理程序,以实现一个图形用户界面图像查看器,该查看器支持打开多个图像文件、切换选择以及删除列表项。
language: zh
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
```

首先，我们来看一下新增功能中的几个部分。

在查看新函数之前，让我们通过 tk.go 第 1017 行的 Destroy 函数，简单了解一下现有函数的结构。

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

该函数通过将操作以 Tcl 脚本格式传递给 evalErr 函数来实现。
这意味着，要实现所需功能，只需以相应的 Tcl 脚本格式传递命令即可。

作为示例，让我们实现一个向列表框添加项目的方法。
首先，为了编写 Tcl 脚本，我们来看一下官方手册中 listbox 可用命令中的 insert。

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

本示例为简化实现，在加载图像时会预先创建所有图像控件，
并且不检查重复文件。
您可以改进前面提到的问题，
也可以利用注释掉的 DefaultTheme 方法更改主题。
建议您尝试编写改进后的程序进行练习。

## 总结

本文介绍了 Go 语言 Tcl/Tk 库的命令调用机制，
并制作了一个添加了列表框的图像查看器。

1. Tcl/Tk 库的命令调用方式
2. 列表框的使用方法
3. 列表框控件属性的更改
4. 图像查看器的编写

希望您能以同样的方式挑战修改其他库，并利用所添加的功能完成一个完整的程序。
