LoginSignup
13
11

More than 1 year has passed since last update.

GoによるFyneを使ったGUI開発

Last updated at Posted at 2020-12-23

GUI開発は、コマンドラインアプリケーションよりも作成が複雑なのは当然だ。
Fyneは、Goの優れた設計を使用して、GUIの構築を単純かつ迅速にするとのことで、一応は、Fyneを選んだ。
ゆくゆくは、Go謹製のshinyを使えるようになりたい。
んで、以下のプログラムをGUIに移行したい。

りそなIB用CSVファイルまとめGoプログラム
minecraftのセーブデータを管理するGoプログラム

と言うことで、参加する予定は無かったアドベントカレンダだが、枠があったので急遽投稿することにした。
(空きに入ったとは言え、遅延させた感じが嫌だな・・・投稿忘れに思われるよ・・・悲しい)

入手(環境構築)方法

以下のコマンで使えるようになる。

コンソール
go get fyne.io/fyne

上記コマンドで環境構築が終わる。
以下はデモ設定

コンソール
go get fyne.io/fyne/cmd/fyne_demo
fyne_demo

以下がデモの起動直後画面。

demo.jpg

大概のオブジェクトは試せる。
※私の環境で、直接 fyne_demo を実行した場合、コマンドエラーになった。そのため、直接叩いて起動したのだが、Path設定がうまくいっていないのかもしれない。

Hello World

まずは、初歩の初歩。

サンプルコード
package main

import (
    "fyne.io/fyne/app"
    "fyne.io/fyne/widget"
)

func main() {
    myApp := app.New()
    myWindow := myApp.NewWindow("Hello")    // ウィンドウタイトル

    myWindow.SetContent(widget.NewLabel("Hello World!"))    // 文字入りラベルをウィンドウコンテンツに配置

    myWindow.ShowAndRun()   // アプリケーションの実行(Show & Run)
    // ここ以降に処理を記述する場合、アプリケーションの実行が終わるまで実行されない。
}

1.jpg

Fyneのパッケージ構成

型の種類によってパッケージを分けている。

fyne.io/fyne

すべてのFyneコードに共通の基本定義の提供。
データ型とインターフェースを含む。

fyne.io/fyne/app

新しいアプリケーションを起動するAPIを提供する。
通常、必要なのはapp.New()のみ。

fyne.io/fyne/canvas

Fyne内のすべての描画APIを提供する。
完全なFyne toolkitは、これらの基本的なグラフィカルタイプで構成されている(単純構成なキャンバス)。

fyne.io/fyne/dialog

確認やエラーなどのダイアログウィンドウは、このパッケージによって処理される。

fyne.io/fyne/layout

さまざまなレイアウト実装を提供する。
コンテナが利用できる。

fyne.io/fyne/test

このパッケージにて、アプリケーションをより簡単にテストできる。

fyne.io/fyne/widget

このパッケージにて、さまざまなウィジェットコレクションが提供される。
ウィジェットとの相互作用もこのパッケージで提供されている。

その他

その他の特徴

よく分かっていないが、理解したと思い込んでいる箇所をかいつまんで述べる。

ウィジェット追加でのSetContent条件

上記のプログラムで、ウィンドウ(NewWindow)作成後、SetContentに追加できるウィジェットは1つのみに限定されている。
そのため、ラベル構造体を追加したことで、それ以上追加することが出来なくなっている。
そもそもSetContentの説明がGoの解説書にないのは、何故?

sample.go
func main() {
    c := 0

    a := app.New()
    w := a.NewWindow("Hello")

    label3 := widget.NewLabel("3つ目のウィジェット")
    label4 := widget.NewHBox(
        widget.NewLabel("4つ目のウィジェット"),
        widget.NewLabel("5つ目のウィジェット"),
    )
    w.SetContent(
        widget.NewVBox(
            widget.NewLabel("Hello Fyne!"),
            label3,
            widget.NewVBox(
                widget.NewLabel("2つ目のウィジェット"),
                widget.NewButton("click me", func() {
                    c++
                    label3.SetText("count: " + strconv.Itoa(c))
                }),
            ),
            label4,
        ),
    )

    w.ShowAndRun()

}

