GUI with Go using fltk Binding
Learn how to start building desktop graphical user interface with Go using `go-fltk` or the Fast Lightweight Toolkit binding.
July 16, 2024
Reading Time: 6 minutes
Hello, everyone!
This is my second time trying a GUI framework with the Go programming language. The first one I tried was the fyne
framework but I hit some issues when I am building significantly complex desktop apps.
In this entry, we are going to see how to start building graphical user interface apps with Go using the Fast Lightweight Toolkit binding.
Disclaimer
Please note that this is not a Go tutorial. So to complete and follow along well, make sure that you understand the basics of the Go programming language such as functions
and structs
.
What we will Build
In this example, we are going to build a desktop application for converting between Celcius and Fahrenheit temperatures. Using some fundamental controls and bindings with references in Go.
Starting a Project
First we start a project. Let's create a folder called "tempconvert".
md tempconvert
mkdir tempconvert
Now let's go into that directory and initialize a Go module.
cd tempconvert
go mod init github.com/alexiusacademia/tempconvert
You can change the module name "github.com/alexiusacademia/tempconvert" such as just "tempconvert".
Then let's open the folder in VS Code
code .
And you will see 1 file called go.mod
. And inside it
module github.com/alexiusacademia/tempconvert
go 1.22.3
Installing Dependency
Now, let's add the library to our dependencies
go get github.com/pwiecz/go-fltk
Your go.mod
file now should look something similar to this.
module github.com/alexiusacademia/tempconvert
go 1.22.3
require github.com/pwiecz/go-fltk v0.0.0-20240525043121-5313f8a5a643 // indirect
Creating a Window
Let us now create our main file. I'll name it main.go
and we will write our codes there.
package main
import (
fltk "github.com/pwiecz/go-fltk"
)
func main() {
fltk.InitStyles()
win := fltk.NewWindow(600, 300)
win.SetLabel("TempConvert")
win.End()
win.Show()
fltk.Run()
}
If we run the program using
go run .
We will get,
UI Design
For the UI components, I figure to have the following as initial design. You can improve if you have something better in mind. This is just a sample app that demonstrates how to build GUI with Go so this will not be expected to be polished.
- A label and an input for Celcius
- A label and an input for Fahrenheit
- A toggle switch that tells if the conversion is from celcius or fahrenheit
The scheme is that whenever the user make changes to the inputs such as the Celsius or Fahrenheit inputs, the result will update dynamically without pressing any other button.
Implementation
Paste the following code to yours:
package main
import (
"strconv"
fltk "github.com/pwiecz/go-fltk"
)
func main() {
win := fltk.NewWindow(500, 100)
win.SetLabel("Temperature Converter")
// Put the ui creation into a new function
createUI()
win.End()
win.Show()
fltk.Run()
}
func createUI() {
fltk.InitStyles()
fltk.SetScheme("gtk+")
c2f := true // Celsius to fahrenheight
celsius := 0.0
fahrenheit := 0.0
result := &Result{}
mainVert := fltk.NewPack(0, 0, 500, 100)
mainVert.SetSpacing(20)
// Row for the inputs
rowInputs := fltk.NewPack(0, 0, 500, 30)
rowInputs.SetType(fltk.HORIZONTAL)
// Controls for the celsius
lblCelsius := fltk.NewBox(fltk.NO_BOX, 0, 0, 70, 30, "Celsius")
rowInputs.Add(lblCelsius)
txtCelsius := fltk.NewFloatInput(0, 0, 150, 30)
txtCelsius.SetValue(strconv.FormatFloat(celsius, 'f', 2, 64))
txtCelsius.SetCallbackCondition(fltk.WhenChanged)
txtCelsius.SetCallback(func() {
v, err := strconv.ParseFloat(txtCelsius.Value(), 64)
if err == nil {
celsius = v
}
if c2f {
// Convert celsius to fahrenheit
fahrenheit = (celsius * 1.8) + 32
result.control.SetValue(strconv.FormatFloat(fahrenheit, 'f', 2, 64))
}
})
rowInputs.Add(txtCelsius)
// Controls for the fahrenheit
lblFahrenheit := fltk.NewBox(fltk.NO_BOX, 0, 0, 70, 30, "Fahrenheit")
rowInputs.Add(lblFahrenheit)
txtFahrenheit := fltk.NewFloatInput(0, 0, 150, 30)
txtFahrenheit.SetValue(strconv.FormatFloat(fahrenheit, 'f', 2, 64))
txtFahrenheit.SetCallbackCondition(fltk.WhenChanged)
txtFahrenheit.SetCallback(func() {
v, err := strconv.ParseFloat(txtFahrenheit.Value(), 64)
if err == nil {
fahrenheit = v
}
if !c2f {
celsius = (fahrenheit - 32) / 1.8
result.control.SetValue(strconv.FormatFloat(celsius, 'f', 2, 64))
}
})
rowInputs.Add(txtFahrenheit)
rowInputs.End()
mainVert.Add(rowInputs)
// Control for the toggle button
toggle := fltk.NewCheckButton(20, 0, 100, 30, "Celsius to Fahrenheit")
toggle.SetValue(c2f)
toggle.SetCallback(func() {
c2f = toggle.Value()
if c2f {
toggle.SetLabel("Celsius to Fahrenheit")
result.control = *txtFahrenheit
} else {
toggle.SetLabel("Fahrenheit to Celsius")
result.control = *txtCelsius
}
})
toggle.SetAlign(fltk.ALIGN_INSIDE | fltk.ALIGN_CENTER)
result.control = *txtFahrenheit
mainVert.Add(toggle)
mainVert.End()
}
type Result struct {
control fltk.FloatInput
}
If you have used other GUI frameworks before, you will notice that they are somewhat instantiating an app before creating the windows and controls. In go-fltk
, you just create the controls and windows directly and then run the app instance at the end with
fltk.Run()
The app should look like the one below:
Code Breakdown
Let's breakdown and understand the code now.
- First is we created a struct. I named it
Result
. This should hold a a reference to the output control. As you can see, it containes only one property,
type Result struct {
control fltk.FloatInput
}
The use of this is dynamic. The value of the property control
will depend on the option button's state. If the conversion is from celsius to fahrenheit, the control will be the input box for fahrenheit. And when it's the opposite, the control will be the input box for celcius.
This is very handy when dealing with references in Go and using it in building GUI apps, making the data more centralized.
- Next we created a function called
createUI
. This will handle the creation of the all the controls.
It is important to note that we didn't explicitly indicated that the window or win
variable will hold the controls. It was handled by the order of the code when calling the creation of controls.
- The window is initialized
- The
createUI
function is called creating all the controls - Ending the window creation and showing it.
- At the start of the createUI function, we have from lines 23-24:
fltk.InitStyles()
fltk.SetScheme("gtk+")
This is to initialize the styles and setting the UI scheme to GTK. There are various schemes to choose from as shown below:
- gtk+
- plastic
- oxy
- gleam
All have different looks on the UI.
Lines 26-30 are variables the will hold data for the app.
c2f
holds a boolean that tells the app if the conversion is from celsius to fahrenheit or vise versacelsius
will hold the value of temperature in celsiusfahrenheit
will hold the value of temperature in fahrenheit, and theresult
will hold the reference to the object we created.
I created a simple layout for the app. The first one is the main one which is a vertical layout defined by:
mainVert := fltk.NewPack(0, 0, 500, 100)
mainVert.SetSpacing(20)
at lines 31-32 and closed at line 100.
mainVert.End()
Next is a horizontal layout for the inputs on lines 35 - 77.
For the horizontal layout
rowInputs := fltk.NewPack(0, 0, 500, 30)
rowInputs.SetType(fltk.HORIZONTAL)
I used the Pack
object. The default behavior of pack is vertical so I didn't have to specify it on the mainVert
. But for the horizontal, we should specify it with the SetType
method.
We have a total of 4 widgets in this row.
- Label for celsius using
Box
widget - Input for celsius using
FloatInput
- And the same set for the fahrenheit
I used FloatInput
to restrict the input to only numeric. The user will not be allowed to enter non numeric characters.
If you read the whole code, you will see that's creating the ui is very straightforward. Except for the concept of app not being initialized first and also the way of adding the elements are ordered.
If you have any questions, please feel free to post it below.