Go の GUI ライブラリである Fyne について雑多にあれこれ紹介します.
Fyne は Go で GUI アプリケーションを作るためのライブラリです. シンプルでわかりやすい, 簡単に綺麗なデザインが作れる, モバイル等も含めたクロスプラットフォームといった点を売りとしています.
- 公式サイト
- GitHub リポジトリ
インストール 〜 Hello world までは他の方が書いたいくつかの記事があるのでそちらを参照してください.
https://qiita.com/KanikoroRerere/items/e2dcc44e625849526015 https://qiita.com/toromaru/items/58a3e93e67394fa1bb4f
基本的なウィジェットの扱い方等については, まずは以下が参考になると思います.
https://github.com/fyne-io/fyne/tree/master/cmd/fyne_demo
Fyne のメインリポジトリに含まれるデモアプリケーションです.
このデモをベースに各種ウィジェットの使い方, 動作をコードと合わせながら確認するのがまずは分かりやすいと思います.
https://github.com/fyne-io/examples
Fyne によって実装された簡単なアプリケーションのサンプル集です.
A Tour of Fyne というものも存在します.
現状ではそれほど内容が充実しているわけではない && ブラウザ上で動作確認するような機能はないため, それほど参考にはならないかもしれません...
実際に少し 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()
と同じように, Window
や Canvas
をダミーとして生成する API も存在します.
GUI アプリケーションでは, ボタンのアイコンとして画像を使用したい, ということがあります. アプリケーションを配布したい場合, 当然画像ファイルなどのリソースファイルもまとめてシングルバイナリとしたいと思います.
Fyne ではこれを行うための仕組みや便利なコマンドが提供されています.
fyne に関するいくつかの操作を実行できる fyne
コマンドが提供されています.
https://github.com/fyne-io/fyne/tree/master/cmd/fyne
go get でインストールします.
$ go get fyne.io/fyne/cmd/fyne
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 の組み込みテーマを利用する場合, 環境変数 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() }
// ...
このうち, TextFont
や TextBoldFont
などを適切に実装することでフォントの設定が可能です.
アイコンの場合と同様に, 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
の第一引数に与えると, 各環境で対応する動作を行った際に第二引数のコールバックが呼ばれることになります.
実際に簡単なアプリケーションを作って遊んでみたので紹介します.
GIF アニメーションを再生できる単純なビューアです. この記事で記載した, リソースファイルの組み込みやキーボードショートカットの実装などを含んでいます.
まだまだ発展途上のライブラリであり, 不足している機能等も多いですが, 興味があれば是非触ってみてください.