Aus Linux-Magazin 09/2021

GUIs in Go programmieren mit Fyne

© Ihor Rudenko / 123RF.com

Beim ursprünglich für die Systemprogrammierung entwickelten Go waren grafische Oberflächen meist nicht erforderlich. Mit dem relativ neuen Toolkit Fyne lassen sich inzwischen trotzdem plattformunabhängig GUIs für Go-Programme bauen.

Es erfordert einige Vorkehrungen, um mit dem Framework Fyne [1] eine grafische Benutzerschnittstelle zu erstellen. Zum einen muss Go installiert sein, und zwar mindestens in Version 1.12. Zusätzlich benötigen Sie noch den Compiler GCC und einen Grafiktreiber [2]. Ist alles installiert, laden Sie mit dem Befehl »go get fyne.io/fyne/v2« auf der Kommandozeile nachträglich Fyne in Version 2 herunter und richten es ein.

Um einen ersten Eindruck der verschiedenen Widgets von Fyne zu bekommen, sehen Sie sich am besten eine Demo-App [3] mit den verfügbaren Steuerelementen an. Diese Fyne-Demo laden Sie mit »go get fyne.io/fyne/v2/cmd/fyne_demo« herunter und starten sie anschließend mit dem Aufruf »fyne_demo« im Terminal.

Grundgerüst

Um ein erstes, noch recht spartanisches Fenster in Fyne zu erstellen, genügen wenige Zeilen Code (Listing 1). Die Import-Anweisungen holen die benötigten Packages in die Anwendung. Um ein ausführbares Programm zu erzeugen, wird in Zeile 9 die Funktion »main()« als Einsprungpunkt definiert. Die Methode »app.New()« legt eine neue Fyne-Instanz an, und mit der Methode »a.NewWindow(“Titel“)« geben Sie einen Titel für die Kopfzeile des neu erzeugten Programmfensters an.

Listing 1

Grundgerüst

package main
import (
  "fyne.io/fyne/v2"
  "fyne.io/fyne/v2/app"
  "fyne.io/fyne/v2/widget"
)
func main() {
  a := app.New()
  w := a.NewWindow("Titel")
  w.Resize(fyne.NewSize(200, 200))
  w.SetContent(widget.NewLabel("Hallo Welt!"))
  w.CenterOnScreen()
  w.ShowAndRun()
}

Wie zu vermuten, lässt die Anweisung »w.Resize(fyne.NewSize(200, 200))« das Fenster in der definierten Breite und Höhe erscheinen. Der Aufruf von »w.SetContent(widget.NewLabel(“Hallo Welt!”))« platziert ein einfaches Label mit dem obligatorischen Text “Hallo Welt!” im Fenster. Als recht nützlich erweist sich die Methode »w.CenterOnScreen()«. Sie stellt das Fyne-Fenster auf dem Bildschirm zentriert dar. Die Methode »w.ShowAndRun()« zeigt das Fenster schließlich an. Die Applikation läuft in einer Art Dauerschleife und wartet auf Benutzeraktionen.

Prüfsummenrechner

Zur Veranschaulichung soll ein kleines Tool zur Berechnung von Prüfsummen mit Fyne dienen (Listing 2). Wir zeigen im Folgenden im Detail, wie man dessen grafische Oberfläche (Abbildung 1) mit Fyne umsetzt. Zuerst wollen wir uns ansehen, wie man eine Menüleiste in eine Fyne-Applikation integriert.

Abbildung 1: Ein kleines Beispielwerkzeug zu Berechnung von Prüfsummen.

Abbildung 1: Ein kleines Beispielwerkzeug zu Berechnung von Prüfsummen.

Listing 2

checksum.go

