GoSuda

Implementacja GUI Tcl/Tk w Go bez CGo (modernc.org/tk9.0): pakowanie widżetów i renderowanie obrazów

By Yunjin Lee views ...

Python zawiera w standardowej bibliotece wbudowane biblioteki GUI, takie jak Tkinter. Niedawno dla języka Go opracowano bibliotekę CGo-Free, Cross Platform Tk, 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!'.

 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}

wynik działania hello-tk


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.

 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}

wynik działania go-tk-svg

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

 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}

wynik działania go-tk-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":

1type rawOption string

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

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

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

Na koniec krótko przyjrzyjmy się wewnętrznej strukturze funkcji Data:

 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}

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.

 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}

wynik działania go-tk-menu-bar

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:

 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}

wynik działania go-tk-image-open

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.