---
id: 974f36e12dfcece861bfda1af9303980
author: Yunjin Lee
title: 'Implementacja GUI Tcl/Tk w Go bez CGo (modernc.org/tk9.0): pakowanie widżetów i renderowanie obrazów'
description: Implementacja tworzenia okien, rozmieszczania widżetów przycisków i paska menu oraz renderowania obrazów SVG/PNG/ICO w Go, z wykorzystaniem biblioteki międzyplatformowej modernc.org/tk9.0 bez zależności od CGo.
language: pl
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 zawiera w standardowej bibliotece wbudowane biblioteki GUI, takie jak Tkinter.
Niedawno dla języka Go opracowano bibliotekę [CGo-Free, Cross Platform Tk](https://pkg.go.dev/modernc.org/tk9.0), która umożliwia korzystanie z Tcl/Tk. Dzisiaj przyjrzymy się jej podstawowemu użyciu.

## Tworzenie Hello, Tk

Zacznijmy od prostego przykładu '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()
}
```

![wynik działania hello-tk](/assets/images/go-tk-helloworld-svg-etc/go-tk-hello.png)

---

Przyjrzyjmy się dokładniej powyższemu kodowi przykładowemu i jego wynikowi.

Jeśli miałeś(-aś) doświadczenie z Tk w Pythonie, zrozumiesz strukturę, w której widgety są pakowane wewnątrz okna lub bezpośrednio w oknie nadrzędnym. W zależności od rodzaju widgetu, etykiety i inne elementy są w nim zawarte.

_Ipadx i Ipady są skrótami od Internal padding, czyli wewnętrznego dopełnienia, i regulują odstępy wewnątrz widgetów. W tym przykładzie dostosowywane jest dopełnienie przycisku._

W tej bibliotece istnieje struktura Window, a zmienna App zarządza oknem najwyższego poziomu. Jest ona zdefiniowana wcześniej wewnątrz biblioteki. Dlatego funkcja tk.App.Destroy(), która kończy działanie tk.App.Wait(), pełni rolę zamykania okna najwyższego poziomu.

Teraz przyjrzyjmy się kilku przykładom znajdującym się w folderze \_examples w serwisie GitLab.

## Obsługa plików SVG

Poniżej przedstawiono przykład wyświetlania pliku SVG w widgecie etykiety.

```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()
}
```

![wynik działania go-tk-svg](/assets/images/go-tk-helloworld-svg-etc/go-tk-svg.png)

Sposób obsługi SVG w tej bibliotece wygląda następująco:

1. Odczytaj zawartość pliku SVG jako łańcuch znaków (lub umieść ją bezpośrednio, jak w powyższym przykładzie).
2. Przekaż tę zawartość do funkcji Data, aby przekształcić ją w łańcuch zawierający opcje (opcja -data).
3. Przekształcone bajty są przekazywane do funkcji NewPhoto, która zwraca wskaźnik do struktury Img reprezentującej obraz Tcl/Tk.
4. Podczas przechodzenia przez funkcję Image wskaźnik do struktury Img jest przekształcany w łańcuch z dodaną opcją -Image.
5. Przekształcenie wartości RAW struktury w łańcuch jest potrzebne do utworzenia widgetu Label.

Pliki ICO i PNG są przetwarzane w podobny sposób.

## Obsługa plików 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()
}
```

![wynik działania go-tk-png](/assets/images/go-tk-helloworld-svg-etc/go-tk-png.png)

Proces obsługi plików PNG przebiega następująco:

1. Osadzony plik gopher.png zamieniany jest na typ łańcuchowy zawierający opcje.
2. Za pomocą funkcji NewPhoto zostaje przekształcony w typ \*Img.
3. Po przejściu przez funkcję Image, wynik zostaje przekształcony w łańcuch RAW, a następnie tworzony jest widget etykiety.

Pliki ICO są przetwarzane w ten sam sposób, a jedyna różnica w stosunku do formatu SVG dotyczy sposobu przetwarzania wewnątrz funkcji Data.

Przyjrzyjmy się teraz temu, czym jest "łańcuch zawierający opcje":

```go
type rawOption string
```

Wspomniany wcześniej łańcuch zawierający opcje jest po prostu sformatowanym łańcuchem znaków.

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

Metoda optionString jest metodą wskaźnika Window i zwraca łańcuch znaków.

Na koniec krótko przyjrzyjmy się wewnętrznej strukturze funkcji 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)))
}
```

Z kodu wynika, że w przypadku plików ICO i PNG potrzebne są procesy kodowania i dekodowania.
W pozostałych przypadkach bez żadnej specjalnej konwersji do łańcucha bajtów dodawana jest jedynie opcja -data, która oznacza, że jest to wynik funkcji Data.

### Wczytywanie obrazów za pomocą widgetu menu

Dodając widget menu do wcześniej przećwiczonego przykładu wczytywania plików PNG i ICO, można stworzyć aplikację wyświetlającą wybrane obrazy.

Najpierw przyjrzyjmy się prostemu przykładowi widgetu menu.

```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()
}
```

![wynik działania go-tk-menu-bar](/assets/images/go-tk-helloworld-svg-etc/go-tk-menu-bar.png)

W tym przykładzie wypróbowaliśmy pasek menu i tworzenie menu, wyróżnianie znaków, opcję Command, wiązanie skrótów klawiszowych oraz ustawianie początkowego rozmiaru okna aplikacji.

Teraz zmodyfikujemy funkcję Command przypisaną do GetOpenFile, aby stworzyć program wczytujący obraz i wyświetlający go.

Plan tworzenia programu wygląda następująco:

1. Ograniczenie otwierania wyłącznie do plików PNG i ICO
2. Obsługa pliku wybranego w oknie dialogowym
3. Implementacja widgetu do wyświetlania obrazu

Poniżej znajduje się kod odzwierciedlający ten plan:

```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()
}
```

![wynik działania go-tk-image-open](/assets/images/go-tk-helloworld-svg-etc/go-tk-image-open.png)

Powyższy kod działa w następujący sposób:

1. Rozszerzenie pliku sprawdzane jest za pomocą funkcji porównujących łańcuchy znaków z pakietu strings
2. Plik jest otwierany, odczytywany i zamykany za pomocą pakietu os
3. Wczytany obraz jest wyświetlany w widgecie etykiety

## Podsumowanie

W tym artykule omówiliśmy, z wykorzystaniem biblioteki Tcl/Tk dla języka Go, następujące zagadnienia:

1. Podstawową strukturę aplikacji GUI
2. Obsługę różnych formatów obrazów, takich jak SVG, PNG i ICO
3. Pakowanie widgetów i zarządzanie układem
4. Strukturę przetwarzania danych obrazów
5. Wiązanie skrótów klawiszowych i polecenia widgetów

Połączenie języka Go z Tcl/Tk pozwala tworzyć proste, a zarazem praktyczne programy z interfejsem graficznym. Zachęcam do dalszego rozwijania swoich umiejętności i podjęcia wyzwania stworzenia bardziej złożonych aplikacji GUI.
