CGo-freies Go: Implementierung einer Tcl/Tk-GUI (modernc.org/tk9.0): Widget-Packing und Bildrendering

Python enthält von Haus aus eine GUI-Bibliothek wie Tkinter. Kürzlich wurde für die Programmiersprache Go die CGo-Free, Cross Platform Tk Bibliothek entwickelt, mit der Tcl/Tk verwendet werden kann. Heute werfen wir einen Blick auf ihre grundlegende Verwendung.
Ein Hello, Tk erstellen
Beginnen wir zunächst mit einem einfachen 'Hello, TK!'-Beispiel.
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}

Sehen wir uns den obigen Beispielcode und seine Ausgabe genauer an.
Wer Erfahrung mit Tk in Python hat, wird die Struktur verstehen, in der Widgets innerhalb eines Fensters oder direkt unterhalb eines Fensters gepackt werden. Je nach Widgettyp sind darin etwa Labels enthalten.
Ipadx und Ipady stehen für Internal padding und steuern den inneren Abstand der Widgets. In diesem Beispiel wird der Abstand der Schaltfläche angepasst.
Diese Bibliothek verfügt über eine Struktur namens Window, und die Variable App verwaltet das oberste Fenster. Diese ist bereits innerhalb der Bibliothek vordefiniert. Daher schließt die Funktion tk.App.Destroy(), die tk.App.Wait() beendet, das oberste Fenster.
Schauen wir uns nun einige Beispiele aus dem Ordner _examples im GitLab an.
SVG-Dateien verarbeiten
Im Folgenden sehen Sie ein Beispiel, das eine SVG-Datei in einem Label-Widget anzeigt.
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}

Die Verarbeitung von SVG in dieser Bibliothek erfolgt wie folgt:
- Der Inhalt der SVG-Datei wird als Zeichenkette eingelesen (oder wie im obigen Beispiel direkt eingebettet).
- Dieser Inhalt wird an die Funktion Data übergeben und in eine Zeichenkette mit Optionen umgewandelt (Option -data).
- Die resultierenden Bytewerte werden an die Funktion NewPhoto übergeben, die einen Zeiger auf die Struktur Img zurückgibt, welche ein Tcl/Tk-Bild repräsentiert.
- Beim Durchlauf durch die Funktion Image wird der Zeiger auf die Struktur Img in eine Zeichenkette mit der Option -Image umgewandelt.
- Die Umwandlung in eine Zeichenkette mit dem RAW-Wert der Struktur erfolgt, um das Label-Widget zu erzeugen.
ICO- und PNG-Dateien werden auf ähnliche Weise verarbeitet.
PNG-Dateien verarbeiten
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}

Der Ablauf bei der Verarbeitung von PNG-Dateien ist wie folgt:
- Die eingebettete Datei gopher.png wird in einen Zeichenkettentyp mit Optionen umgewandelt.
- Über die Funktion NewPhoto wird sie in den Typ *Img umgewandelt.
- Nach dem Durchlauf durch die Funktion Image wird sie in eine RAW-Zeichenkette umgewandelt und anschließend als Label-Widget erzeugt.
ICO-Dateien werden auf die gleiche Weise verarbeitet; der Unterschied zum SVG-Format liegt allein in der internen Verarbeitung der Funktion Data.
Schauen wir uns nun das Wesen der "Zeichenkette mit Optionen" an:
1type rawOption string
Die zuvor erwähnte Zeichenkette mit Optionen ist lediglich eine formatierte Zeichenkette.
1func (w *Window) optionString(_ *Window) string {
2 return w.String()
3}
Die Methode optionString ist eine Methode für einen Window-Zeiger und gibt eine Zeichenkette zurück.
Zum Schluss betrachten wir kurz den internen Aufbau der Funktion 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}
Der Code zeigt, dass bei ICO- und PNG-Dateien Kodierungs- und Dekodierungsvorgänge erforderlich sind. In allen anderen Fällen wird keine besondere Umwandlung vorgenommen; der bereits in Bytes umgewandelten Zeichenkette wird lediglich die Option -data hinzugefügt, um sie als Ergebnis der Funktion Data zu kennzeichnen.
Bilder über ein Menü-Widget laden
Wenn man das zuvor geübte Beispiel zum Laden von PNG- und ICO-Dateien um ein Menü-Widget erweitert, lässt sich eine Anwendung erstellen, die die gewünschten Bilder anzeigt.
Zunächst betrachten wir ein einfaches Beispiel für ein Menü-Widget.
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}

In diesem Beispiel wurden eine Menüleiste und Menüs erstellt, Buchstaben hervorgehoben, die Option Command verwendet, Tastenkombinationen gebunden und die anfängliche Größe des Anwendungsfensters festgelegt.
Nun ändern wir die mit GetOpenFile verknüpfte Funktion Command so, dass ein Programm entsteht, das Bilder lädt und anzeigt.
Der Plan für die Programmerstellung lautet wie folgt:
- Nur das Öffnen von PNG- und ICO-Dateien zulassen
- Die im Dateidialog ausgewählte Datei verarbeiten
- Ein Widget zur Anzeige des Bildes implementieren
Der folgende Code setzt diesen Plan um:
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}

Der obige Code arbeitet auf folgende Weise:
- Die Dateiendung wird mithilfe der Zeichenkettenvergleichsfunktion des Pakets strings überprüft
- Mit dem Paket os wird die Datei geöffnet, gelesen und anschließend geschlossen
- Das eingelesene Bild wird in einem Label-Widget angezeigt
Zusammenfassung
In diesem Beitrag haben wir mithilfe der Tcl/Tk-Bibliothek für Go folgende Inhalte behandelt:
- Grundstruktur einer GUI-Anwendung
- Verarbeitung verschiedener Bildformate wie SVG, PNG und ICO
- Packen von Widgets und Layoutverwaltung
- Struktur der Bildverarbeitung
- Tastenkombinationen und Widget-Befehle
Mit Go und Tcl/Tk lassen sich einfache und dennoch praxisnahe GUI-Programme erstellen. Auf dieser Grundlage wird empfohlen, sich an die Entwicklung komplexerer GUI-Anwendungen zu wagen.