GoSuda

Implementing a Multi-File Listbox-Based Image Viewer with Go and Tcl/Tk 9.0

By Yunjin Lee views ...

In the previous post, we briefly looked at the CGo-Free Tcl/Tk library. This time, we will build on the example from last time to create an image viewer.

Image Viewer Plan

  1. The image display from last time had no image deletion feature, so the window ran out of space as more images were loaded. Let's delete labels that are no longer used.
  2. If we are going to list multiple images in the image display, let's create a list.
  3. It would be good to load multiple images at once.
  4. Let's also implement removing images from the list that we no longer want to view.
  5. Let's create a feature that selects and displays a specific image from multiple images.

Modified Tcl/Tk 9.0 Library

The existing library's Listbox implementation is insufficient, making it difficult to display the image list. Let's download the modified library. If the Git CLI is not installed, you may download it as a tarball or zip file.

1git clone https://github.com/gg582/tk9.0

First, let's look at some of the added features.

Before looking at the new functions, let's briefly examine the structure of the existing functions through the Destroy function at line 1017 of tk.go.

1func Destroy(options ...Opt) {
2	evalErr(fmt.Sprintf("destroy %s", collect(options...)))
3}

This function is implemented by passing an action to the evalErr function in the form of a Tcl script. That means that to implement the desired functionality, you only need to pass the command in the corresponding Tcl script format.

As an example, let's implement a method that adds an item to a listbox. First, for Tcl scripting, let's look at insert, one of the commands available for listbox in the official manual.

insert command

Looking at the description page for the insert command, insert inserts the listed items at a specific index. Then, to implement this, we can write code like the following:

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

Now that we understand the general implementation principle, let's explain the additional features for Listbox.

List: Insertion/Deletion

 1package main
 2import . "modernc.org/tk9.0"
 3
 4func main() {
 5    length := 3
 6    l := Listbox()
 7    l.AddItem(0, "item1 item2 item3")
 8    b1 := TButton(Txt("Delete Multiple Items, index (0-1)"), Command( func(){
 9        if length >= 2 {
10            l.DeleteItems(0,1)
11            length-=2
12        }
13    }))
14    b2 := TButton(Txt("Delete One Item, index (0)"), Command( func () {
15        if length > 0 {
16            l.DeleteOne(0)
17            length-=1
18        }
19    }))
20    Pack(TExit(),l,b1,b2)
21    App.Wait()
22}
23

In the program above, AddItem inserts the distinct items separated by spaces, starting from index 0 in order. item1 through item3 will have indices 0, 1, and 2, respectively. Let's run the example to see how item deletion works.

List: Getting Selected Items

Now let's retrieve the items selected in the Listbox and check them.

 1package main
 2
 3import . "modernc.org/tk9.0"
 4
 5func main() {
 6    l := Listbox()
 7    l.SelectMode("multiple")
 8    l.AddItem(0, "item1 item2 item3")
 9    btn := TButton(Txt("Print Selected"), Command( func() {
10        sel := l.Selected()
11        for _, i := range sel {
12            println(l.GetOne(i))
13        }
14    }))
15
16    Pack(TExit(), l, btn)
17    App.Wait()
18}

The Selected method retrieves the indices of all items currently selected in the Listbox. The GetOne method retrieves the value of the item at the corresponding index. This can be confirmed by the result printed to the console. For reference, the similar Get method takes a start and end index and retrieves the values of all items within the range.

Next, let's change the colors of the listbox.

First, let's look at the example below.

 1package main
 2
 3import . "modernc.org/tk9.0"
 4
 5func main() {
 6    l := Listbox()
 7    l.Background("blue")
 8    l.Foreground("yellow")
 9    l.SelectBackground("black")
10    l.SelectForeground("white")
11    l.Height(20)
12    l.Width(6)
13    l.AddItem(0, "item1 item2 item3")
14    l.ItemForeground(0,"red")
15    l.ItemBackground(0,"green")
16    l.ItemSelectBackground(0,"white")
17    l.ItemSelectForeground(0,"black")
18    l.Relief("ridged")
19    Pack(TExit(),l)
20    App.Wait()
21}

Color application result

As written in the code above, the height has increased. You can also see that the colors have been applied correctly. Here, options that change colors only for specific items are set, so you can see that only the first line has a different color applied.

Also, although the difference is not large, using the Relief method allows you to change the border style of the widget to one of flat, groove, raise, ridge, solid, or sunken.

Image Viewer Example