package main
import (
  "fyne.io/fyne/v2"
  "fyne.io/fyne/v2/app"
  "fyne.io/fyne/v2/container"
  "fyne.io/fyne/v2/dialog"
  "fyne.io/fyne/v2/theme"
  "fyne.io/fyne/v2/widget"
  "os"
  "strings"
)
func main() {
  a := app.New()
  a.Settings().SetTheme(theme.LightTheme())
  w := a.NewWindow("Prüfsummen-Rechner")
  w.Resize(fyne.NewSize(600, 800))
  menuItemLight := fyne.NewMenuItem("Light Theme", func() {
    a.Settings().SetTheme(theme.LightTheme())
  })
  menuItemDark := fyne.NewMenuItem("Dark Theme", func() {
    a.Settings().SetTheme(theme.DarkTheme())
  })
  menuQuit := fyne.NewMenu("Datei")
  menuTheme := fyne.NewMenu("Ansicht", menuItemLight, menuItemDark)
  mainMenu := fyne.NewMainMenu(menuQuit, menuTheme)
  w.SetMainMenu(mainMenu)
  w.CenterOnScreen()
  labelFile := widget.NewLabel("Datei:")
  entryFile := widget.NewEntry()
  buttonSelect := widget.NewButton("Datei wählen", func() {
    dialog.ShowFileOpen(func(read fyne.URIReadCloser, err error) {
      if err != nil {
        dialog.ShowError(err, w)
        return
      }
      if read == nil {
        return
      }
      selectedFile := read.URI().String()
      filePath := strings.TrimPrefix(selectedFile, "file://")
      entryFile.SetText(filePath)
    }, w)
  })
  labelHash := widget.NewLabel("Hashfunktion:")
  radioGroup := widget.NewRadioGroup([]string{"md5", "sha1", "sha256", "sha512"}, func(s string) {
  })
  radioGroup.SetSelected("md5")
  labelResult := widget.NewLabel("Ergebnis:")
  entryResult := widget.NewEntry()
  entryResult.Disable()
  buttonGenerate := widget.NewButton("Berechnen", func() {
    if entryFile.Text != "" {
      info, err := os.Stat(entryFile.Text)
      if os.IsNotExist(err) {
        dialog.ShowInformation("Info",
          "Datei existiert nicht!", w)
      } else {
        if info.IsDir() {
          dialog.ShowInformation("Info",
            "Datei ist ein Verzeichnis!", w)
        } else {
          if radioGroup.Selected == "md5" {
            go func() {
              checksum := genChecksum(entryFile.Text, "md5")
              entryResult.SetText(checksum)
            }()
          } else if radioGroup.Selected == "sha1" {
            go func() {
              checksum := genChecksum(entryFile.Text, "sha1")
              entryResult.SetText(checksum)
            }()
          } else if radioGroup.Selected == "sha256" {
            go func() {
              checksum := genChecksum(entryFile.Text, "sha256")
              entryResult.SetText(checksum)
            }()
          } else if radioGroup.Selected == "sha512" {
            go func() {
              checksum := genChecksum(entryFile.Text, "sha512")
              entryResult.SetText(checksum)
            }()
          }
        }
      }
    } else {
      dialog.ShowInformation("Info",
        "Bitte eine Datei auswählen!", w)
    }
  })
  labelCompare := widget.NewLabel("Vergleich:")
  entryCompare := widget.NewEntry()
  buttonCompare := widget.NewButton("Vergleichen", func() {
    if entryResult.Text != "" && entryCompare.Text != "" {
      if entryResult.Text == entryCompare.Text {
        dialog.ShowInformation("Info",
          "Prüfsummen identisch!", w)
      } else {
        dialog.ShowInformation("Fehler",
          "Prüfsummen nicht identisch!", w)
      }
    } else {
      dialog.ShowInformation("Info",
        "Bitte beide Eingabefelder ausfüllen!", w)
    }
  })
  c := container.NewVBox(labelFile, entryFile, buttonSelect, labelHash, radioGroup,  labelResult, entryResult, buttonGenerate, labelCompare, entryCompare, buttonCompare)
  w.SetContent(c)
  w.ShowAndRun()
}

Im vorliegenden Beispiel definieren die Zeilen 20 und 24 dafür zwei Menü-Items. Jedes entspricht einem Menüpunkt in der aufklappenden Liste, die erscheint, wenn man auf ein Menü in der Menüleiste klickt. Mit dem ersten Parameter legen Sie fest, wie der Menüeintrag heißt, und mit dem zweiten definieren Sie eine anonyme Funktion, deren Anweisungen das Programm ausführt, sobald der Benutzer auf den Menüeintrag klickt.