と言うことで、それを解決するのがコンテナの存在にある。
このコンテナを駆使することで、ウィジェットを複数配置でき、しかも、好きに配置換えすることもできる。

全て英語のマニュアルしか無いため、取りかかりにくいが、頑張ろうと思う。

日本語化

以下、Macの場合
export FYNE_FONT=/System/Library/Fonts/STHeiti\ Medium.ttc

読み込めない形式の場合は、以下のエラーが発生し、日本語部分が化ける。

ターミナル画面
$ export FYNE_FONT=/Users/chesscommands/Desktop/ヒラギノ明朝\ ProN.ttc
$ 
$ go run sample.go
2021/06/23 13:09:39 Fyne error:  font load error
2021/06/23 13:09:39   Cause: freetype: invalid TrueType format: bad TTF version
2021/06/23 13:09:39   At: /Users/chesscommands/go/src/fyne.io/fyne/internal/painter/font.go:20
$

本来の場所からデスクトップにフォントファイルを移動させたことは問題ないと思っているのだが・・・。

他の人も発言している通り、このままでは環境構築した場所でしか動かせないことになる。
それは困る。普通に配布できない。
であれば、片っ端からフォントファイルを埋め込む処理をプログラムに埋め込んでおけば解決するそうに思いませんか?

普通に成功した場合、以下のように当たり前の画面が当たり前として出てくる。

日本語フォント設定成功.jpg

sample.go
func main() {
    os.Setenv("FYNE_FONT", "/System/Library/Fonts/STHeiti Medium.ttc")  // あらかじめ、存在するフォントファイルを設定しておく。
    fyneFontPath := os.Getenv("FYNE_FONT")
    oneButton := widget.NewButton("", nil)
    if fyneFontPath == "" {
        oneButton = widget.NewButton("One", nil)    // どうしてもフォントファイルを設定できない場合、こちらが使われる。
    } else {
        oneButton = widget.NewButton("いち", nil) // フォントファイルを設定できた場合、日本語表記ができる。
    }

    myApp := app.New()
    myWindow := myApp.NewWindow("Hello")
    myWindow.SetContent(
        widget.NewHBox(
            widget.NewLabel("こんにちは世界"),   // フォントファイルを設定できない場合、ここは文字化けになる。
            oneButton,
        ),
    )

    myWindow.ShowAndRun()
}

上記のフォントファイルを設定失敗した場合、以下の画像になる。

日本語フォント設定失敗.jpg

そもそも設定しない処理の場合は、以下の画像になる。

日本語フォント設定なし.jpg

どのように対処するかはプログラムを組む労力次第かな。

クロスコンパイラ

OSごとにアイコンとメタデータが関連付けられており、それらの環境に必要な形式が求められる。
その複雑な環境に対して、fyneコマンドはtoolkitがサポートするOSであれば、対応できるようになっている。

"fyne package"を実行することで、各OSごとに異なるインストール形式のアプリケーションが生成されるのだろうか・・・(理解できず)。

WindowsOSの場合は、アイコン込みのexeファイルが作られる。
MacOSの場合は、appバンドルを生成する。
Linuxの場合は、tar.xzファイルを作る。

コンソール
go get fyne.io/fyne/cmd/fyne

go build
fyne package -icon mylogo.png

残念ながら私の環境ではできなかった(Pathを通す必要があるのだろう)。

上記サンプルコードの詳細

GUIが立ち上がるには、描画イベントを処理するランループ(Runloop・イベントループ)が必須になる。
Fyneでは、 Window.ShowAndRun() もしくは、 App.Run() で行う。
また、これは、main()関数の最後に呼び出す必要がある(サンプルコード参照)。
処理途中に呼び出した場合のそれ以降の処理は、ウィンドウが終了するまで実行されないことに注意すること。

