---
id: e55cbf5b5116abf3a5fc62c1af99e19b
author: Yunjin Lee
title: Go と Tcl/Tk 9.0 による複数ファイル対応リストボックスベースの画像ビューアの実装
description: modernc.org/tk9.0に Tcl listbox のコマンドバインディングと仮想イベントハンドラを追加し、複数の画像ファイルの読み込み、選択の切り替え、リスト項目の削除が可能な GUI 画像ビューアを実装します。
language: ja
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...)))
}
```

この関数は、evalErr という関数に Tcl スクリプトの形式で動作を渡すことで実装されています。
つまり、目的の機能を実装するには、該当する 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. 画像ビューアの作成

このような方法で他のライブラリの修正にも挑戦し、追加した機能を用いて完成したプログラムを作成してみてください。
