Photo by Santoshi Guruju on Unsplash

Photo by Santoshi Guruju on Unsplash

Program GUIs in Go with Fyne

Fyne Work

Article from ADMIN 66/2021
By
In Go, which was originally developed for system programming, graphical user interfaces were not typically necessary. But a relatively new toolkit, Fyne, lets programmers build platform-independent GUIs for Go programs.

Creating a graphical user interface (GUI) with the Fyne framework [1] requires some preparation, including installing the GCC compiler and a graphics driver [2]. You will also need at least Go v1.12. The command

go get fyne.io/fyne/v2

downloads and sets up Fyne v2.

To get a first impression of the different Fyne widgets, you can take a look at a demo app [3] and its available controls. To download and launch this Fyne demo, enter

go get fyne.io/fyne/v2/cmd/fyne_demo
fyne_demo

in the terminal.

Basic Framework

A few lines of code are all it takes to create a spartan window in Fyne (Listing 1). The import statement brings in the required packages. To create an executable program, line 9 defines the main() function as the entry point. The app.New() method creates a new Fyne instance, and the a.NewWindow("<Title>") method specifies a title for the header of the newly created program window.

Listing 1

Basic Fyne Framework

01 package main
02
03 import (
04   "fyne.io/fyne/v2"
05   "fyne.io/fyne/v2/app"
06   "fyne.io/fyne/v2/widget"
07 )
08
09 func main() {
10   a := app.New()
11   w := a.NewWindow("<Title>")
12   w.Resize(fyne.NewSize(200, 200))
13   w.SetContent(widget.NewLabel("Hello World!"))
14   w.CenterOnScreen()
15   w.ShowAndRun()
16 }

As you might expect from the last four lines, the program makes the window appear with the defined width and height, writes a simple label with the obligatory Hello World! to the window, draws the Fyne window at the center of the screen, and displays the window. The application runs in a continuous loop waiting for user actions.

Checksum Calculator

To illustrate the capability of the Fyne framework, I look at a small tool for calculating checksums (Listing 2) and show in detail how to implement its graphical interface (Figure 1).

Listing 2

checksum.go

001 package main
002
003 import (
004   "fyne.io/fyne/v2"
005   "fyne.io/fyne/v2/app"
006   "fyne.io/fyne/v2/container"
007   "fyne.io/fyne/v2/dialog"
008   "fyne.io/fyne/v2/theme"
009   "fyne.io/fyne/v2/widget"
010   "os"
011   "strings"
012 )
013
014 func main() {
015   a := app.New()
016   a.Settings().SetTheme(theme.LightTheme())
017   w := a.NewWindow("checksum calculator")
018   w.Resize(fyne.NewSize(600, 800))
019
020   menuItemLight := fyne.NewMenuItem("Light Theme", func() {
021     a.Settings().SetTheme(theme.LightTheme())
022   })
023
024   menuItemDark := fyne.NewMenuItem("Dark Theme", func() {
025     a.Settings().SetTheme(theme.DarkTheme())
026   })
027
028   menuQuit := fyne.NewMenu("File")
029   menuTheme := fyne.NewMenu("View", menuItemLight, menuItemDark)
030
031   mainMenu := fyne.NewMainMenu(menuQuit, menuTheme)
032   w.SetMainMenu(mainMenu)
033   w.CenterOnScreen()
034
035   labelFile := widget.NewLabel("File:")
036   entryFile := widget.NewEntry()
037   buttonSelect := widget.NewButton("Select file", func() {
038     dialog.ShowFileOpen(func(read fyne.URIReadCloser, err error) {
039       if err != nil {
040         dialog.ShowError(err, w)
041         return
042       }
043       if read == nil {
044         return
045       }
046       selectedFile := read.URI().String()
047       filePath := strings.TrimPrefix(selectedFile, "file://")
048       entryFile.SetText(filePath)
049     }, w)
050   })
051   labelHash := widget.NewLabel("Hash:")
052
053   radioGroup := widget.NewRadioGroup([]string{"md5", "sha1", "sha256", "sha512"}, func(s string) {
054
055   })
056
057   radioGroup.SetSelected("md5")
058   labelResult := widget.NewLabel("Result:")
059   entryResult := widget.NewEntry()
060   entryResult.Disable()
061
062   buttonGenerate := widget.NewButton("Compute", func() {
063     if entryFile.Text != "" {
064       info, err := os.Stat(entryFile.Text)
065       if os.IsNotExist(err) {
066         dialog.ShowInformation("Info", "File does not exist!", w)
067       } else {
068         if info.IsDir() {
069           dialog.ShowInformation("Info", "File is a directory!", w)
070         } else {
071           if radioGroup.Selected == "md5" {
072             go func() {
073               checksum := genChecksum(entryFile.Text, "md5")
074               entryResult.SetText(checksum)
075             }()
076
077           } else if radioGroup.Selected == "sha1" {
078             go func() {
079               checksum := genChecksum(entryFile.Text, "sha1")
080               entryResult.SetText(checksum)
081             }()
082           } else if radioGroup.Selected == "sha256" {
083             go func() {
084               checksum := genChecksum(entryFile.Text, "sha256")
085               entryResult.SetText(checksum)
086             }()
087           } else if radioGroup.Selected == "sha512" {
088             go func() {
089               checksum := genChecksum(entryFile.Text, "sha512")
090               entryResult.SetText(checksum)
091             }()
092           }
093         }
094       }
095     } else {
096       dialog.ShowInformation("Info", "Please select a file!", w)
097     }
098   })
099
100   labelCompare := widget.NewLabel("Compare:")
101   entryCompare := widget.NewEntry()
102   buttonCompare := widget.NewButton("Compare", func() {
103     if entryResult.Text != "" && entryCompare.Text != "" {
104       if entryResult.Text == entryCompare.Text {
105         dialog.ShowInformation("Info", "Checksums identical!", w)
106       } else {
107         dialog.ShowInformation("Error", "Checksums not identical!", w)
108       }
109     } else {
110       dialog.ShowInformation("Info", "Please fill out both input boxes!", w)
111     }
112   })
113
114   c := container.NewVBox(labelFile, entryFile, buttonSelect, labelHash, radioGroup, labelResult, entryResult, buttonGenerate, labelCompare, entryCompare, buttonCompare)
115   w.SetContent(c)
116   w.ShowAndRun()
117 }
Figure 1: A small tool for computing checksums.