このrunloopは、アプリケーション内で1回のみ実行可能なため、複数実行した場合エラーが発生する(1回の呼び出しは必須)。

サンプルコード
func main() {
    myApp := app.New()
    myWindow := myApp.NewWindow("Hello")
    myWindow.SetContent(widget.NewLabel("Hello"))

    myWindow.Show() // ウィンドウ表示
    myApp.Run() // ウィンドウ実行
    tidyUp()    // ウィンドウ終了後に実行される。
}

func tidyUp() {
    // GUIウィンドウ終了後に呼び出される関数。
    fmt.Println("Exited")
}

コード上で App.Quit() を実行することで、アプリケーションの終了が出来る。
しかし、ソースコードから直接終了指示を出すことで予期しない出来事の発生があるため、ユーザからの呼び出しで使う必要がある(ウィンドウ上部のいつもの×ボタンね)。
※ソースコード上で、開発者が任意に呼び出すのは避けるべき。

さらなる詳細

ウィンドウは、App.NewWindow()を使用して作成され、Show()関数にて表示する。

一部抜粋
    myApp := app.New()
    myWindow := myApp.NewWindow("Hello")    // ウィンドウ生成
        
        
        
    myWindow.Show() // ウィンドウ表示
    myApp.Run() // ウィンドウ実行

上記サンプルコード通り、ShowAndRun()関数にて、ShowとRunの両方を兼ねた実行が出来る。
このShowAndRun関数は、fyne.Windowのヘルパーメソッドのひとつ。

一部抜粋
    myApp := app.New()
    myWindow := myApp.NewWindow("Hello")    // ウィンドウ生成
        
        
        
    myWindow.ShowAndRun()   // ウィンドウ表示と実行

子ウィンドウを作る場合は、Show()関数のみを呼び出す必要がある(ゴルーチン関数呼び出し先でできる)。

ゴルーチン関数

ここ以降の説明は、goroutine(並行処理)の知識が要求される。

キャンバスについて

Fyneでは、Canvasはアプリケーションが描画される。
各ウィンドウには、Window.Canvas()で描画するが、使わないことも出来る。
そもそもFyneで描画できるものはすべて、CanvasObjectの一種になる。

サンプルコード
func main() {
    myApp := app.New()
    myWindow := myApp.NewWindow("Canvas") // ウィンドウタイトル
    myCanvas := myWindow.Canvas()         // キャンバス生成

    text := canvas.NewText("Text", color.Black) // キャンバスに黒文字準備
    text.TextStyle.Bold = true                  // 太字
    myCanvas.SetContent(text)                   // 上記テキスト描画
    go changeContent(myApp, myCanvas)           // ゴルーチン関数呼び出し

    myWindow.Resize(fyne.NewSize(200, 100))
    myWindow.ShowAndRun()
}

func changeContent(a fyne.App, c fyne.Canvas) {
    time.Sleep(time.Second * 2)

    c.SetContent(canvas.NewRectangle(color.Black)) // キャンバスを黒で塗りつぶす(長方形固定)。

    time.Sleep(time.Second * 2)
    c.SetContent(canvas.NewLine(color.Gray{0x66})) // キャンバスに線引き

    win := a.NewWindow("子ウィンドウ")
    win.SetContent(widget.NewLabel("5 seconds later"))
    win.Resize(fyne.NewSize(200, 200))
    circle := canvas.NewCircle(color.Black) // キャンバスに円形(黒で塗りつぶし)
    circle.StrokeWidth = 8
    circle.StrokeColor = color.RGBA{0x00, 0x00, 0xFF, 0x00} // 円形の線は青色
    win.SetContent(circle)
    win.Show() // 子ウィンドウ表示
    time.Sleep(time.Second * 2)
    win.Hide() // 子ウィンドウ非表示

    time.Sleep(time.Second * 2)
    c.SetContent(canvas.NewImageFromResource(theme.FyneLogo())) // ロゴ表示
}

child.jpg