Diese beiden Menüeinträge werden in Zeile 29 als Parameter der Methode »fyne.NewMenu()« mitgegeben, um den Menüpunkt Ansicht in die Menüleiste einzufügen. Das Menü Ansicht umfasst folglich die beiden Menüeinträge Light Theme und Dark Theme, mit denen man im laufenden Betrieb das Theme umschaltet (siehe dazu auch Abschnitt “Themes” weiter unten).

Zeile 31 legt letztendlich mittels »fyne.NewMainMenu()« die Menüleiste mit den beiden Menüs an und fügt sie mit »w.SetMainMenu()« dem Fyne-Fenster hinzu. Das erste Menü in der Menüleiste eines Fyne-Fensters enthält stets automatisch den Menüeintrag »Quit« zum Schließen der Anwendung, ohne dass man dafür ein Menü-Item definieren muss.

Widgets

Die Definition von Widgets wie Schaltflächen, Labels, Eingabefeldern und so weiter erfolgt nach dem gleichen Schema. Ganz oben in der GUI der Beispielanwendung befindet sich ein Label, das den Text »Datei:« anzeigt. Zeile 35 erstellt dafür eine Variable »labelFile« und ordnet ihm mit »widget.NewLabel(“Datei:”)« ein Fyne-Label zu.

Der Parameter von »widget.NewLabel()« gibt den String an, der zu sehen sein soll. Um ihn sichtbar zu machen, muss man dem Container am Ende in Zeile 120 das Widget »NewVBox()« hinzufügen. Ein Container-Widget fungiert dabei als eine Art Layoutmanager, ein Konzept, das man auch aus anderen Sprachen wie Java kennt.

Dabei erstellt »container.NewVBox(labelFile)« ein Box-Layout, das alle Elemente der grafischen Oberfläche vertikal untereinander anordnet. Es handelt sich um eine variadische Funktion, die unbegrenzt Parameter aufnimmt, sodass sich beliebig viele GUI-Elemente hinzufügen lassen. Das Element aus dem ersten Parameter befindet sich dabei ganz oben in der grafischen Oberfläche, jedes folgende Element in der Parameterliste ordnet Fyne in der GUI darunter an. Die Anweisung »w.SetContent(c)« fügt das Layout dem Fyne-Fenster hinzu.

Analog funktioniert auch die Erstellung von Schaltflächen. Zuerst definiert Zeile 37 die Variable »buttonSelect«, der man mit »widget.NewButton()« einen Schalter zuweist. Beim ersten Übergabewert handelt es sich um ein String zur Beschriftung der Schaltfläche. Der zweite Parameter ist wieder eine anonyme Funktion, die das Programm ausführt, sobald ein Anwender den Button betätigt. In unserem Fall wird mit »dialog.ShowFileOpen()« ein Dateiauswahldialog aufgerufen (siehe Abschnitt “Grafische Dialoge”).

Die Eingabefelder von Fyne fallen relativ simpel aus. Sie erzeugen sie wie üblich mit »widget.NewEntry()« und integrieren sie wieder in dem Container-Widget in Zeile 120. Die Eingabefelder bringen verschiedene Methoden mit, darunter »Disable()«, das ein Editieren durch den Benutzer verhindert. Zeile 60 nutzt diese Möglichkeit für das Eingabefeld, das die berechnete Prüfsumme anzeigt.

Auch die Definition von Optionsfeldern (Radio-Buttons) gestaltet sich einfach. Zeile 53 definiert über »widget.NewRadioGroup()« eine zusammenhängende Gruppe von Optionsfeldern, bei der immer nur ein Radio-Button aktiv sein kann. Als Parameter dient lediglich ein sogenanntes Slice Literal mit Strings. Als Slice bezeichnet man in Go ein Array ohne feste Größe. Die einzelnen Strings spiegeln dabei die Beschriftungen der Optionsfelder wider, und man kann über sie auch auf die Radio-Buttons zugreifen.

