fyneでいろいろやってみよー
Golangのデスクトップアプリを作るにあたって、
fyneというライブラリを使ってみることにした。
Webサイトの経験はあってもクライアント開発の経験はない。
アプリを作るときもWebレンダリングに頼ったCordovaでの開発が主やった。
ってことで、Webでできることをできるのかーというかんじでやっていく。
2週間くらいかけて試すことになるんじゃないかなーって印象
参考
とりあえずこれやっとけってやつ
スタンダードなWidgetの一覧
とりあえず視覚的にどんなのがあるのか見れるのがいい
フォントとか画像とかの埋め込み方が書かれてる
その名の通りexample
こんなことできるんだよ~の色が強くて、実用的なのはない、かも
中国語はわからんけど、こんなことができるよってのが雰囲気でわかる
Spacerが載ってたページ
Layoutについてが書かれてる
ウィンドウを開いて文字列を出力する
https://gyazo.com/782ec1e5f6742b421269505ac7505c72
やってること
ウィンドウの生成とタイトルの設定
Vボックスを作って、文字列を表示
ボックスはなくても結果は同じ
デフォルトのウィンドウのサイズを256, 256に設定
ウィンドウ表示
日本語フォントを追加して日本語を表示する
https://gyazo.com/28d89afd4440902d231d87f13a7c3807
やってること
日本語フォントの追加
fyne bundleコマンドでttfファイルをgoに変換
$ fyne bundle -package bundle -prefix Resource ./bundle/resorces/misaki_gothic_2nd.ttf > ./bundle/misaki2nd.go
-package : package名
-prefix : 変数の接頭辞
先頭を全角にしないと他のパッケージから見れないから明示的に指定してる
日本語フォントを使っているテーマを追加
日本語の文字を表示する
フォントをそのままバイト配列にするのでおっきい.goファイルができあがる。
IDEでうっかり開くとプログレスサークルがぐーるぐる
素材は軽量フォントということでこちらからお借りしてます
終了ボタンを追加する
https://gyazo.com/b5f252c95282997df1cf2558fd741e7f
やったこと
VBoxで縦並びというのは変わらない
ボタンをBox内に追加
ボタンの動作に app.Quit() を追加
オンマウスで色が変わるってのが勝手についてるすごい
アニメーションを変えるとかできるのかはのちほど
入力欄を追加してみる
https://gyazo.com/2f484a9869a9303e79e1e6df400dfe61
やったこと
平文入力のためのEntryを追加
パスワード入力のためのPasswordEntryを追加
複数行入力のためのMultiLineEntryを追加
動かないけどログインボタンを追加し、標準出力に押下されたことを出してみた
Formで見出しをつけてみた
関係ないけどやったこと
ちょっと画面がせまくなってきたのでサイズを512x512に変更
フォントサイズをちょっと大きくするためthemeを変更
軽量フォントじゃ見づらくなってきた気がするなー
あと、IMEが絶対にウィンドウの外にあるのが気になる
IMEが入力欄のところに出る仕組みがどういうものか分かってないけど、解決したほうがいい気がするなー
Webっぽいパーツをならべてみる
https://gyazo.com/83280ce2cd5daaf216e80283f76429f0
やったこと
ラジオボタンの設置
ついでに横並びにしてみた
チェックボックスの設置
こっちの横並びはHBox
セレクトボックス
関係ないけどやったこと
可読性悪かったからboxごとに変数にいれた
デフォルトでradioボタンのチェックが外せたり、グループとして作ってくれたり、イベントが簡単に拾えるのは本当に助かる。
ページ遷移っぽい挙動
https://gyazo.com/4397796eb038f186e7a5650d6c508005
やったこと
ページという名のfyne.CanvasObjectを作る関数を追加
ボタンのアクションとしてページ遷移を設定
layout.Spacerで中央寄せっぽく表示
spacerおもしろい。幅内で限界まで広がるっていう特徴があるっぽくて細かい表現はできないまでも、基本的な表現では困らない
注意
どうも、一度表示したCanvasObjectは再利用できないみたい。
なので表示のたびに一式作る必要がある。もしかしたらRefreshとかでいけるかもなので試してみる必要はある
Fluxみたいなの導入して、アクションによってViewが変わるような仕組みは構築したほうがいいかも
ダイアログ各種
https://gyazo.com/7ad7a6b6a606fa0bbf58ff165d2bf6dd
やったこと
確認ダイアログ
ファイル選択ダイアログ
テキストファイルなら標準出力に読みだしてみた
エラーダイアログ
通知ダイアログ
プログレスダイアログ
無期限ダイアログしかうまく動かせなかった
複数ウィンドウひらく
https://gyazo.com/a7386e23b27f0488d3f8945599284ba2
やったこと
新しいウィンドウを生成してShow
サブウィンドウは閉じるボタンがあって、自分自身を閉じれる
親ウィンドウは全てのサブウィンドウを閉じてから自身を閉じる
アプリケーションを終了してまとめてウィンドウを消す
注意
RunとShowAndRunはmainのgoroutineでしかできないらしい。
Runではループしてイベントを待機してくれるやつで、最後のウィンドウが閉じられたときに終了するっぽい。
ちなみにアプリケーションをQuitしたらすぐにループから抜けて終了する。
分割とスクロール
https://gyazo.com/593a4f5d751d34d2d8698b0491938d8e
やったこと
NewHSplitContainerで左右に分割
NewVSplitContainerを使えば上下に分割できる
分割されたそれぞれにNewVScrollContainerで縦スクロールできるようにした
デスクトップ通知
https://gyazo.com/8fa5460287211009a8e3235a5bfc0633
やったこと
ボタンをおしたら通知をだす
注意
Windowsで動かそうとすると、PowerShellにUTF8で吐かれたスクリプトが渡されて、
マルチバイト文字列が化ける。結果、実行できない問題があった。
なので仕方なく、osがwindowsのときはsjisに変換した文字列を吐き出すようにした。
code:main.go
widget.NewButton("ですくとっぷつうち", func() {
title := "Fyneの練習"
content := "通知のテスト"
if runtime.GOOS == "windows" {
enc := japanese.ShiftJIS.NewEncoder()
var err error
if title, _, err = transform.String(enc, title); err != nil {
log.Println(err)
return
}
if content, _, err = transform.String(enc, content); err != nil {
log.Println(err)
return
}
}
fyne.CurrentApp().SendNotification(fyne.NewNotification(title, content))
})
もちろん、windowsでも日本以外のエディションでは動かない。
どうにかしてくれー
必死こいてissueたてた
キータイプ
https://gyazo.com/2a39bf1b954544c4d5281f2507c42ab3
やったこと
ウィンドウでキーイベントを拾うようにした
拾ったイベントからキーの名前をLabelとして表示
多すぎると描画負荷がありそうやったから100件までにした
ラベルじゃない文字列表示
https://gyazo.com/175720d6391f714c990e800eddd3c968
やったこと
文字列の表示をcanvas.Textを使う
一定間隔で色を変えてRefresh()
2つのTextを1文字ずつ変えて色が変わっていくようにしてみた
無理やと思って試したら、widgetの中にcanvas入れれる。
テキスト表示に関してはcanvas.Textのほうがいいかも
最後の行の色が変わっていくとずれているように見えるのは、
左の文字と右の文字の間に空白がないからだと思う。
最初は文字がちいさくなってる?!とか思ってあせった
ここまでの知見でタイピングゲームくらいは作れそうな気がする。
タイピングゲーム
https://gyazo.com/0ae6de33d2c966b959eec351397ccdd9
やったこと
新しくやったことは特になし
簡単やけど、ちょっとした達成感
ただ状態の管理とかイベントの管理がおろそかやから、その辺は見直さなあかん
Rasterをつかった描画
https://gyazo.com/191edd2da631577e76d6ff505bd3ed8f
やったこと
セルの位置がx, yで得られるので、25pxごとに色が変わるようにした
1秒ごとにその結果が反転するようにした
基本的な描画はWidgetで簡単に作ればいいけど、
コアな部分はcanvasで自分で描いたりしないといけなさそうやなーという印象。
にしてもこの色合いは目が痛くなるな
カスタムレイアウト
https://gyazo.com/acb39f49ccd8ba734055f9debb82a9a2
やったこと
Layout interfaceを満たすdiagonalLayout構造体を作成
Layout()では引数のオブジェくトをどこに設置するのかを決定
第二引数にキャンバスの大きさがもらえるので、そこから計算して右上にずらしていった
MinSize()では引数のオブジェクトを含めた場合の最低サイズを計算
斜めに並べるから、縦の幅と横の幅を全部足していっただけ
複雑な見た目を作るには必須な気がする
カスタムウィジェット
カスタムウィジェットには大きく2つの構造体があり、それぞれがfyneのinterfaceを実装することになる。
一つ目がwidget.Widgetで、ウィジェットの状態を管理するための構造体。
widget.Buttonでいうと、テキストやスタイル、アイコンの他に、
ボタンの有効無効や、ボタンを押したときのアクション、ホバーとかを持ってる。
ホバーとかに関しては、HoverableとかTappableってinterfaceがあるから、そっちの実装もある。
widget.BaseWidgetってのがあって、それを埋め込むことで必要なメソッドの大半がそろう。
internal.cacheに依存する部分があったりして実装不可能な部分もあったりするけど、
widget.BaseWidgetでややこしいの気にしないでも大丈夫になる。
二つ目がwidget.WidgetRendererで、ウィジェットの描画を行なうための構造体。
一応、widget.BaseRendererっていう、wdiget.BaseWidget的なのがあるけどinternalに入っちゃってるから、
結局全部の処理を書いてあげないとダメ。
Layout(Size)
カスタムレイアウトでもやったあれ。どこに何を置くのかってのを書いていく
MinSize() Size
ウィジェットの最小サイズ
基本的に計算で出す
Refresh()
描画とかテーマが変更されたときにトリガーされるやつ
いわゆる再描画
BackgroundColor() color.Color
背景色
Objects() []CanvasObject
CanvasObjectのsliceを返す
Destroy()
このレンダラーが要らなくなった時に破棄するやつ
ウィジェットの出し入れがおおかったらメモリクリアのためにあったほうがいいのかも?よくわからん
とはいえ、下3つはとりあえず固定で行ける気がする。
code:widget.go
func (r *customWidgetRenderer) BackgroundColor() color.Color { return theme.BackgroundColor() }
func (r *customWidgetRenderer) Objects() []fyne.CanvasObject { return r.objects }
func (r *customWidgetRenderer) Destroy() {}
必要になったら書いてあげるくらいのつもりでいとければOKそう。
https://gyazo.com/2747306242caef0ae56756587e928686
テキストにpaddingをちょっとつけるだけのウィジェットやけど、コード量はそこそこ
状態管理のWidget側
code:customwidget.go
// newCustomWidget - 新しいカスタムウィジェットの生成
func newCustomWidget(text string) fyne.CanvasObject {
wid := &customWidget{text: text}
wid.ExtendBaseWidget(wid) // BaseWidgetに自分自身をいれる。BaseWidgetの処理で必要
return wid
}
// customWidget - Widget interfaceを実装する独自ウィジェット
// 他のwidgetと同じく、BaseWidgetを埋め込んでる
type customWidget struct {
widget.BaseWidget
text string // テキストはcanvasで作らず、状態の一つということで文字列で持っていく
}
// CreateRenderer - ウィジェットのレンダラーを作る
// ウィジェットの描画をする構造体を返すやつ
func (w *customWidget) CreateRenderer() fyne.WidgetRenderer {
text := canvas.NewText(w.text, theme.TextColor()) // レンダラーを作るときに絶対にNewする
return &customWidgetRenderer{
customWidget: w,
text: text,
objects: []fyne.CanvasObject{text},
}
}
描画管理のWidgetRenderer側。
code:customwidget.go
type customWidgetRenderer struct {
customWidget *customWidget
text *canvas.Text
objects []fyne.CanvasObject
}
// Layout - widget内に何をどのようにレイアウトするか
func (r *customWidgetRenderer) Layout(fyne.Size) {
r.text.Move(fyne.NewPos(theme.Padding()*2, theme.Padding()*1)) // paddingの分だけ内側にずらす
r.text.Resize(r.text.MinSize())
}
// MinSize - ウィジェットの最小サイズ
func (r *customWidgetRenderer) MinSize() fyne.Size {
size := r.text.MinSize()
size = size.Add(fyne.NewSize(theme.Padding()*4, theme.Padding()*2)) // テキスト+上下左右のpadding
return size
}
// Refresh - 再描画
func (r *customWidgetRenderer) Refresh() {
r.Layout(r.customWidget.MinSize())
canvas.Refresh(r.customWidget)
}
func (r *customWidgetRenderer) BackgroundColor() color.Color { return theme.BackgroundColor() }
func (r *customWidgetRenderer) Objects() []fyne.CanvasObject { return r.objects }
func (r *customWidgetRenderer) Destroy() {}
カスタムイベントウィジェット
イベントっぽいのをパーっと出してみた。
Hoverable
Draggable
Mouseable
Tappable
SecondaryTappable
DoubleTappable
Disableable
Scrollable
Focusable
Shortcutable
Cursorable
Keyable
他にもあるかもやし、一部イベントじゃないかもやけど、とりあえず。
そのうち分かりやすいHoverableを試してみる。
https://gyazo.com/d9e2f64f4156873911bc04657e969c67
前提として、カスタムウィジェットは作れないといけない。
code:mouse.go
// Hoverable is used when a canvas object wishes to know if a pointer device moves over it.
type Hoverable interface {
MouseIn(*MouseEvent)
MouseOut()
MouseMoved(*MouseEvent)
}
Hoberableには3つのメソッドが必要。
ただ乗ったタイミングと降りたタイミングだけ分かればいいので、処理が必要なのは上2つ。
code:customwidget.go
func (w *customWidget) MouseIn(*desktop.MouseEvent) {
w.hover = true
w.Refresh()
}
func (w *customWidget) MouseOut() {
w.hover = false
w.Refresh()
}
func (w *customWidget) MouseMoved(*desktop.MouseEvent) {}
func (r *customWidgetRenderer) BackgroundColor() color.Color {
if r.customWidget.hover {
return color.White
} else {
return color.Black
}
}
customWidgetにはhoverというBooleanのフィールドがあって、
そこをtrueかfalseにして再描画するだけ。
んで、RendererがcustomWidgetの状態をみて背景色を変えてる。
Layoutをかえたり、MinSizeを変えたりすることでOnMouseで膨らむとか、アイコンが表示されるとか、色んな事ができるはず。
あと楽しいDraggableもやっとく。
https://gyazo.com/765ede966432665d6d0a124563883660
code:canvasobject.go
// Draggable indicates that a CanvasObject can be dragged.
// This is used for any item that the user has indicated should be moved across the screen.
type Draggable interface {
Dragged(*DragEvent)
DragEnd()
}
code:customwidget.go
func (w *customWidget) Dragged(event *fyne.DragEvent) {
w.posY += event.DraggedY
w.Refresh()
}
func (w *customWidget) DragEnd() {
w.Refresh()
}
func (r *customWidgetRenderer) Layout(fyne.Size) {
r.customWidget.Move(fyne.NewPos(r.customWidget.posX, r.customWidget.posY))
r.text.Move(fyne.NewPos(theme.Padding()*2, theme.Padding()*1))
r.text.Resize(r.text.MinSize())
}
Y方向にどれくらい動かしたかをとって、それを位置に反映。んで再描画。
描画時にcustomWidgetのXとYの位置を取得して描画する。
基本的にXは変わらないようにしてるから0で、Yが動いた分だけ動くって仕組み。
更新履歴
2020/07/23 内容が増えすぎたのでこの記事はここまで
2020/07/23 カスタムウィジェットを試した
2020/07/17 引き続き色んなWidgetとCanvasを試し中
2020/07/16 引き続き色んなWidgetを試し中
2020/07/15 書き始め