Now, using the widgets we have just learned, let's create an image viewer. The example program is as follows:

  1package main
  2
  3import (
  4    "fmt"
  5    "log"
  6    "os"
  7    "runtime"
  8    "strings"
  9    "path"
 10
 11    . "modernc.org/tk9.0"
 12)
 13
 14var pbuttons []*TButtonWidget
 15var extensions []FileType
 16var pbutton *TButtonWidget = nil
 17var listbox, listbox2 *ListboxWidget
 18var cur *LabelWidget = nil
 19var imagesLoaded []*LabelWidget
 20func PhotoName(fileName string) string {
 21        fileName = path.Base(fileName)
 22        return fileName[:len(fileName)-len(path.Ext(fileName))]
 23}
 24
 25func handleFileOpen() {
 26    res := GetOpenFile(Multiple(true),Filetypes(extensions)) //๋‹ค์ค‘ ์„ ํƒ์„ ํ™œ์„ฑํ™”ํ•˜๊ณ  ํ•„ํ„ฐ๋ฅผ ์ผญ๋‹ˆ๋‹ค.
 27    s := make([]string,0,1000)
 28    for _, itm := range res {
 29        if itm != "" {
 30            tmp := strings.Split(itm," ")
 31            s = append(s,tmp...)
 32        }
 33    }
 34
 35    for _, photo := range s {
 36        formatCheck := strings.Split(photo, ".")
 37        format := formatCheck[len(formatCheck)-1]
 38
 39        if (strings.Compare(format, "png") == 0) || (strings.Compare(format, "ico") == 0) {
 40            picFile, err := os.Open(photo)
 41            if err != nil {
 42                log.Println("Error while opening photo, error is: ", err)
 43            }
 44
 45            pictureRawData := make([]byte, 10000*10000)
 46            picFile.Read(pictureRawData)
 47
 48            imageLabel := Label(Image(NewPhoto(Data(pictureRawData))))
 49                        imagesLoaded = append(imagesLoaded,imageLabel)
 50            var deleteTestButton *TButtonWidget
 51            deleteTestButton = TButton(
 52                Txt("Unshow Image"),
 53            Command(func() {
 54                GridForget(imageLabel.Window)
 55                GridForget(deleteTestButton.Window)
 56            }))
 57
 58            pbuttons = append(pbuttons,deleteTestButton)
 59
 60                        listbox.AddItem(len(imagesLoaded)-1,PhotoName(photo))
 61                        listbox2.AddItem(len(imagesLoaded)-1,PhotoName(photo))
 62            picFile.Close()
 63        }
 64    }
 65}
 66
 67func DeleteSelected () {
 68    s:=listbox.Selected()
 69    if len(s) == 0 {
 70        return
 71        }
 72    for _, i := range s {
 73        listbox.DeleteOne(i)
 74        listbox2.DeleteOne(i)
 75        if len(imagesLoaded)-1>i {
 76            continue
 77        }
 78        if cur == imagesLoaded[i] {
 79            pbutton = nil
 80            cur = nil
 81        }
 82        Destroy(imagesLoaded[i])
 83        Destroy(pbuttons[i])
 84                imagesLoaded = append(imagesLoaded[:i],imagesLoaded[i+1:]...)
 85        pbuttons = append(pbuttons[:i], pbuttons[i+1:]...)
 86    }
 87}
 88
 89func SelectImage() {
 90        s:=listbox2.Selected()
 91        if len(s) == 0 {
 92                return
 93        }
 94
 95    if len(imagesLoaded) -1 < s[0] {
 96        return
 97    }
 98    if imagesLoaded[s[0]] == nil {
 99        return
100    }
101    if cur != nil {
102            GridForget(cur.Window)
103    }
104    if pbutton != nil {
105        GridForget(pbutton.Window)
106    }
107
108        Grid(imagesLoaded[s[0]], Row(0), Column(2))
109    Grid(pbuttons[s[0]], Row(0), Column(3))
110    cur = imagesLoaded[s[0]]
111    pbutton = pbuttons[s[0]]
112}
113
114func SelectIndex(index int) {
115
116    if len(imagesLoaded) -1 <index {
117        return
118    }
119    if imagesLoaded[index] == nil {
120        return
121    }
122    if cur != nil {
123            GridForget(cur.Window)
124    }
125    if pbutton != nil {
126        GridForget(pbutton.Window)
127    }
128
129        Grid(imagesLoaded[index], Row(0), Column(2))
130    Grid(pbuttons[index], Row(0), Column(3))
131    cur = imagesLoaded[index]
132    pbutton = pbuttons[index]
133}
134
135func main() {
136    menubar := Menu()
137    //DefaultTheme("awdark","themes/awthemes-10.4.0")
138    //ํ…Œ๋งˆ๋ฅผ ์‚ฌ์šฉํ•˜๊ณ  ์‹ถ์„ ๋•Œ์—๋Š” ํ…Œ๋งˆ ๋ช…๊ณผ ๊ฒฝ๋กœ๋ฅผ ์ง€์ •ํ•ด ์ค๋‹ˆ๋‹ค.
139    fileMenu := menubar.Menu()
140    extensions = make([]FileType,0,1)
141    extensions = append(extensions, FileType{ "Supported Images", []string {".png",".ico"}, "" } )
142    //ํ•„ํ„ฐ์— png์™€ ico๋ฅผ ๋„ฃ์–ด ์ค๋‹ˆ๋‹ค.
143    fileMenu.AddCommand(Lbl("Open..."), Underline(0), Accelerator("Ctrl+O"), Command(func () {
144        handleFileOpen()
145        SelectIndex(len(imagesLoaded)-1)
146    } ))
147    Bind(App, "<Control-o>", Command(func() { fileMenu.Invoke(0) }))
148    fileMenu.AddCommand(Lbl("Exit"), Underline(1), Accelerator("Ctrl+Q"), ExitHandler())
149    Bind(App, "<Control-q>", Command(func() { fileMenu.Invoke(1) }))
150    menubar.AddCascade(Lbl("File"), Underline(0), Mnu(fileMenu))
151        imagesLoaded = make([]*LabelWidget, 0, 10000)
152    pbuttons = make([]*TButtonWidget,0,10000)
153    var scrollx, scroll, scroll2, scrollx2 *TScrollbarWidget
154        listbox = Listbox(Yscrollcommand(func(e *Event) { e.ScrollSet(scroll)}) , Xscrollcommand( func(e *Event) { e.ScrollSet(scrollx)}))
155        listbox2 = Listbox(Yscrollcommand(func(e *Event) { e.ScrollSet(scroll2)}), Xscrollcommand(func(e *Event) { e.ScrollSet(scrollx2)}))
156        listbox.SelectMode("multiple")
157        listbox2 = Listbox()
158        listbox.Background("white")
159        listbox.SelectBackground("blue")
160        listbox.SelectForeground("yellow")
161        listbox2.Background("grey")
162        listbox2.SelectBackground("green")
163    listbox2.SelectForeground("blue")
164    listbox2.SelectBackground("brown")
165        listbox.Height(5)
166        listbox.Width(4)
167        listbox2.Height(5)
168        listbox2.Width(4)
169        delBtn := Button(Txt("Delete"), Command(func () { DeleteSelected() }))
170        selBtn := Button(Txt("Select"), Command(func () { SelectImage() }))
171        scroll = TScrollbar(Command(func(e *Event) { e.Yview(listbox) }))
172        scrollx = TScrollbar(Orient("horizontal"),Command(func(e *Event) { e.Xview(listbox) }))
173    scroll2 = TScrollbar(Command(func(e *Event) { e.Yview(listbox2) }))
174        scrollx2 = TScrollbar(Orient("horizontal"),Command(func(e *Event) { e.Xview(listbox2) }))
175        Grid(listbox,Row(1),Column(0), Sticky("nes"))
176        Grid(scroll,Row(1),Column(1), Sticky("nes"))
177    Grid(scrollx,Row(2),Column(0),  Sticky("nes"))
178        Grid(delBtn,Row(3),Column(0), Sticky("nes"))
179        Grid(listbox2,Row(1),Column(2), Sticky("nes"))
180        Grid(scroll2,Row(1),Column(3), Sticky("nes"))
181    Grid(scrollx2,Row(2),Column(2), Sticky("nes"))
182        Grid(selBtn,Row(3),Column(2), Sticky("nes"))
183    App.WmTitle(fmt.Sprintf("%s on %s", App.WmTitle(""), runtime.GOOS))
184    App.Configure(Mnu(menubar), Width("80c"), Height("60c")).Wait()
185}
186

Image viewer execution result

In this example, to keep the implementation simple, all image widgets are created in advance when loaded, and duplicate files are not checked. You may improve on the problems mentioned above, and you may also try changing the theme using the DefaultTheme method in the commented-out section. We encourage you to practice by writing a new program that improves these aspects.

Summary

In this post, we looked at how command invocation works in the Tcl/Tk library for Go, and created an image viewer with a listbox added.

  1. How Tcl/Tk library command invocation works
  2. How to use the listbox
  3. Changing listbox widget properties
  4. Writing an image viewer

We encourage you to apply this approach to modifying other libraries as well, and to write a complete program with the features you have added.