Über die Methode »radioGroup.SetSelected(“md5”)« wird der Radio-Button mit der Beschriftung »md5« vorausgewählt. Später prüft die Anwendung bei dem Button, der die Berechnung der Prüfsumme anstößt, welches Optionsfeld derzeit ausgewählt ist, und verwendet abhängig davon die entsprechende Hash-Funktion.

Sehen wir uns als Nächstes die Schaltfläche detaillierter an, die die Prüfsummenberechnung auslöst. Wie gewohnt erzeugt »widget.NewButton()« in Zeile 62 einen Fyne-Schalter. An dieser Stelle prüft das Programm beim Betätigen des Buttons zuerst, ob im obersten Eingabefeld ein Pfad zu einer Datei steht. Falls ja, wird mit »os.Stat(entryFile.Text)« der Text im Eingabefeld als Parameter der Methode »Stat()« übergeben und ein Interface »FileInfo« erzeugt, das die Datei beschreibt.

Zeile 65 prüft dabei mit der Methode »os.IsNotExist(err)«, ob die Datei auch wirklich existiert. Falls nicht, zeigt das Programm einen entsprechenden Dialog mit der Info Datei existiert nicht an. Gibt es die Datei, wird noch einmal mit der Funktion »IsDir()« kontrolliert, ob es sich dabei eventuell um ein Verzeichnis handelt.

Die folgende Kaskade von If-Abfragen ermittelt, welches Optionsfeld gerade ausgewählt ist. Das geschieht mit dem Attribut »radioGroup.Selected«, das den zum Radio-Button gehörigen String enthält. Dementsprechend startet eine Go-Routine, die mit der selbst geschriebenen Funktion »genChecksum()« (dazu später mehr) sowie den beiden Parametern »entryFile.Text« und dem String des aktiven Radio-Buttons die Prüfsumme errechnet. Das Ergebnis landet via »entryResult.SetText()« im mittleren Eingabefeld.

Themes

Zu den schon erläuterten Anweisungen kommt in Zeile 16 noch der Befehl »a.Settings().SetTheme(theme.LightTheme())« hinzu. Durch die Methode »SetTheme()« können Sie das Programmfenster in verschiedenen Optiken erstrahlen lassen. Fyne bringt dafür zwei eingebaute Themes für ein helles Erscheinungsbild (Light Theme) und eine dunkle Farbgebung (Dark Theme) mit. Standardmäßig erscheint eine mit Fyne erstellte grafische Oberfläche im Dark Theme (Abbildung 2).

Abbildung 2: In der Vorgabe erscheint eine mit Fyne erstellte grafische Oberfläche im Dark Theme.

Abbildung 2: In der Vorgabe erscheint eine mit Fyne erstellte grafische Oberfläche im Dark Theme.

Da die GUI wie schon erwähnt in einer Dauerschleife verharrt, lässt sich das Theme mit »a.Settings().SetTheme(theme.LightTheme())« respektive »a.Settings().SetTheme(theme.DarkTheme())« im laufenden Betrieb umschalten. Zur Veranschaulichung gibt es in der Menüleiste der Beispielanwendung unter Ansicht ein Menü mit den Anweisungen zum Ändern des Themes.

Grafische Dialoge

Fyne bringt verschiedene Dialoge mit, etwa zur Dateiauswahl oder für Benutzerinformationen. So ruft beispielsweise Zeile 38 mit »dialog.ShowFileOpen()« einen Dateiauswahldialog auf (Abbildung 3). Diese Funktion benötigt als ersten Parameter eine Callback-Funktion. Der zweite Parameter bestimmt das Fyne-Fenster, in dem der Dialog erscheinen soll.

Abbildung 3: Die Funktion »dialog.ShowFileOpen()« zeigt einen einfachen Dateiauswahldialog an.

Abbildung 3: Die Funktion »dialog.ShowFileOpen()« zeigt einen einfachen Dateiauswahldialog an.

Der Callback-Funktion übergeben Sie ein Interface »URIReadCloser«, das den Datenstrom der ausgewählten Datei beschreibt. »read.URI().String()« (Zeile 46) liest die ausgewählte Datei dann aus. Zum Schluss müssen Sie noch mit »TrimPrefix()« das führende »file://« aus der String-Variablen des Dateipfads entfernen, damit das Programm das Resultat als Angabe weiterverarbeiten kann, von welcher Datei es eine Prüfsumme berechnen soll.

