Skip to content

Instantly share code, notes, and snippets.

@lusingander
Last active April 18, 2020 23:13
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save lusingander/9c30e6b0ac268c6596bcb95b85943c4e to your computer and use it in GitHub Desktop.
Save lusingander/9c30e6b0ac268c6596bcb95b85943c4e to your computer and use it in GitHub Desktop.
Go + Fyne で GUI アプリケーション(Fyne についてのメモ)

Go の GUI ライブラリである Fyne について雑多にあれこれ紹介します.

Fyne とは

Fyne は Go で GUI アプリケーションを作るためのライブラリです. シンプルでわかりやすい, 簡単に綺麗なデザインが作れる, モバイル等も含めたクロスプラットフォームといった点を売りとしています.

基本

インストール 〜 Hello world までは他の方が書いたいくつかの記事があるのでそちらを参照してください.

https://qiita.com/KanikoroRerere/items/e2dcc44e625849526015 https://qiita.com/toromaru/items/58a3e93e67394fa1bb4f

基本的なウィジェットの扱い方等については, まずは以下が参考になると思います.

Fyne demo app

https://github.com/fyne-io/fyne/tree/master/cmd/fyne_demo

Fyne のメインリポジトリに含まれるデモアプリケーションです.

このデモをベースに各種ウィジェットの使い方, 動作をコードと合わせながら確認するのがまずは分かりやすいと思います.

examples

https://github.com/fyne-io/examples

Fyne によって実装された簡単なアプリケーションのサンプル集です.

A Tour of Fyne

A Tour of Fyne というものも存在します.

https://tour.fyne.io/

現状ではそれほど内容が充実しているわけではない && ブラウザ上で動作確認するような機能はないため, それほど参考にはならないかもしれません...

雑多な話題

実際に少し Fyne を触ってみて, 気になったところやハマったポイントなどをあれこれ記載します.

ユニットテスト

test パッケージに, いくつかのテストのための API が用意されています. これらを利用することで, (ある程度は) GUI の操作等もテストすることが可能です.

https://godoc.org/fyne.io/fyne/test

起動するとメインウインドウが開き, "Counter" をクリックすることでカウンターのウインドウが開きます. カウンターでは, ボタンをクリックするたびにカウント表示をインクリメントします.

コードは以下のような感じです. (全体のコードは lusingander/fyne-test-example にあります)

// メインウインドウの表示, 起動
func main() {
	mainApp := app.New()
	w := mainApp.NewWindow("Sample")
	w.Resize(fyne.NewSize(200, 30))

	w.SetContent(
		fyne.NewContainerWithLayout(
			layout.NewVBoxLayout(),
			widget.NewButton("Counter", func() { newCounterWindow(mainApp).Show() }),
		),
	)
	w.ShowAndRun()
}
// カウンターを構成するウィジェット, 現在のカウントを保持
type counter struct {
	*widget.Label
	*widget.Button
	count int
}

func newCounter() *counter {
	c := &counter{}
	c.Label = widget.NewLabel(fmt.Sprintf("Count: %d", c.count))
	c.Button = widget.NewButton("Click!", c.countUp)
	return c
}

func (c *counter) countUp() {
	c.count++
	c.Label.SetText(fmt.Sprintf("Count: %d", c.count))
}

// カウンターを描画するウインドウ
type counterWindow struct {
	fyne.Window
	*counter
}

func newCounterWindow(app fyne.App) *counterWindow {
	counter := newCounter()
	w := app.NewWindow("Counter")
	w.Resize(fyne.NewSize(200, 30))
	w.SetContent(
		fyne.NewContainerWithLayout(
			layout.NewVBoxLayout(),
			counter.Button, counter.Label,
		),
	)
	return &counterWindow{w, counter}
}

このカウンターに対して, 例えば以下のようなテストを実装することができます.

// ボタンをクリックしてカウントがインクリメントされることを確認
func TestCounter(t *testing.T) {
	c := newCounter()

    // ボタンに対するクリック操作
	test.Tap(c.Button)
	test.Tap(c.Button)
	test.Tap(c.Button)

    // 三回ボタンをクリックしたので現在のカウントは 3
	assert.Equal(t, 3, c.count)
}

