スポンサーリンク
はじめに
この記事はGo Advent Calendar 2021の20日目の記事です。
(既に25日で空き枠はもうない状態なので、空いている前の日に入れてしまいました・・・。)
ScanSnap等のドキュメントスキャナで自炊した、pdfやzip書籍のリネームするツール、ISBN Book Titler FyneをGo言語で作りました。
MacとWindowsで動くように作っています。(Linuxでの動作確認はできていません。)
(また、DLは準備がまだできていないので、年末に時間をとって調整して、出来次第公開しようと思います。)
Go言語を最近勉強して触りだし、勉強のアウトプットとして作りました。
まだまだ初心者レベルなので、大した内容はありませんが折角なので、実装した上での気がついた点や苦労した点、実際にFyneを使った上でのメモなどを書いてみようと思います。
スポンサーリンク
背景等
ベースとなるISBN Book Titlerというツールは、元々Windows向けに6~7年ほど前に作ったものです。
.NET Framework(C#)で実装されています。
ここ数年は個人開発において、Macを使う事が増え、Windows向けではなくMacでも動くようなツールにしたいと前々から思っていました。
数年前にVueやReactを使って、Electronで作ろうかと思っていましたが、最近読んだ、「Go言語ハンズオン」という書籍の勉強結果として作ってみるのも良いなぁと思い立って作ってみました。
Go言語ハンズオンの感想はこちら
ツールの機能
いわゆる自炊と呼ばれる、自分でスキャンした書籍を自動でリネームするツールです。
書籍の自炊といえば、富士通から発売されているScanSnapによって、ここ10年くらいでだいぶ一般化しました。
最近は電子書籍が一般化した事で、一時期ほどのブーム感はありませんが、いまだに自炊している人は多いかと思います。(私もその一人)
大量の書籍をスキャンした後に、1つ1つファイル名を設定していくのは結構手間がかかる作業なので、とりあえずある程度自動でリネームできたら嬉しいよねという考えで作られています。
仕組みとしては、スキャンした書籍から、裏表紙などにあるISBNコード(バーコード)を検出し、そのISBNコードから書籍情報APIで書籍情報を取得してリネームするとなっています。
PDFから画像を抜き出したり、バーコードを検出するためにGhostscriptやZBarといった外部のツールと連携しています。(要別途インストール)
設定画面(ツールのパスや書籍情報取得APIの設定)
ドラッグドロップでファイルをまとめて一括で変換できるといったお手軽さも売りですが、今回は使用したGUIフレームワーク側がドラッグドロップにまだ対応していないため、一旦アプリケーションを落とした上で、ツール本体(exe)に直接ドラッグドロップといった方式を採用しています。
将来的に対応されそうなので、その際には実装してみたいと考えています。(参考元)
スポンサーリンク
UIフレームワーク
Goのマルチプラットフォームの向けのUIフレームワークに関しては、何種類かあるものの、デファクトと呼べるようなものは無いと感じました。
(あまり調べてないのですみません。)
今年のアドベントカレンダーでちょうどGoのUIフレームワークとその特徴をまとめているものがあって、参考になりました。
大きく分けると2種類に分かれていて、
- HTMLを使って表示するタイプ:Electronなどのように画面はchromiumなどのレンダリングエンジンを使用
- Goでそのまま記述するタイプ
今回は後者であるUIフレームワークのFyneを使用して実装しています。
理由としては、単純でGo言語ハンズオンで使われていて、良さげだったからという理由です・・・。
苦戦したところ
Go言語自体の書き方
以前に一度AWS Lambdaの処理をGoで書くといった事をやっているので、クラスベースではなく構造体ベースな点や例外がないといった点などはあまり驚き等は無く、こういったものと慣れてきました。
ただ、実装方法はわかっても、どういう構造で実装するのが良い書き方なのかといった点が、把握ができていないというのが正直な点です。
そういった意味で1月に発売される、Goの下記の書籍は文法などが説明されている入門書とは異なる視点で学びがあるのではないかと期待しています。
マルチプラットフォームを前提としたAPI
Go自体がマルチプラットフォームを前提としたAPIとなっているため、OSによって差がある部分などは少し苦戦しました。
具体的にはファイルパスの制御やプロセスの実行などの部分です。
Macで動かすと大丈夫だけど、Windowsに持っていって動かしてみると動かない場合などがあり、内容を確認しながら直していきました。
Fyneの画面制御
Fyne自体の画面の制御方法を理解するのに時間を要しました。
UIコンポーネントの種類は多く、ドキュメント自体もあるものの、細かい部分などは書いていない事が多く、GitHub上にあるサンプルのアプリを動かしつつ、ソースコードを見ながら使い方を確認していくといった事が必要でした。
まだまだ発展途上ということもあり、ある程度完成された(枯れた)UIフレームワークであればある機能が無かったり、実装が大変だったりといった事があります。
以降に、個人的にFyneで苦労した点やメモなどをまとめています。
Fyneでの実装メモ
Tableの実装
Tableを用いて一覧を表示する事ができます。ヘッダー機能が無かったり、各カラムの幅を表示内容に合わせて自分で制御しないといけないなど、まだまだ発展途上感はあります。
コレクションを渡せばいい感じで一覧表示してくれる便利なコンポーネント群に慣れてしまった自分には、かなり衝撃的でした。
(ヘッダー自体を通常のレコードとして1行目に入れるといった方法で実装。ただし、ヘッダ固定はできない。)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
var data = [][]string{[]string{"top left", "top right"}, []string{"bottom left", "bottom right"}} func makeTables() *widget.Table { // table内の関数内でも保持するために先に定義だけする var t *widget.Table // 最大幅を保持するための変数 colwidth := make([]float32, len(data[0])) t = widget.NewTable( func() (int, int) { return len(data), len(data[0]) }, func() fyne.CanvasObject { return widget.NewLabel("Template") }, func(id widget.TableCellID, cell fyne.CanvasObject) { // 各セルが描画する際に呼ばれる label := cell.(*widget.Label) // ラベルに文言を表示する label.SetText(fmt.Sprintf(data[id.Row][id.Col])) // 表示に必要なサイズを取得しこれまでのカラム幅と比較 currentWidth := label.MinSize().Width if colwidth[id.Col] < currentWidth { // 最大幅を保持 colwidth[id.Col] = currentWidth // goルーチンで非同期で幅の変更を行う go func() { t.SetColumnWidth(id.Col, colwidth[id.Col]) }() } }) return t } |
実際の画面:一行目のヘッダっぽいのも通常のレコードで実装
UI要素の表示や更新方法
Fyne自体はよくある画面要素を直接操作する方法と、変数とリンクして使用するデータバインディングの両方に対応しています。
今回は、極力データバインディングを使用して実装しました。
画面を触る方法(v1~
v1からある画面インスタンスを直接触る方法です。
ボタンを押下した時に、テキスト入力(Entry)のTextフィールドから値を取得して、ラベルのSetTextメソッドを使用して値を設定しています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
package main import ( "fyne.io/fyne/v2/app" "fyne.io/fyne/v2/container" "fyne.io/fyne/v2/widget" ) func main() { a := app.New() w := a.NewWindow("Hello") label1 := widget.NewLabel("no binding") inputEntry := widget.NewEntry() w.SetContent(container.NewVBox( label1, widget.NewButton("Hi!", func() { // get a data from ui val1 := inputEntry.Text // changing label label1.SetText(val1) }), widget.NewForm( widget.NewFormItem("Name", inputEntry), ), )) w.ShowAndRun() } |
バインディングを使用する(v2~
v2からはバインディングを使用した操作ができます。(v1同様に画面インスタンスを触ることも可能)
string型の2つのbindingを作成し、それをEntryとLabelに渡します。
渡す際にはNewXXXWtihDataというコンストラクタが用意されているのでそちらを使用します。
bindingには、GetとSetというメソッドが用意されているのでそれを使用して値の取得および設定をすれば画面側にも反映されるようになります。
もちろん、構造体のメンバに関してもバインディングは可能です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
package main import ( "fyne.io/fyne/v2/app" "fyne.io/fyne/v2/container" "fyne.io/fyne/v2/data/binding" "fyne.io/fyne/v2/widget" ) func main() { a := app.New() w := a.NewWindow("Hello") // create a binding text inputText := binding.NewString() inputText.Set("input value") // create a binding text str := binding.NewString() str.Set("binding") label2 := widget.NewLabelWithData(str) inputEntry := widget.NewEntryWithData(inputText) w.SetContent(container.NewVBox( label2, widget.NewButton("Hi!", func() { // get a data from binding val2, _ := inputText.Get() // change binding value str.Set(val2) }), widget.NewForm( widget.NewFormItem("Name", inputEntry), ), )) w.ShowAndRun() } |
日本語への対応
現時点ではFyneはデフォルトでは日本語に対応しておらず、そのまま表示すると文字化けます。
一番お手軽な解決方法として、環境変数にフォントを指定するといった事で解決できるようです。
ですが、自分自身しか使わないならばまだしも、配布等を行う場合には使うのは難しい方法になります。
下記が参考になりますが、フォントファイルを用意してリソースとしてバンドルするといった方法で解決できました。
Go + Fyne で GUI アプリケーション(Fyne についてのメモ)
フォントファイル(ttf)を用意して、下記のようなコマンドでバンドルする事ができます。
1 2 3 4 5 |
// フォントファイルをコードに出力 resourceMPLUSRounded1cRegularTtfといった形で参照可能になる(resourceファイル名+拡張子) $HOME/go/bin/fyne bundle MPLUSRounded1c-Regular.ttf > bundle.go // 別のフォントを追加する $HOME/go/bin/fyne bundle -append MPLUSRounded1c-Bold.ttf >> bundle.go |
今回はM+フォントを利用しています。
Fyneにはカスタムテーマを設定する方法があり、そこでフォントの変更ができるようになるので、先程のバンドルしたリソースのフォントを返すようにします。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
package theme import ( "image/color" "fyne.io/fyne/v2" "fyne.io/fyne/v2/theme" ) type MyTheme struct{} var _ fyne.Theme = (*MyTheme)(nil) // return bundled font resource func (*MyTheme) Font(s fyne.TextStyle) fyne.Resource { if s.Monospace { return theme.DefaultTheme().Font(s) } if s.Bold { if s.Italic { return theme.DefaultTheme().Font(s) } return resourceMPLUSRounded1cBoldTtf } if s.Italic { return theme.DefaultTheme().Font(s) } return resourceMPLUSRounded1cRegularTtf } func (*MyTheme) Color(n fyne.ThemeColorName, v fyne.ThemeVariant) color.Color { return theme.DefaultTheme().Color(n, v) } func (*MyTheme) Icon(n fyne.ThemeIconName) fyne.Resource { return theme.DefaultTheme().Icon(n) } func (*MyTheme) Size(n fyne.ThemeSizeName) float32 { return theme.DefaultTheme().Size(n) } func main() { a := app.New() a.Settings().SetTheme(&mytheme.MyTheme{}) ... } |
まとめ
簡単にですが、GoのUIフレームワークであるFyneを使って作ったツールの実装時の話をしてみました。
まだまだGo言語を触り始めたばかりなので、もっと触って慣れていきたいと思っています。
ツール自体もまだまだ機能追加などをやりつつ、次はWEBアプリなどにも挑戦してみたいと思います。