GoSuda

Creating a GUI with Tk in Go

By Yunjin Lee
views ...

Python inherently includes GUI libraries such as Tkinter. Recently, a CGo-Free, Cross Platform Tk library has been developed to enable the use of Tcl/Tk in the Go language. Today, we will examine its fundamental usage.

Creating "Hello, Tk"

Let us commence 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


We shall now meticulously analyze the aforementioned example code and its execution result.

Individuals familiar with Python's Tk will comprehend the structure wherein widgets are packed within a window or directly under the window. Labels and other elements are incorporated based on the type of widget.

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

This library features a Window struct, and the variable App manages the top-level window. This is pre-defined within the library. Consequently, the tk.App.Destroy() function, which terminates tk.App.Wait(), serves to close the top-level window.

Let us now examine several examples located in the _examples folder of GitLab.

Processing SVG Files

The following illustrates an example of displaying an SVG file within 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. The content of the SVG file is read as a string (or directly included as in the preceding example).
  2. This content is transmitted to the Data function to convert it into a string containing options (the -data option).
  3. The converted byte value is transmitted to the NewPhoto function, which returns an Img struct pointer representing the Tcl/Tk image.
  4. As it passes through the Image function, the Img struct pointer is converted into a string with the -Image option appended.
  5. The reason for converting to a string containing the raw struct value is for the creation of the Label widget.

ICO and PNG files are processed in a similar manner.

Processing 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. The embedded gopher.png is converted into a string type that includes options.
  2. It is converted into an *Img type via the NewPhoto function.
  3. After passing through the Image function and being converted into a RAW string, it is created as a label widget.

ICO files are processed identically, with the sole difference from the SVG format being the internal processing method of the Data function.

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

1type rawOption string

The aforementioned string with options is merely a formatted string.

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

The optionString method is a method for a Window pointer, which 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}

Examination of the code reveals that ICO or PNG files necessitate an encoding/decoding process. In other instances, no specific conversion is required; only the -data option is appended to the byte-converted string to indicate that it is the output of the Data function.

Loading Images with a Menu Widget

By incorporating a menu widget into the previously practiced PNG and ICO loading examples, an application capable of displaying requisite images can be constructed.

Let us first 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-menubar-execution-result

In this example, we have configured a menu bar and menu creation, text highlighting, Command options, shortcut key binding, and the initial size of the application window.

We shall now modify the Command function designated as GetOpenFile to create a program that loads and displays images.

The program development plan is as follows:

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

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-loading-execution-result

The aforementioned code operates in the following manner:

  1. The file extension is verified using string comparison functions from the strings package.
  2. The file is opened, read, and closed using the os package.
  3. The read image is displayed as a label widget.

Summary

In this discourse, we utilized the Tcl/Tk library for the Go language to address the following topics:

  1. Fundamental structure of GUI applications
  2. Processing various image formats, including SVG, PNG, and ICO
  3. Widget packing and layout management
  4. Structure of image data processing
  5. Shortcut key binding and widget commands

By integrating the Go language with Tcl/Tk, it is feasible to construct simple yet practical GUI programs. We encourage you to undertake the development of more intricate GUI applications based on this foundation.