GoSuda

Implementing a CGo-free Go Tcl/Tk GUI (modernc.org/tk9.0): Widget Packing and Image Rendering

By Yunjin Lee views ...

Python ships with GUI libraries such as Tkinter built in. Recently, the CGo-Free, Cross Platform Tk library was developed to make Tcl/Tk available in the Go language. Today, we will examine its basic usage.

Creating Hello, Tk

Let us begin with a simple 'Hello, TK!' example.

 1package main
 2
 3import tk "modernc.org/tk9.0"
 4
 5func main() {
 6    tk.Pack(
 7        tk.TButton(
 8            tk.Txt("Hello, TK!"),
 9            tk.Command(func() {
10                tk.Destroy(tk.App)
11            })),
12        tk.Ipadx(10), tk.Ipady(5), tk.Padx(15), tk.Pady(10),
13    )
14    tk.App.Wait()
15}

hello-tk execution result


Let us examine the above example code and its execution result in detail.

If you have experience using Tk in Python, you will understand the structure in which widgets are packed inside a window or packed directly beneath the window. Depending on the type of widget, labels and other elements are contained within it.

Ipadx and Ipady are abbreviations of Internal padding, which adjusts the padding of internal widgets. In this example, the padding of the button is adjusted.

This library has a Window structure, and the variable App manages the top-level window. This is predefined within the library. Accordingly, the tk.App.Destroy() function, which terminates tk.App.Wait(), serves to close the top-level window.

Now, let us examine some examples located in the _examples folder on GitLab.

Handling SVG Files

The following is an example that displays an SVG file in a label widget.

 1package main
 2
 3import . "modernc.org/tk9.0"
 4
 5// https://en.wikipedia.org/wiki/SVG
 6const svg = `<?xml version="1.0" encoding="UTF-8" standalone="no"?>
 7<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
 8<svg width="391" height="391" viewBox="-70.5 -70.5 391 391" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
 9<rect fill="#fff" stroke="#000" x="-70" y="-70" width="390" height="390"/>
10<g opacity="0.8">
11    <rect x="25" y="25" width="200" height="200" fill="lime" stroke-width="4" stroke="pink" />
12    <circle cx="125" cy="125" r="75" fill="orange" />
13    <polyline points="50,150 50,200 200,200 200,100" stroke="red" stroke-width="4" fill="none" />
14    <line x1="50" y1="50" x2="200" y2="200" stroke="blue" stroke-width="4" />
15</g>
16</svg>`
17
18func main() {
19    Pack(Label(Image(NewPhoto(Data(svg)))),
20        TExit(),
21        Padx("1m"), Pady("2m"), Ipadx("1m"), Ipady("1m"))
22    App.Center().Wait()
23}

go-tk-svg execution result

The method for processing SVG in this library is as follows:

  1. Read the contents of the SVG file as a string (or include it directly, as in the example above).
  2. Pass this content to the Data function to convert it into a string containing options (the -data option).
  3. The converted byte value is passed to the NewPhoto function, which returns a pointer to an Img structure representing a Tcl/Tk image.
  4. As it passes through the Image function, the Img structure pointer is converted into a string with the -Image option added.
  5. The conversion into a string containing the raw structure value is performed for the purpose of creating the Label widget.

ICO and PNG files are processed in a similar manner.

Handling PNG Files

 1package main
 2
 3import _ "embed"
 4import . "modernc.org/tk9.0"
 5
 6//go:embed gopher.png
 7var gopher []byte
 8
 9func main() {
10    Pack(Label(Image(NewPhoto(Data(gopher)))),
11        TExit(),
12        Padx("1m"), Pady("2m"), Ipadx("1m"), Ipady("1m"))
13    App.Center().Wait()
14}

go-tk-png execution result

The PNG file processing procedure is as follows:

  1. Convert the embedded gopher.png into a string type containing options.
  2. Convert it into a *Img type through the NewPhoto function.
  3. Pass it through the Image function, where it is converted into a RAW string, and then a label widget is created.

ICO files are processed in the same manner, and the only difference from the SVG format lies in how the Data function handles them internally.

Let us now examine the nature of the "string containing options":

1type rawOption string

The aforementioned string containing options is simply a formatted string.

1func (w *Window) optionString(_ *Window) string {
2    return w.String()
3}

The optionString method is a method on the Window pointer and returns a string.

Finally, let us briefly examine the internal structure of the Data function:

 1func Data(val any) Opt {
 2    switch x := val.(type) {
 3    case []byte:
 4        switch {
 5        case bytes.HasPrefix(x, pngSig):
 6            // ok
 7        case bytes.HasPrefix(x, icoSig):
 8            b := bytes.NewBuffer(x)
 9            img, err := ico.Decode(bytes.NewReader(x))
10            if err != nil {
11                fail(err)
12                return rawOption("")
13            }
14
15            b.Reset()
16            if err := png.Encode(b, img); err != nil {
17                fail(err)
18                return rawOption("")
19            }
20
21            val = b.Bytes()
22        }
23    }
24    return rawOption(fmt.Sprintf(`-data %s`, optionString(val)))
25}

