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".

Windows
md 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,

Preview of window
Preview of window

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:

App preview 1
App preview 1

Code Breakdown

Let's breakdown and understand the code now.

  1. 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.

  1. 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.
  1. 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 versa
  • celsius will hold the value of temperature in celsius
  • fahrenheit will hold the value of temperature in fahrenheit, and the
  • result 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.