// ウインドウ初期化時にカウンターが初期化され設定されることを確認
func TestNewCounterWindow(t *testing.T) {
    // test.NewApp() で画面描画によらないダミーの fyne.App を生成
	w := newCounterWindow(test.NewApp())

    // fyne.Window と counter が生成されている
	assert.NotNil(t, w.Window)
	assert.NotNil(t, w.counter)
}

上述の test.NewApp() と同じように, WindowCanvas をダミーとして生成する API も存在します.

リソースファイルの取り扱い

アイコン/画像

GUI アプリケーションでは, ボタンのアイコンとして画像を使用したい, ということがあります. アプリケーションを配布したい場合, 当然画像ファイルなどのリソースファイルもまとめてシングルバイナリとしたいと思います.

Fyne ではこれを行うための仕組みや便利なコマンドが提供されています.

fyne コマンド

fyne に関するいくつかの操作を実行できる fyne コマンドが提供されています.

https://github.com/fyne-io/fyne/tree/master/cmd/fyne

go get でインストールします.

$ go get fyne.io/fyne/cmd/fyne

bundle

fyne bundle コマンドを利用することで, 画像等のリソースファイルを Fyne で扱えるデータ(バイト列生データを持った構造体)に変換することができます.

$ fyne bundle foo.svg > bundle.go

bundle.go の中身は以下のようなファイルになっています.

package main

import "fyne.io/fyne"