再描画させることもできるようだ?
canvas.Refresh(circle)
しかし、使用前後で違いが分からなかった。

canvas.NewLine は、デフォルトでは左上から右下に線を引く。

子ウィンドウを表示させるまでの過程が合っているのか不安だ。
また、githubから導入した外部のパッケージは、補完が効かず、大変不便を強いられる。

MSCode.jpg

MSCode特有と思うのだが、解消して欲しい。

複数のキャンバスについて

上記は1つのみ利用した。
しかし、それでは実運用に耐えられない。
複数のキャンバスを使うには、コンテナを併用する。

サンプルコード
func main() {
    myApp := app.New()
    myWindow := myApp.NewWindow("Container")

    text1 := canvas.NewText("Hello", color.Black)   // テキストオブジェクトの作成
    text2 := canvas.NewText("There", color.Black)   // テキストオブジェクトの作成
    text2.Move(fyne.NewPos(20, 20))                 // 2つ目のテキストオブジェクトの移動
    text3 := canvas.NewText("World", color.Black)   // テキストオブジェクトの作成
    container := fyne.NewContainer(text1, text2, text3) // 3つのテキストオブジェクトをコンテナに配置。

    myWindow.SetContent(container)  // 上記コンテナをコンテンツとしてウィンドウに設定
    myWindow.ShowAndRun()   // ウィンドウの表示及び実行。
}

当然コンテナもキャンバスオブジェクトの一部であるため、キャンバスのコンテンツに設定できる。
手順

  1. テキストオブジェクトの作成
  2. それを3回行う。
  3. 2つ目のテキストオブジェクト(text2)を移動させる。
  4. 3つのテキストオブジェクトをコンテナに配置する。

と言う流れになる。

2.jpg

オブジェクトの移動を行っているのは、レイアウトセットを使っていないのが原因。
要は、移動させていないtext1とtext3がかぶってしまっている。
以下は、グリッドレイアウト。

container := fyne.NewContainerWithLayout(layout.NewGridLayout(2), text1, text2, text3)

以下は、グリッドラップレイアウト。

layout.NewGridWrapLayout(fyne.NewSize(50, 50))

以下は、ボーダーラインレイアウト。
4つの引数は配置場所。不要であればnilを指定する。

layout.NewBorderLayout(top, bottom, left, right)

fyne.Layoutは、コンテナ内のアイテムを整理するためのメソッドを実装する。
2列のグリッドレイアウトを使用するようにコンテナーを変更する。
ウィンドウのサイズ変更にて、テキストがレイアウトによって変更されることが確認できる。
また、text2の手動変更したコードが無視されていることも確認すること。

3.jpg

ウィジェット

fyne.Widgetは、特殊タイプのコンテナになる(追加のロジックが関連付けられたようだが、意味不明)。
(WidgetRendererとも呼ぶようだ)。
よく分かっていないが、ウィジェットもキャンバスオブジェクトの一部。

widget.Entryをウィンドウコンテンツに追加する(以下の例は1つのみ)。
複数のウィジェットを追加するには、コンテナではなく、ウィジェットのセットを利用する。

サンプルコード
func main() {
    myApp := app.New()
    myWindow := myApp.NewWindow("ウィジェット")

    myWindow.SetContent(widget.NewEntry())
    myWindow.ShowAndRun()
}

今回のウィジェットでは、文字入力できる箱だが、多バイトは入力できない。
※ダイアログでのディレクトリ名に日本語が含まれていた場合は表示されない(フォント指定で改善する?)。

4.jpg

しかし、ソースコードへのべた書きであれば表示が可能になっている。
今のところ困らないかな・・・。

一応は、環境変数である FYNE_FONT にフォントを指定することにより日本語表示も可能なようだ。

参考URL

大本(?)のfyneサイト
package fyne
Go 製 UI ツールキット Fyne で始めるクロスプラットフォーム GUI アプリケーション開発

バージョン

今回は1.4を使っているようだ。

以上。

13
11
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
13
11