---
id: 974f36e12dfcece861bfda1af9303980
author: Yunjin Lee
title: CGo なしの Go による Tcl/Tk(modernc.org/tk9.0) GUI 実装：ウィジェットのパッキングと画像レンダリング
description: CGo への依存がないクロスプラットフォームライブラリ modernc.org/tk9.0 を使用して、Go でのウィンドウ作成、ボタンやメニューバーのウィジェットのパッキング、SVG/PNG/ICO 画像のレンダリングを実装します。
language: ja
date: 2024-10-22T11:51:27.614782351Z
path: /blog/posts/creating-a-gui-with-tk-in-go-z004dd008
category: gui
lang_canonical:
    ko: https://blog.naver.com/bugaku/223629101405
---

Pythonには、Tkinterのような GUI ライブラリが標準で組み込まれています。
最近、Go 言語でも Tcl/Tk を利用できるように [CGo-Free, Cross Platform Tk ライブラリ](https://pkg.go.dev/modernc.org/tk9.0) が開発されました。本日は、その基本的な使い方を見ていきます。

## Hello, Tk の作成

まずは簡単な 'Hello, TK!' の例から始めましょう。

```go
package main

import tk "modernc.org/tk9.0"

func main() {
    tk.Pack(
        tk.TButton(
            tk.Txt("Hello, TK!"),
            tk.Command(func() {
                tk.Destroy(tk.App)
            })),
        tk.Ipadx(10), tk.Ipady(5), tk.Padx(15), tk.Pady(10),
    )
    tk.App.Wait()
}
```

![hello-tk 実行結果](/assets/images/go-tk-helloworld-svg-etc/go-tk-hello.png)

---

上記のサンプルコードと実行結果を詳しく見ていきます。

Pythonで Tk を使用した経験がある方であれば、ウィンドウ内にウィジェットがパックされる構造や、ウィンドウの直下に直接ウィジェットがパックされる構造を理解されるでしょう。ウィジェットの種類によっては、ラベルなどがその中に含まれます。

_Ipadx と Ipady は Internal padding の略称で、内部ウィジェットの余白を調整します。この例では、ボタンの余白が調整されます。_

このライブラリには Window 構造体があり、App という変数が最上位ウィンドウを管理します。これはライブラリ内部で事前に定義されています。したがって、tk.App.Wait() を終了させる tk.App.Destroy() 関数が、最上位ウィンドウを閉じる役割を果たします。

次に、GitLab の \_examples フォルダにあるいくつかの例を見ていきましょう。

## SVG ファイルの処理

以下は、SVG ファイルを Label ウィジェットに表示する例です。

```go
package main

import . "modernc.org/tk9.0"

// https://en.wikipedia.org/wiki/SVG
const svg = `<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<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">
<rect fill="#fff" stroke="#000" x="-70" y="-70" width="390" height="390"/>
<g opacity="0.8">
    <rect x="25" y="25" width="200" height="200" fill="lime" stroke-width="4" stroke="pink" />
    <circle cx="125" cy="125" r="75" fill="orange" />
    <polyline points="50,150 50,200 200,200 200,100" stroke="red" stroke-width="4" fill="none" />
    <line x1="50" y1="50" x2="200" y2="200" stroke="blue" stroke-width="4" />
</g>
</svg>`

func main() {
    Pack(Label(Image(NewPhoto(Data(svg)))),
        TExit(),
        Padx("1m"), Pady("2m"), Ipadx("1m"), Ipady("1m"))
    App.Center().Wait()
}
```

![go-tk-svg 実行結果](/assets/images/go-tk-helloworld-svg-etc/go-tk-svg.png)

このライブラリで SVG を処理する方法は次のとおりです:

1. SVG ファイルの内容を文字列として読み込みます(または上記の例のように直接埋め込みます)。
2. この内容を Data 関数に渡し、オプションを含む文字列に変換します(-data オプション)。
3. 変換されたバイト値は NewPhoto 関数に渡され、Tcl/Tk イメージを表す Img 構造体のポインタを返します。
4. Image 関数を通過する過程で、Img 構造体のポインタは -Image オプションが追加された文字列に変換されます。
5. 構造体の RAW 値を格納した文字列に変換するのは、Label ウィジェットを生成するためです。

ICO と PNG ファイルも、同様の方法で処理されます。

## PNG ファイルの処理

```go
package main

import _ "embed"
import . "modernc.org/tk9.0"

//go:embed gopher.png
var gopher []byte

func main() {
    Pack(Label(Image(NewPhoto(Data(gopher)))),
        TExit(),
        Padx("1m"), Pady("2m"), Ipadx("1m"), Ipady("1m"))
    App.Center().Wait()
}
```

![go-tk-png 実行結果](/assets/images/go-tk-helloworld-svg-etc/go-tk-png.png)

PNG ファイルの処理過程は次のとおりです:

1. 埋め込まれた gopher.png を、オプションを含む文字列型に変換します。
2. NewPhoto 関数によって *Img 型に変換します。
3. Image 関数を経て RAW 文字列に変換された後、Label ウィジェットとして生成されます。

ICO ファイルも同じ方法で処理されますが、SVG 形式との違いは Data 関数内部の処理方法のみです。

ここで、「オプションを含む文字列」の正体を見ていきましょう:

```go
type rawOption string
```

前述のオプションを含む文字列は、単に整形された文字列にすぎません。

```go
func (w *Window) optionString(_ *Window) string {
    return w.String()
}
```

optionString メソッドは Window ポインタに対するメソッドであり、文字列を返します。

最後に、Data 関数の内部構造を簡単に見ていきましょう:

```go
func Data(val any) Opt {
    switch x := val.(type) {
    case []byte:
        switch {
        case bytes.HasPrefix(x, pngSig):
            // ok
        case bytes.HasPrefix(x, icoSig):
            b := bytes.NewBuffer(x)
            img, err := ico.Decode(bytes.NewReader(x))
            if err != nil {
                fail(err)
                return rawOption("")
            }

            b.Reset()
            if err := png.Encode(b, img); err != nil {
                fail(err)
                return rawOption("")
            }

            val = b.Bytes()
        }
    }
    return rawOption(fmt.Sprintf(`-data %s`, optionString(val)))
}
```

コードを見ると、ICO や PNG ファイルの場合はエンコード/デコードの処理が必要であることがわかります。
それ以外の場合には特別な変換を行わず、バイト型に変換された文字列に -data オプションを追加するだけで、Data 関数の結果であることを示します。

### メニューウィジェットによる画像の読み込み

先ほど練習した PNG、ICO の読み込み例にメニューウィジェットを追加すると、必要な画像を表示するアプリケーションを作成できます。

まず、簡単なメニューウィジェットの例を見ていきましょう。

```go
package main

import (
    "fmt"
    . "modernc.org/tk9.0"
    "runtime"
)

func main() {
    menubar := Menu()

    fileMenu := menubar.Menu()
    fileMenu.AddCommand(Lbl("New"), Underline(0), Accelerator("Ctrl+N"))
    fileMenu.AddCommand(Lbl("Open..."), Underline(0), Accelerator("Ctrl+O"), Command(func() { GetOpenFile() }))
    Bind(App, "<Control-o>", Command(func() { fileMenu.Invoke(1) }))
    fileMenu.AddCommand(Lbl("Save As..."), Underline(5))
    fileMenu.AddSeparator()
    fileMenu.AddCommand(Lbl("Exit"), Underline(1), Accelerator("Ctrl+Q"), ExitHandler())
    Bind(App, "<Control-q>", Command(func() { fileMenu.Invoke(4) }))
    menubar.AddCascade(Lbl("File"), Underline(0), Mnu(fileMenu))

    App.WmTitle(fmt.Sprintf("%s on %s", App.WmTitle(""), runtime.GOOS))
    App.Configure(Mnu(menubar), Width("8c"), Height("6c")).Wait()
}
```

![go-tk-メニューバー 実行結果](/assets/images/go-tk-helloworld-svg-etc/go-tk-menu-bar.png)

この例では、メニューバーとメニューの作成、文字の強調表示、Command オプション、ショートカットキーのバインディング、そしてアプリケーションウィンドウの初期サイズの設定を試みました。

次に、GetOpenFile で指定された Command 関数を修正し、画像を読み込んで表示するプログラムを作成してみましょう。

プログラムの作成計画は次のとおりです:

1. PNG と ICO ファイルのみを開けるように制限する
2. ファイルダイアログで選択されたファイルを処理する
3. 画像を表示するためのウィジェットを実装する

以下は、この計画を反映したコードです:

```go
package main

import (
    "fmt"
    "log"
    "os"
    "runtime"
    "strings"

    . "modernc.org/tk9.0"
)

func handleFileOpen() {
    s := GetOpenFile()
    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))))
            Pack(imageLabel,
                TExit(),
                Padx("1m"), Pady("2m"), Ipadx("1m"), Ipady("1m"))
        }
        picFile.Close()
    }
}

func main() {
    menubar := Menu()

    fileMenu := menubar.Menu()
    fileMenu.AddCommand(Lbl("Open..."), Underline(0), Accelerator("Ctrl+O"), Command(handleFileOpen))
    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))

    App.WmTitle(fmt.Sprintf("%s on %s", App.WmTitle(""), runtime.GOOS))
    App.Configure(Mnu(menubar), Width("10c"), Height("10c")).Wait()
}
```

![go-tk-画像の読み込み 実行結果](/assets/images/go-tk-helloworld-svg-etc/go-tk-image-open.png)

上記のコードは次のように動作します:

1. strings パッケージの文字列比較関数を使ってファイル拡張子を確認します
2. os パッケージを使ってファイルを開き、読み込んだ後に閉じます
3. 読み込んだ画像は Label ウィジェットとして表示されます

## まとめ

本記事では、Go 言語の Tcl/Tk ライブラリを活用し、次の内容を扱いました:

1. GUI アプリケーションの基本構造
2. SVG、PNG、ICO などの多様な画像フォーマットの処理
3. ウィジェットのパッキングとレイアウト管理
4. 画像データの処理構造
5. ショートカットキーのバインディングとウィジェットコマンド

Go 言語と Tcl/Tk を組み合わせることで、シンプルかつ実用的な GUI プログラムを作成できます。これを基礎として、より複雑な GUI アプリケーションの開発にぜひ挑戦してみてください。