var fooSvg = &fyne.StaticResource{
	StaticName: "foo.svg",
	StaticContent: []byte{
		... // 生データの数値列

使ってみる

あとは必要な箇所(fyne.Resource 型を要求する箇所)にこの StaticResource を渡してやればよいだけです.

たとえばボタンであれば以下のようになります.

widget.NewButtonWithIcon("button", fooSvg, func() { /* do something */ })

ボタンのアイコンとして SVG ファイルを設定する, というような場合, テーマに合わせて適切な色で描画したいということがあります. theme.NewThemedResource を利用することで, テーマカラーに合わせていい感じの描画を行ってくれます.

widget.NewButtonWithIcon("button", theme.NewThemedResource(fooSvg, nil), func() { /* do something */ })

具体的な例については, 記事の最後に記載しているサンプルのアプリケーションなどを参考にしてください.

フォント

Fyne は現在デフォルトでは日本語での表示ができません 😭

以下のような簡単なサンプルアプリケーションで確認してみます. ラベル, ボタンがあり, ボタンを押すとダイアログが表示されます.

func main() {
	a := app.New()
	a.Settings().SetTheme(theme.LightTheme())
	w := a.NewWindow("font")
	w.Resize(fyne.NewSize(300, 200))
	w.SetContent(
		fyne.NewContainerWithLayout(
			layout.NewVBoxLayout(),
			layout.NewSpacer(),
			widget.NewLabel("こんにちは、ファイン"),
			widget.NewLabel("これは日本語のラベルです"),
			widget.NewButton("これはボタンです", func() {
				dialog.ShowInformation("確認", "これはダイアログです", w)
			}),
			layout.NewSpacer(),
		),
	)
	w.ShowAndRun()
}

go run main.go してみると以下のようなことになります.

悲しすぎます. 全く OK じゃありません.

こちらについては Issue もよく上がっているのですが, いずれ公式に対応したいと思ってるよ, というステータスのようです.

よって日本語を扱いたい場合は一手間必要になります.

現状, 以下の二つの対応が公式に与えられています.

  • 環境変数 FYNE_FONT の利用
  • カスタムテーマを用意する

FYNE_FONT の利用

Fyne の組み込みテーマを利用する場合, 環境変数 FYNE_FONT にフォントファイルが指定されている場合にはそれをフォントとして利用することができます.

先ほどと同じコードで, FYNE_FONT を指定した上で実行してみます.

(サンプルのフォントとして M+ FONTS を利用しています. http://mplus-fonts.osdn.jp/)

$ FYNE_FONT=mplus-1c-regular.ttf go run main.go

今度は綺麗に表示されました! とはいえ, 自分の手元でちょっと試すだけならまだしも, アプリケーションを配布したいと思うとちょっとこのアプローチではいろいろと問題があります.

カスタムテーマの設定

Fyne は組み込みで Dark/Light テーマを用意しており, これを利用するだけで綺麗なアプリケーションが作れるよ, というのを一つのウリとしています. しかし, テーマは自分で設定して利用することももちろん可能です. このテーマで設定できる項目の中にフォントが含まれているため, 独自のテーマを定義することで任意のフォントを利用することが可能になります.

テーマは以下のように Theme インターフェースを実装することで利用可能となります.

type myTheme struct{}

func (myTheme) TextFont() fyne.Resource { return resourceMplus1cRegularTtf } // フォントを設定

// その他の様々な設定
func (myTheme) BackgroundColor() color.Color      { return theme.LightTheme().BackgroundColor() }
func (myTheme) ButtonColor() color.Color          { return theme.LightTheme().ButtonColor() }
func (myTheme) DisabledButtonColor() color.Color  { return theme.LightTheme().DisabledButtonColor() }
// ... 

このうち, TextFontTextBoldFont などを適切に実装することでフォントの設定が可能です. アイコンの場合と同様に, fyne bundle コマンドによってフォントファイルをバンドルし, それを利用する, ということになります.

$ fyne bundle mplus-1c-regular.ttf > bundle.go 

theme.go, bundle.go が用意できたので, 先ほど main.go で指定していた a.Settings().SetTheme(theme.LightTheme())a.Settings().SetTheme(&myTheme{}) に修正した上で実行してみます.

$ go run *.go

この画像ではダイアログのタイトルがまだ正しく表示されていないですが, これはダイアログが Bold フォントを使用するようになっているためです. 前述の例では Regular のみ指定設定しているためこのようになっています.

コード全体は lusingander/fyne-font-example にあります.

キーボード入力/ショートカット

fyne.Canvas に以下の関数が定義されています.

  • SetOnTypedRune(func(rune))
  • SetOnTypedKey(func(*KeyEvent))
  • AddShortcut(shortcut Shortcut, handler func(shortcut Shortcut))

なので, ウインドウに対してキーボード操作を与えたい場合は以下のような記述をすることになります.

func main() {
	w := app.New().NewWindow("title")

	w.Canvas().SetOnTypedKey(v.handleKeys)
	w.Canvas().SetOnTypedRune(v.handleRune)

    w.ShowAndRun()
}

func handleKeys(e *fyne.KeyEvent) {
	switch e.Name {
	case fyne.KeyUp:
		// do something
	case fyne.KeyDown:
		// do something
        // ...
	}
}

func handleRune(r rune) {
	switch r {
	case '+':
		// do something
	case '-':
		// do something
        // ...
	}
}

また, ショートカットキーを割り当てたい場合は以下のようになります.

func main() {
	w := app.New().NewWindow("title")

    // Ctrl+O のショートカットキーを定義
	w.Canvas().AddShortcut(
		&desktop.CustomShortcut{
            KeyName: fyne.KeyO,
            Modifier: desktop.ControlModifier,
        },
		func(s fyne.Shortcut) {
            // do something
        },
	)

    w.ShowAndRun()
}

コードの通り, 例えば Ctrl+O のようなショートカットキーを定義したい場合, desktop パケージを利用する必要があります.

Fyne はモバイルも含めたクロスプラットフォームの GUI ライブラリとして作られているため, デスクトップアプリケーション特有の処理はこのように分離されています.

fyne パッケージにはデフォルトのショートカット定義として以下が存在しますが, これらはそれぞれモバイル環境でも対応する動作が定義されているものになります.

  • ShortcutPaste
  • ShortcutCopy
  • ShortcutCut
  • ShortcutSelectAll

https://github.com/fyne-io/fyne/blob/master/shortcut.go

これらを AddShortcut の第一引数に与えると, 各環境で対応する動作を行った際に第二引数のコールバックが呼ばれることになります.

遊んでみた

実際に簡単なアプリケーションを作って遊んでみたので紹介します.

go-gif-viewer

lusingander/go-gif-viewer

GIF アニメーションを再生できる単純なビューアです. この記事で記載した, リソースファイルの組み込みやキーボードショートカットの実装などを含んでいます.

まとめ

まだまだ発展途上のライブラリであり, 不足している機能等も多いですが, 興味があれば是非触ってみてください.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment