Native GTK+ apps written in Go

19 September 2017

James Ralphs

About Me

GUI libraries for Go

In short, we are still missing a fully-featured, pure Go, GUI library.
However there are some creative solutions.

GTK+

This talk will be about GTK+ 3 gotk3 bindings, and GTK+ 2 with the go-gtk bindings.

-- gotk3 examples
-- go-gtk demo page
-- PyGTK documentation.
A couple of sample projects:
-- github.com/jamesrr39/taskrunner-app (go-gtk)
-- github.com/jamesrr39/gtk3-image-gallery-demo-app (gotk3)

Installation & build

For Ubuntu:16.04:

Install C dependencies:

sudo apt update && sudo apt install -y libgtk-3-dev libcairo2-dev libglib2.0-dev
go get github.com/gotk3/gotk3/gtk
go install -tags gtk_3_18 github.com/gotk3/gotk3/gtk

Build for Ubuntu 16.04:

go build -tags gtk_3_18 -gcflags "-N -l" -o <output file> <entry file>

Install C dependencies:

sudo apt update && sudo apt install -y build-essential libgtk2.0-dev

Starting out

Error handling omitted so it would fit on one slide. Taken from the gotk3 README.

package main

import (
    "github.com/gotk3/gotk3/gtk"
)

func main() {
    gtk.Init(nil)

    win, _ := gtk.WindowNew(gtk.WINDOW_TOPLEVEL)
    win.SetTitle("Simple Example")
    win.Connect("destroy", func() {
        gtk.MainQuit()
    })

    label, _ := gtk.LabelNew("Hello, gotk3!")

    win.Add(label)
    win.SetDefaultSize(800, 600)
    win.ShowAll()
    gtk.Main()
}

Layout

padding := 5
hbox, _ := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, padding)
label, _ := gtk.LabelNew("my label")
hbox.PackStart(label, false, false, 0)
scrollWin, _ := gtk.ScrolledWindowNew(nil, nil)
scrollWin.SetPolicy(gtk.POLICY_ALWAYS, gtk.POLICY_ALWAYS)
scrollWin.Add(hbox)

Images

While both sets of bindings have image from file path methods, in Go we would like support for showing an image.Image.

pixbuf, _ := gdk.PixbufNew(gdk.COLORSPACE_RGB, true, 8, width, height)
pixelSlice := pixbuf.GetPixels() // underlying slice of bytes. Each pixel is 4 bytes (r, g, b, a).
log.Printf("red value of pixel (1, 0): %d\n", pixelSlice[4])

imageWidget, err := gtk.ImageNewFromPixbuf(pixbuf)

If you make changes to any widgets, remember to call myWidget.ShowAll()

You can find utilities to convert image.Image to gdk pixel buffers here: github.com/jamesrr39/go-gtk-extra

Events

myWidget.Connect("clicked", func() { ... }) // also works for tab + space
myWidget.Connect("size-allocate", func() { ... }) // on the widget size allocation changing
window.Connect("destroy", func() { ... })

Patterns

Card interface

type Card interface {
    Render() gtk.IWidget
}

Can be coupled with

func (w *AppWindow) RenderCard(card Card) {
    if nil != w.contentContainer {
        w.contentContainer.Destroy()
    }
    w.contentContainer, _ = gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 0)
    w.contentContainer.PackStart(card.Render(), true, true, 0)
    w.outerContainer.PackStart(w.contentContainer, true, true, 0)

    w.win.ShowAll()
}

where AppWindow is a struct containing outerContainer, contentContainer, the window and data access objects.

Asynchronous/goroutines

GTK+ is not thread-safe, but has APIs for handling UI updates by different goroutines.

glib.IdleAdd(f func(), args ...interface{})
glib.IdleAdd(myLabel.SetText, "hello") // for those that like interface{}
glib.IdleAdd(func() {
  loadingLabel.Destroy()
  imageWidgetContainer.PackStart(imageWidget, false, false, 0)
  imageWidgetContainer.ShowAll()
}) // for everyone else
// in `func main()`, before gtk.Init(nil)
glib.ThreadInit(nil)
gdk.ThreadsInit()
gdk.ThreadsEnter()

// later on, from your goroutine:
gdk.ThreadsEnter()
myLabel.SetText(calculationResult)
gdk.ThreadsLeave()

Lazy loading

Embedding static assets

github.com/jteeuwen/go-bindata is a package that turns binary assets into Go source code.
- Basically turns binary files into: var _fileX = []byte("\xff\ ... )
- Has worked well for me for web resources.
- Simple and effective way of keeping the "one binary" deployment.

Another GUI project with a different approach worth mentioning:

So, I want to write a Desktop Application in Go for my app! What should I use?

exec.Cmd("google-chrome", "--app=file:///path/to/resource.ext").Run()

(google-chrome binary name may vary by platform)

Thank you

James Ralphs