As the code shows, ICO and PNG files require encoding and decoding processes. In other cases, no special conversion is performed; only the -data option is added to the byte-converted string to indicate that it is the result of the Data function.

Loading Images with a Menu Widget

By adding a menu widget to the PNG and ICO loading example practiced above, you can create an application that displays the required image.

First, let us examine a simple menu widget example.

 1package main
 2
 3import (
 4    "fmt"
 5    . "modernc.org/tk9.0"
 6    "runtime"
 7)
 8
 9func main() {
10    menubar := Menu()
11
12    fileMenu := menubar.Menu()
13    fileMenu.AddCommand(Lbl("New"), Underline(0), Accelerator("Ctrl+N"))
14    fileMenu.AddCommand(Lbl("Open..."), Underline(0), Accelerator("Ctrl+O"), Command(func() { GetOpenFile() }))
15    Bind(App, "<Control-o>", Command(func() { fileMenu.Invoke(1) }))
16    fileMenu.AddCommand(Lbl("Save As..."), Underline(5))
17    fileMenu.AddSeparator()
18    fileMenu.AddCommand(Lbl("Exit"), Underline(1), Accelerator("Ctrl+Q"), ExitHandler())
19    Bind(App, "<Control-q>", Command(func() { fileMenu.Invoke(4) }))
20    menubar.AddCascade(Lbl("File"), Underline(0), Mnu(fileMenu))
21
22    App.WmTitle(fmt.Sprintf("%s on %s", App.WmTitle(""), runtime.GOOS))
23    App.Configure(Mnu(menubar), Width("8c"), Height("6c")).Wait()
24}

go-tk-menu-bar execution result

In this example, we set up a menu bar and menus, character underlining, the Command option, keyboard shortcut bindings, and the initial size of the application window.

Now, let us modify the Command function assigned to GetOpenFile to create a program that loads and displays an image.

The program development plan is as follows:

  1. Restrict opening to PNG and ICO files only
  2. Process the file selected in the file dialog
  3. Implement a widget for displaying the image

The following code reflects this plan:

 1package main
 2
 3import (
 4    "fmt"
 5    "log"
 6    "os"
 7    "runtime"
 8    "strings"
 9
10    . "modernc.org/tk9.0"
11)
12
13func handleFileOpen() {
14    s := GetOpenFile()
15    for _, photo := range s {
16        formatCheck := strings.Split(photo, ".")
17        format := formatCheck[len(formatCheck)-1]
18
19        if (strings.Compare(format, "png") == 0) || (strings.Compare(format, "ico") == 0) {
20            picFile, err := os.Open(photo)
21            if err != nil {
22                log.Println("Error while opening photo, error is: ", err)
23            }
24
25            pictureRawData := make([]byte, 10000*10000)
26            picFile.Read(pictureRawData)
27
28            imageLabel := Label(Image(NewPhoto(Data(pictureRawData))))
29            Pack(imageLabel,
30                TExit(),
31                Padx("1m"), Pady("2m"), Ipadx("1m"), Ipady("1m"))
32        }
33        picFile.Close()
34    }
35}
36
37func main() {
38    menubar := Menu()
39
40    fileMenu := menubar.Menu()
41    fileMenu.AddCommand(Lbl("Open..."), Underline(0), Accelerator("Ctrl+O"), Command(handleFileOpen))
42    Bind(App, "<Control-o>", Command(func() { fileMenu.Invoke(0) }))
43    fileMenu.AddCommand(Lbl("Exit"), Underline(1), Accelerator("Ctrl+Q"), ExitHandler())
44    Bind(App, "<Control-q>", Command(func() { fileMenu.Invoke(1) }))
45    menubar.AddCascade(Lbl("File"), Underline(0), Mnu(fileMenu))
46
47    App.WmTitle(fmt.Sprintf("%s on %s", App.WmTitle(""), runtime.GOOS))
48    App.Configure(Mnu(menubar), Width("10c"), Height("10c")).Wait()
49}

go-tk-image-open execution result

The above code operates in the following manner:

  1. It checks the file extension using the string comparison functions of the strings package.
  2. It opens, reads, and then closes the file using the os package.
  3. The loaded image is displayed in a label widget.

Summary

In this article, we covered the following topics using the Tcl/Tk library for Go:

  1. The basic structure of a GUI application
  2. Processing of various image formats such as SVG, PNG, and ICO
  3. Widget packing and layout management
  4. The structure of image data processing
  5. Keyboard shortcut bindings and widget commands

Using Go and Tcl/Tk together enables the creation of simple yet practical GUI programs. Based on this, we encourage you to take on the challenge of developing more complex GUI applications.