Lines 20 and 24 integrate a menubar into a Fyne application by defining two menu items. Each corresponds to a menu item in the list that appears when you click on an entry in the menubar. The first parameter specifies what the menu item is called, and the second defines an anonymous function that the program executes when the user clicks on the menu item.

These two menu items are passed as parameters to the fyne.NewMenu() method in line 29 to add the View menu item to the menubar. Consequently, the View menu includes the two menu items Light Theme and Dark Theme , which are used to toggle the theme on the fly (see also the "Themes" section below).

Line 31 creates the menubar with the two menus with fyne.NewMainMenu() and adds it to the Fyne window with w.SetMainMenu(). The first menu in the menubar of a Fyne window always automatically contains the Quit item to close the application, without the need to define a menu item.

Widgets

Defining widgets like buttons, labels, input fields, and so on follows the same pattern. At the very top of the sample application's GUI is a label that displays the text File: . Line 35 creates the labelFile variable and assigns it a Fyne label with widget.NewLabel("File:").

The widget.NewLabel() parameter specifies that the string is visible. To make it visible, you have to add the NewVBox() widget to the container at the end of the code in line 114. A container widget acts as a kind of layout manager – a concept familiar from other languages like Java.

Here, container.NewVBox(labelFile) creates a box layout that arranges all the elements of the graphical interface vertically one below the other. This is a variadic function that takes unlimited parameters, so any number of GUI elements can be added. The element from the first parameter is placed at the top of the GUI, and each subsequent element in the parameter list is placed below it in the GUI. The w.SetContent(c) statement adds the layout to the Fyne window.

Creating buttons works in a similar way. First, line 37 defines the buttonSelect variable, to which you assign a button with widget.NewButton(). The first value passed in is a string to label the button. The second parameter is again an anonymous function that the program executes as soon as a user presses the button. In this case, dialog.ShowFileOpen() calls a file selection dialog (see the "Graphical Dialogs" section).

Fyne's input fields turn out to be relatively simple. You create them in the usual way with widget.NewEntry() and reintegrate them into the container widget in line 114. The input fields come with several methods, including Disable(), which prevents editing by the user. Line 60 uses this capability for the input field that displays the calculated checksum.

Defining radio buttons is simple, as well. Line 53 uses widget.NewRadioGroup() to define a contiguous group of radio buttons, wherein only one radio button can be active at any given time. The only parameter is a slice literal with strings. In Go, a slice is an array without a fixed size. The individual strings reflect the labels of the radio buttons, and you can also use them to access the radio buttons.

The SetSelected() method in line 57 is used to preselect the radio button with the md5 label. When a button press later triggers the checksum calculation, the application checks which radio button is selected and uses the appropriate hash function accordingly.

Now I'll look in more detail at the button that triggers the checksum calculation. As usual, widget.NewButton() creates a Fyne button in line 62. When the button is pressed, the program first checks for a path to a file in the top input field. If a path exists, os.Stat(entryFile.Text) passes the text in the input field as a parameter to the Stat() method, which returns a Go FileInfo structure that describes the file.

Line 65 uses the os.IsNotExist(err) method to check whether the file really exists. If not, the program displays a matching dialog with the info File does not exist . If the file does exist, it checks again with the IsDir() function to see if it is possibly a directory.

The following cascade of if queries determines which radio button is currently selected with the radioGroup.Selected attribute, which contains the string associated with the radio button. Accordingly, a Go routine starts, with the DIY genChecksum() function (more on that later) and the parameters entryFile.Text and the string of the active radio button to calculate the checksum. The result ends up in the middle input field via entryResult.SetText().

Buy this article as PDF

Express-Checkout as PDF
Price $2.95
(incl. VAT)

Buy ADMIN Magazine

SINGLE ISSUES
 
SUBSCRIPTIONS
 
TABLET & SMARTPHONE APPS
Get it on Google Play

US / Canada

Get it on Google Play

UK / Australia

Related content

  • Program from one source to many apps with Flutter
    Developing apps in the past for Android, iOS, the web browser, and the desktop meant having to write different versions of the code; however, that's no longer the case thanks to Google's Flutter framework.
  • Manipulation detection with AFICK
    AFICK is a small, free tool that helps administrators detect attempts to manipulate documents and system files.
  • VAX emulation with OpenVMS
    Emulators beam a long-lost computer age into the present day with SIMH software, which brings the OpenVMS system back to life on an emulated VAX computer.
  • Building a low-powered NAS
    Build a network-attached storage box with Rockstor to manage your data.
  • Digital asset management
    If you want to make your photos, videos, audio files, and text documents available to others, your best bet is to manage this multimedia content with special software. Four programs can help you create, publish, and share these digital assets.
comments powered by Disqus