Goでブラウザで動くGUIを作る


ブラウザの中で動くGUIをGoで作ってみる変態手法。

今回の記事はGoアドベントカレンダー2017 その3の19日目の記事です。

GTK+3のbroadwayバックエンド

GTK+3のbroadwayバックエンドはGTKのウィジェットをHTML5でレンダリング、 WebSocketでイベントハンドリングや描画をコントロールするためのデーモンです。

GTK+3でビルドされたアプリであればすべてこのbroadwayバックエンド上で動作させることができます。

gotk3

GTK+3のGoバインディングライブラリgotk3をつかってGUIアプリを作ってみます。

https://github.com/gotk3/gotk3

インストール

go get -u github.com/gotk3/gotk3/glib
go get -u github.com/gotk3/gotk3/gtk

サンプルコード「sample.go」

package main

import (
	"log"
	"os"

	"github.com/gotk3/gotk3/glib"
	"github.com/gotk3/gotk3/gtk"
)

// IDs to access the tree view columns by
const (
	COLUMN_VERSION = iota
	COLUMN_FEATURE
)

// Add a column to the tree view (during the initialization of the tree view)
func createColumn(title string, id int) *gtk.TreeViewColumn {
	cellRenderer, err := gtk.CellRendererTextNew()
	if err != nil {
		log.Fatal("Unable to create text cell renderer:", err)
	}

	column, err := gtk.TreeViewColumnNewWithAttribute(title, cellRenderer, "text", id)
	if err != nil {
		log.Fatal("Unable to create cell column:", err)
	}

	return column
}

// Creates a tree view and the list store that holds its data
func setupTreeView() (*gtk.TreeView, *gtk.ListStore) {
	treeView, err := gtk.TreeViewNew()
	if err != nil {
		log.Fatal("Unable to create tree view:", err)
	}

	treeView.AppendColumn(createColumn("Version", COLUMN_VERSION))
	treeView.AppendColumn(createColumn("Feature", COLUMN_FEATURE))

	// Creating a list store. This is what holds the data that will be shown on our tree view.
	listStore, err := gtk.ListStoreNew(glib.TYPE_STRING, glib.TYPE_STRING)
	if err != nil {
		log.Fatal("Unable to create list store:", err)
	}
	treeView.SetModel(listStore)

	return treeView, listStore
}

// Append a row to the list store for the tree view
func addRow(listStore *gtk.ListStore, version, feature string) {
	// Get an iterator for a new row at the end of the list store
	iter := listStore.Append()

	// Set the contents of the list store row that the iterator represents
	err := listStore.Set(iter,
		[]int{COLUMN_VERSION, COLUMN_FEATURE},
		[]interface{}{version, feature})

	if err != nil {
		log.Fatal("Unable to add row:", err)
	}
}

// Create and initialize the window
func setupWindow(title string) *gtk.Window {
	win, err := gtk.WindowNew(gtk.WINDOW_TOPLEVEL)
	if err != nil {
		log.Fatal("Unable to create window:", err)
	}
	win.SetTitle(title)
	if os.Getenv("GDK_BACKEND") == "broadway" {
		win.Connect("show", func() {
			//win.SetHideTitlebarWhenMaximized(true)
			win.Maximize()
		})
	}
	win.Connect("delete-event", func() bool {
		win.Emit("destroy")
		return false
	})
	win.Connect("destroy", func() {
		gtk.MainQuit()
	})
	win.SetPosition(gtk.WIN_POS_CENTER)
	win.SetDefaultSize(600, 300)
	return win
}

func main() {
	gtk.Init(nil)

	win := setupWindow("Go Feature Timeline")

	treeView, listStore := setupTreeView()
	win.Add(treeView)

	// Add some rows to the list store
	addRow(listStore, "r57", "Gofix command added for rewriting code for new APIs")
	addRow(listStore, "r60", "URL parsing moved to new \"url\" package")
	addRow(listStore, "go1.0", "Rune type introduced as alias for int32")
	addRow(listStore, "go1.1", "Race detector added to tools")
	addRow(listStore, "go1.2", "Limit for number of threads added")
	addRow(listStore, "go1.3", "Support for various BSD's, Plan 9 and Solaris")

	win.ShowAll()
	win.Maximize()
	gtk.Main()
}

実行結果

サンプル

いたって普通のGUIアプリ作成ですね。

broadwayバックエンドで起動する

broadwayバックエンドを有効にしたGTK+3を用意するのは骨が折れるので、 dockerを使って用意して見ました。

イメージを取得する

docker pull nobonobo/broadway-alpine

このイメージがどうやって作られるのかに興味がある人は以下のリポジトリを参照してください。 (Travis-CI経由で自動ビルドしています)

https://github.com/nobonobo/docker-broadway

前述のサンプルのそばに以下のDockerfileをおきます。

FROM nobonobo/broadway-alpine AS build
RUN apk add -U go git
RUN apk add musl-dev
RUN go get -u github.com/gotk3/gotk3/glib
RUN go get -u github.com/gotk3/gotk3/gtk
ADD sample.go /app/
WORKDIR /app
RUN go build .

FROM nobonobo/broadway-alpine
COPY --from=build /app/app /usr/bin/app
ENTRYPOINT sh -c "broadwayd :0 & /usr/bin/app"
EXPOSE 8080

ビルド

docker build --rm -t localhost/gtkapp1 .

実行

docker run -it --rm -p 8080:8080 localhost/gtkapp1

さあ、 http://localhost:8080 を開いてみよう。

サンプル

なんとブラウザの中でさっきと同じGUIが動くではありませんか?!

ここでアプリを終わらせると?

サンプル

broadwayの仕組み

ほとんど空のHTMLとbroadway.js、Websocket接続だけです。

サンプル

あとはWebSocketで描画コマンドやイベントを交換して jsで全てレンダリングしてるんですね。

まとめ

  • GTK+3をGoでやるならgotk3がよくできてる
  • GTK+3はbroadwayバックエンド機能を持ってる
  • broadwayバックエンド機能はX11の代わりにブラウザを使う
  • docker使うとややこしいものはコンテナに全て込められるよ
  • broadwayのポートだけ通せばブラウザでGUIを利用できるよ
  • broadwayのポートひとつがディスプレイセッションに対応しています
  • WebSocketはディスプレイセッションひとつに1接続だけで後勝ちです。
  • 複数タブや複数ユーザーから開かれるようなことをするにはちょっと細工が必要です。
  • (ユーザーの識別とかディスプレイプールとかポートプロキシとか)
  • 最大の難点はIMEサポートをどうすればいいのかが不透明な点でしょうか。

まあ、使いどころは難しいのですが、例えばRaspberryPiにGUIをbroadway経由でサーブさせれば リモートコントロール用コンソールとしては便利なんじゃないでしょうか。

comments powered by Disqus