Program GUIs in Go with Fyne
Fyne Work
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 }
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
(incl. VAT)