Ausgegliederte Funktion

Die Funktion genChecksum() (Listing 3), die die Prüfsummen berechnet, liegt in einer separaten Datei namens »genchecksum.go«. So lässt sich die Funktion in einer Go-Routine starten, einer Art separatem Thread, um die Berechnung der Prüfsumme von der GUI zu entkoppeln. Ansonsten würde die Berechnung die grafische Oberfläche einfrieren und sie wäre für die Dauer der Berechnung nicht mehr bedienbar.

Die Funktion »genChecksum()« nimmt zwei Parameter entgegen. Beim ersten handelt es sich um den Pfad zur Datei, deren Prüfsumme berechnet werden soll, beim zweiten um die anzuwendende Hash-Funktion. Die benötigten Algorithmen für unterschiedlichste Hash-Funktionen bringt die Standardbibliothek von Go im Package crypto mit.

Listing 3

genchecksum.go

package main
import (
  "crypto/md5"
  "crypto/sha1"
  "crypto/sha256"
  "crypto/sha512"
  "encoding/hex"
  "hash"
  "io"
  "log"
  "os"
)
func genChecksum(file, hashfunc string) string {
  var h hash.Hash
  f, err := os.Open(file)
  if err != nil {
    log.Fatal(err)
  }
  defer f.Close()
  switch hashfunc {
  case "md5":
    h = md5.New()
  case "sha1":
    h = sha1.New()
  case "sha256":
    h = sha256.New()
  case "sha512":
    h = sha512.New()
  }
  if _, err := io.Copy(h, f); err != nil {
    log.Fatal(err)
  }
  result := hex.EncodeToString(h.Sum(nil))
  return result
}

Kompilierung

Um das Programm zu kompilieren, wechseln Sie ins Verzeichnis mit den Quellcodedateien und setzen den gewohnten Zweisatz »go mod init checksum« und »go build« ab. Dadurch entsteht eine ausführbare Binärdatei namens »checksum«.

Fyne bringt auch einen Cross-Compiler namens Fyne-cross mit [4]. Damit lassen sich ausführbare Dateien für die meisten Betriebssysteme kompilieren. Fyne-cross setzt zur Installation (Listing 4, erste Zeile) ein bereits eingerichtetes Docker voraus. Anschließend genügt ein Aufruf des Cross-Compilers im Verzeichnis mit den Quellcode-Dateien, um eine ausführbare Datei für das gewünschte Betriebssystem zu kompilieren (zweite Zeile). Weiterführende Optionen zum Konfigurieren des Kompilats liefert die Online-Hilfe zu Fyne-cross (letzte Zeile).

Listing 4

Cross-Kompilierung

$ go get github.com/fyne-io/fyne-cross
$ ~/go/bin/fyne-cross Betriebssystem
$ fyne-cross Betriebssystem -help

Fazit

Alles in allem ist das Entwickeln einer grafischen Oberfläche mit dem GUI-Toolkit Fyne kein Hexenwerk. Wer zumindest solide Grundkenntnisse in Go hat oder vielleicht auch von einer anderen Programmiersprache kommt, der findet sich schnell zurecht. In der vorliegenden Version 2.0 fühlt das Framework sich schon sehr rund an. Wer sich noch weiter in die Materie einarbeiten möchte, sollte sich das Buch [5] des Fyne-Erfinders Andrew Williams näher ansehen. (jcb/jlu)

DIESEN ARTIKEL ALS PDF KAUFEN
EXPRESS-KAUF ALS PDFUmfang: 5 HeftseitenPreis €0,99
(inkl. 19% MwSt.)
LINUX-MAGAZIN KAUFEN
EINZELNE AUSGABE Print-Ausgaben Digitale Ausgaben
ABONNEMENTS Print-Abos Digitales Abo
TABLET & SMARTPHONE APPS Readly Logo
E-Mail Benachrichtigung
Benachrichtige mich zu:
0 Kommentare
Älteste
Neuste Beste Bewertung
Nach oben