お知らせ Fyne v2.0.0 がリリースされ、この記事の内容は少し古くなってしまったのでご注意ください (2021/01/25 追記)
皆さんは デスクトップアプリ を作ろう!となったとき、どんな UI ツールキットで作りますか? OS が決まっているならば SwiftUI, WPF あるいは GTK+ や Qt でしょうか。 色んな OS をサポートしたい場合は、Tcl/Tk, wxWidgets, JavaFX, Xamarin, Electron, React Native なども良いですね。
それでは Go 言語で 作りたいとなったときはどうしましょう? awesome-go[参考文献1] によると、以下のような選択肢がありそうです。
パッケージ | ツールキット/エンジン |
---|---|
github.com/mattn/go-gtk | GTK+ |
github.com/gotk3/gotk3 | GTK+ |
github.com/therecipe/qt | Qt |
github.com/andlabs/ui | libui |
github.com/asticode/go-astilectron | Electron |
github.com/maxence-charriere/go-app | ブラウザ系 |
github.com/sciter-sdk/go-sciter | ブラウザ系 |
github.com/dtylman/gowd | ブラウザ系 |
github.com/wailsapp/wails | ブラウザ系 |
github.com/zserge/webview | ブラウザ系 |
fyne.io/fyne | オリジナル |
こうやって分類すると、fyne というのはいったい何者なのだと思いませんか? そう思ったわたしは実際に試用してみて気に入り、社内で使う簡単な GUI アプリの開発に採用、ほんの一瞬で目的通りのアプリを実際に作れてしまいました。 作ったアプリはのちほど紹介しますが、まずは基本的な使い方と多くの人が遭遇するであろうハマりポイントをいくつか紹介したいと思います。
ただし、現状 Go の GUI 領域はまだまだ発展途上です。Fyne においてもデータバインディングやアニメーション機能が未提供だったりと GUI ツールキットとしては大穴があいているのも事実。限界を承知の上で、過度に期待せずに読んでいただけたらと思います。
はじめかた
既に Go 言語の開発環境は整っている前提で始めたいと思います。整っていない方は Getting Started してください。
では早速コードから。”Hello, world!” を表示するだけの最小構成のプログラムは以下のようになります。
package main
import (
"fyne.io/fyne/app"
"fyne.io/fyne/widget"
)
func main() {
a := app.New()
w := a.NewWindow("Hello")
w.SetContent(widget.NewLabel("Hello, world!"))
w.ShowAndRun()
}
デスクトップアプリの本体とは思えない短さですね!
go.mod
はこんな感じ。
module github.com/user/repo
go 1.14
require fyne.io/fyne v1.3.2
(バージョンは執筆時点の最新)
さあ、実行してみましょう!
go run .
表示されましたか?おめでとうございます👏
動きませんでしたか?問題ありません。計算通りです。続きを読んでください。
Windows の方
素の Windows で Go のツールチェインだけ入ってるという方、おそらくビルドできないはずです。 Fyne のビルドには C コンパイラが必要です。いくつか選択肢があります。
公式のチュートリアルでは以下が紹介されていました:
私が使っているのは mingw-w64 です。MSYS2 にもこれが入っています。
Chocolatey パッケージマネージャーをお使いの方は choco install mingw
で mingw-w64 が一発で入ります!
導入後は cc コマンドにパスが通っていることを確認してください。
macOS の方
Xcode とその command line tools が必要です。
command line tools の導入は xcode-select --install
でいけます。
導入済であっても、macOS をアップグレードした後とかに再度このコマンドを打たないといけないことがあります。
Linux の方
gcc に加え、X11 関係の開発用ヘッダファイル等が必要になります。
Debian/Ubuntu では apt install gcc xorg-dev libgl1-mesa-dev
, Fedora とかでは dnf install gcc libXcursor-devel libXrandr-devel mesa-libGL-devel libXi-devel libXinerama-devel
, Arch では pacman -S xorg-server-devel
です。
ライトテーマ
Fyne でアプリを起動して、ダークモードっぽい見た目になるのに驚いた人もいるかもしれません。 Fyne ではダークテーマがデフォルトになっています。ライトテーマもあります。環境変数を設定するだけで切り替わるので、早速試してみましょう!
Unix 系の方は、
FYNE_THEME=light go run .
Windows (コマンドプロンプト) の方は、
set FYNE_THEME=light
go run .
PowerShell の方は、
$Env:FYNE_THEME='light'
go run .
(クロスプラットフォームの手順の説明はたいへんだなぁ)
わずか環境変数1つで驚きの白さに!
レイアウト
基本的には、複数要素を垂直に並べる VBox
, 水平に並べる HBox
を組合せてレイアウトします。
先程のコードの w.SetContent
部を以下に書き換えてみましょう。
w.SetContent(widget.NewVBox(
widget.NewLabel("Label 1"),
widget.NewLabel("Label 2"),
))
縦に2つのラベルが並びました。
次は HBox
。
w.SetContent(widget.NewHBox(
widget.NewLabel("Label 1"),
widget.NewLabel("Label 2"),
))
横に2つのラベルが並びました。
入れ子もできます。
w.SetContent(widget.NewVBox(
widget.NewLabel("Label 1"),
widget.NewLabel("Label 2"),
widget.NewHBox(
widget.NewLabel("Label 3"),
widget.NewLabel("Label 4"),
),
))
自由自在!
また、タブやアコーディオン、スクロール対応などのいくつかのコンテナが用意されています。
w.SetContent(widget.NewTabContainer(
widget.NewTabItem("Tab1", widget.NewLabel("Label 1")),
widget.NewTabItem("Tab2", widget.NewLabel("Label 2")),
))
簡単ですね!
ウィジェット
UI ツールキットはウィジェットがあればあるほど嬉しいです。しかし・・・ Fyne はあまりありません。 なによりデータバインディングに未対応 (v2 で計画) なので、テーブル系がないのは致命的です。 とはいえ、いまあるものは簡単に使えます。また本記事では割愛しますが拡張もできるようになっています。
ありったけのウィジェットを並べてみました。
w.SetContent(widget.NewVBox(
widget.NewGroup("Button", widget.NewButton("Button", func() {})),
widget.NewGroup("Check", widget.NewCheck("Check", func(_ bool) {})),
widget.NewGroup("Entry", widget.NewEntry()),
widget.NewGroup("PasswordEntry", widget.NewPasswordEntry()),
widget.NewGroup("Form", widget.NewForm(widget.NewFormItem("FormItem", widget.NewEntry()))),
widget.NewGroup("HyperLink", widget.NewHyperlink("HyperLink", &url.URL{})),
widget.NewGroup("Icon", widget.NewIcon(theme.FyneLogo())),
widget.NewGroup("Label", widget.NewLabel("Label")),
widget.NewGroup("ProgressBar", widget.NewProgressBar()),
widget.NewGroup("Radio", widget.NewRadio([]string{"Option1", "Option2"}, func(_ string) {})),
widget.NewGroup("Select", widget.NewSelect([]string{"Select"}, func(_ string) {})),
widget.NewGroup("ToolBar", widget.NewToolbar(widget.NewToolbarAction(theme.DocumentSaveIcon(), func() {}))),
))
日本語表示
冒頭に張ったスクショには日本語が表示されていましたが、実は Fyne は日本語表示に対応していません! リポジトリを見ると Noto Sans が同梱されているのが確認できますが、日本語対応の CJK JP ではありません。
ですがもちろん解決策があります。テーマを切り替えたときと同じように環境変数でフォントを指定するだけです!
macOS:
FYNE_FONT=/System/Library/Fonts/AquaKana.ttc go run .
Windows 10 PowerShell:
$Env:FYNE_FONT='C:\Windows\Fonts\YuGothM.ttc'
go run .
ヒラギノ角ゴとか指定しようとしたら bad TTF version というエラーで落ちてしまいました。現状では指定できるフォントは限られているようです。
2020/07/12 08:34:21 Fyne error: font load error
2020/07/12 08:34:21 Cause: freetype: invalid TrueType format: bad TTF version
2020/07/12 08:34:21 At: go/pkg/mod/fyne.io/fyne@v1.3.2/internal/painter/font.go:20
panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x1 addr=0x1d4 pc=0x42a5c31]
もうひとつ問題があります。
Label
や HyperLink
ではスタイルを指定できる NewXxxWithStyle()
という関数があり、こんな感じで太字・斜体・等幅が指定できます。
widget.NewLabelWithStyle("らべる", fyne.TextAlignCenter, fyne.TextStyle{
Bold: true,
Italic: true,
Monospace: true,
})
しかしフォントの指定の口は環境変数1つのみです。Fyne のコードを見る限り、ファイル名に Regular
が含まれていると、その部分を Bold
, BoldItalic
, Italic
に変換するようです。このファイル名パターンに一致しない場合は TextStyle
を指定しても変化しません。この実装はちょっと不便です (しかも undocumented)。
また等幅フォントについては FYNE_FONT
環境変数ではなく FYNE_FONT_MONOSPACE
環境変数で指定するように実装されています。
実際にアプリケーションとしてパッケージングして配布することを考えても、環境変数で指定するというのはあまり都合の良い方式ではありません。 いまのところ現実的な解決策としては、
TextStyle
を使いたいときは自分でフォントを (名前を整えた上で) 成果物に同梱し、app.New()
より前に環境変数に設定TextStyle
を使わないのであれば、実行 OS から適切なシステムフォントを探し、app.New()
より前に環境変数に設定
とするのが良さそうです。
冒頭の例では、以下のようなコードになっています。ファイルが存在するか判定して、あれば環境変数に設定してから app.New()
します。
fontPath := "/System/Library/Fonts/AquaKana.ttc"
if _, err := os.Stat(fontPath); err == nil {
os.Setenv("FYNE_FONT", fontPath)
}
a := app.New()
w := a.NewWindow("Hello")
パッケージング
Fyne で作ったデスクトップアプリは、 go run
で動いたことからもわかるようにそのまま go build
したものを配布しても構いません。
しかし go build
だけだと Windows の場合は exe ファイルにアイコンがない上、コマンドプロンプト画面 (いわゆる DOS 窓) が出現してしまいます。そして Mac の場合は当然ながら .app
形式のアプリではなく生のバイナリファイルで、デスクトップアプリらしさがありません。Linux もデスクトップ環境が読み込めるパスでアイコンを置いておきたいものです。
というわけでパッケージングコマンドが提供されています。導入方法は簡単。
go get fyne.io/fyne/cmd/fyne
使い方もシンプルです。適当なアイコンを png ファイルで用意しておいて・・・
fyne package -icon icon.png
たったこれだけです。-icon
のほかクロスコンパイル用の -os <os>
があります。ただし前述のプラットフォーム事前条件を満たせないとクロスコンパイルは成功しません。
あとは -release
というリリースビルド用オプションもありますが、手元で試したところ true
でも false
でも同じバイナリが出力されました・・・。
fyne package
以外には fyne bundle <file|directory>
なんてサブコマンドもあります。
静的コンテンツ (ファイルやディレクトリ) を Go ソース化したものが生成され、ビルドするとバイナリに取り込まれるという仕組みです。
おまけ: Windows で exec.Command で DOS 窓が出る
Windows 限定の話ですが、 fyne package
でバイナリを作るとアプリ起動時にコマンドプロンプト画面 (いわゆる DOS 窓) が出ないよう対策してくれるということを先程紹介しました。
これは何も fyne
の魔法ではなく、内部で go build
を以下のように実行しているだけです:
go build -ldflags -H=windowsgui .
しかしこの副作用なのか、GUI から何らかの外部コマンドを実行するコードを fyne package
した exe を実行すると、この外部コマンドの DOS 窓が出現するという現象に遭遇しました。
ちょっと長いですが次のようなコードを動かしてみます。
package main
import (
"os/exec"
"fyne.io/fyne/app"
"fyne.io/fyne/widget"
)
func main() {
a := app.New()
w := a.NewWindow("ping")
label := widget.NewLabel("Not executed")
w.SetContent(widget.NewVBox(
widget.NewButton("Button", func() {
label.SetText("Executing...")
if err := exec.Command("ping", "-n", "3", "1.1.1.1").Run(); err != nil {
label.SetText("ERROR: " + err.Error())
return
}
label.SetText("Success")
}),
label,
))
w.ShowAndRun()
}
先程の fyne package
でも go build -ldflags -H=windowsgui .
でもどちらでも、確かに exe 起動時の DOS 窓は出なくなりますが、 “Button” を押すと DOS 窓が出現してしまいます。
色々と調べたところ、これを解決する方法がありました。 syscall.SysProcAttr
構造体の HideWindow
という Windows 専用項目でした[参考文献3]。
先程のコードに適用すると、以下のようになります。これで DOS 窓は出なくなりました。
cmd := exec.Command("ping", "-n", "3", "1.1.1.1")
cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true}
if err := cmd.Run(); err != nil {
label.SetText("ERROR: " + err.Error())
return
}
しかしここで新たな問題が発生します。HideWindow
は Windows 用の Go ツールチェインにしかないので、他の OS ではコンパイルできなくなるのです。
これを解決するにはもはや Go の Build Constraints 機能に頼るしかないでしょう。まず、Windows かそれ以外かで分岐する部分を別のソースファイルにし、そして関数化します。ファイル名は cmd_windows.go
とします (_windows
の部分が Build Constraints の規約)。
cmd_windows.go
:
package main
import (
"os/exec"
"syscall"
)
func prepareBackgroundCommand(cmd *exec.Cmd) {
cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true}
}
Windows 以外の場合は何もしなくて良いですが、関数は必要です。そこでもうひとつ cmd.go
を作成します。
cmd.go
:
// +build !windows
package main
import "os/exec"
func prepareBackgroundCommand(_ *exec.Cmd) {
// no-op
}
冒頭に書いた // +build !windows
は GOOS
が windows じゃない場合はこのソースをビルドしてねという指示です。
準備ができたら、exec.Cmd
に適用しましょう。
cmd := exec.Command("ping", "-n", "3", "1.1.1.1")
prepareBackgroundCommand(cmd)
if err := cmd.Run(); err != nil {
label.SetText("ERROR: " + err.Error())
return
}
これで Windows 以外の OS のビルドが壊れなくなります。めでたしめでたし。
まとめ
Fyne は GUI ツールキットとして基本的な機能を備え、それなりのルックスのデスクトップアプリケーションを簡単に開発できることが分かりました。 しかも、今回は触れませんでしたが Android や iOS で動くアプリも作れるようになっているなど、デスクトップだけでなくモバイルもカバーできる真のクロスプラットフォームなツールキットを目指しているのも心強いです。Fyne の開発は活発に進められており、今後の発展にも大いに期待できます。
一方で、現時点ではまだ機能が出揃っていないという点でフィットしなかったり、あるいは開発環境構築でつまづいたり、新たな問題に遭遇したりして挫折することもありえるツールキットでもあります。問題を解決するには Go や OS の一段深い理解が必要になることもあるでしょう。 万人にお勧めできるツールキットではない点も付け加えないといけません。
なお、本記事を作成する元ネタになったアプリは GitHub で公開しています。 ARPG (あーぷじー) という名前で、LAN 内で IP アドレスと MAC アドレスを相互に解決できる GUI アプリです。中身は単に OS のコマンドを叩いているだけですが、どうしても業務で GUI が欲しかったのでサクッと作りました。コードも短いですが、本記事で紹介したノウハウが詰まっています。
mikan/arpg: IP address / MAC address resolving GUI tool
何かの参考になれば幸いです。
Happy hacking!