Go&Ebitengine、オレGUIでお絵描きツールモドキを作ってみる
はじめに
Zennのトップに出てくる記事を見ているととてもレベルが高く、場違い感がすごいのだが。まあ枯れ木も山の賑わいと言うことでヨシとする。ヨシッ
Go言語と2DゲームライブラリのEbitengineを使ってGUIを作っていて、基本的な挙動は問題なさげだと思ったのでアプリケーションっぽい物を作ってみようと、手っ取り早くお絵描きツールを作ってみた。
これまでの記事は以下。
お絵描きツールの見た目はこのような感じ。
お絵描き中
Githubに置く
実行するのはこちら。なんかスマホで表示すると今までと違う感じになってしまった。なんでだろう。動くは動くんだけど。 コードはこちら。
これは何か
σ(゚∀゚ )オレGUIでお絵描きツールを作ってみた。
今回新たに作った機能はスクロールバーと、スクロール可能なパネル、あとはブラウザでクリッピングできるようにした修正と、関連して色々な調整である。
これを活用した例としてexamples/painttoolを用意してみた、というわけだ。物としては大したことは無く、ただそれっぽいものが動くだけで、実用的なわけではない。
内容
package分割とHandleInput
uiディレクトリ以下に全部置いていたらファイルが増えてきて何がなんだかわからんくなったのでディレクトリを分けて、packageも分けた。
package名を書く必要があると文字数は増えるがそれが何であるかが見てわかるようになるので良いと思う。
あと、UpdateメソッドをHandleInputとUpdateに分けた。新規の入力を処理するのがHandleInputで、内部情報を更新するのがUpdateとなっている。
ごちゃまぜになっているとややこしかったので、わかりやすくはなったと思っている。
クリッピングとSubImage
前回記事で悩んでいたブラウザでクリッピングしようとすると描画できない問題は解決した。
ebiten.WindowSize()がブラウザだと0,0を返してくるせいで、クリッピングする範囲が0になっていたというカラクリである。この挙動はebiten.WindowSize()の実装のコメントに書いてあった。
以下のようにブラウザであることを判定するように修正したら描画できるようになった。判定方法はEbitenのサンプルを参考にした。
func (ms *MainScreen) HandleInput(t input.Touch) bool {
// ブラウザではウィンドウサイズが取得できないので固定値にする
if runtime.GOOS == "js" {
ms.ControlBase.Width = 640
ms.ControlBase.Height = 480
} else {
ms.ControlBase.Width, ms.ControlBase.Height = ebiten.WindowSize()
}
ところでSubImageの挙動なのだが。
AIに質問するとSubImageに対して描画する際は左上が0,0になるという回答が得られるのだが、実際には元画像の座標系となっていて、俺のコードもその動作を利用して簡素化している。
SubImageをアサーションで*ebiten.Imageにするのだから、これはImageの一つであるとすれば、左上は0,0になるのが透過的な挙動だろうと考える。実装の複雑さとか便利さとかを考えた結果だろうか。
ちなみにEbitenの実装は見ていないが、こういう挙動を俺が作るとしたら、Viewport行列を細工するだろうな、と思った。
スクロールバーとスクロール可能パネル
スクロールバーもスクロール可能パネルも、複数のコントロールを組み合わせた複合コントロールである。こういった物は今まで無かったかというと、Windowがそういう物なので別段目新しくは無い。
なので、オートレイアウトと子コントロールを駆使して構成している。例えばScrollBarHはこんな感じ。
// ScrollBarHは横方向にスクロールするための複合コントロール
type ScrollBarH struct {
*parts.ControlBase
*parts.Grouping
buttonLeft *ScrollButton
slider *ScrollSliderH
buttonRight *ScrollButton
OnSlide func()
}
3つのコントロールをGroupingに放り込んで子コントロールにしているわけだ。描画も操作もすべてそっちでやっている。
OnSlideはツマミを動かしたときに呼ばれる関数で、MouseInteractionでも使っているがイベントとして扱うには使い勝手のよい手法だ。
なんなら既に登録されている関数を取り出して自分の処理を実行する前や後に呼ぶという方法で連結することもできる。連結を解除するには別の機構が必要になるが。
スクロール可能パネルのほうはこの縦横のスクロールバーを保持した極めて複雑な複合コントロールになっている。こんな感じで。
// ScrollablePanelはスクロール可能なパネル
type ScrollablePanel struct {
*parts.ControlBase
*parts.Grouping
topGroup *Blank
Area *Blank
Panel *Blank
ScrollbarV *ScrollBarV
bottomGroup *Blank
ScrollbarH *ScrollBarH
corner *Blank
OnSlide func(x, y float64)
}
BlankというのはControlBaseとGroupingだけを持った何もしないコントロールで、位置とサイズと子コントロールは持っているのでオートレイアウトの位置調整用に使う。
現状オートレイアウトは縦もしくは横の調整しかできず、多段に組み合わせて頑張ることで複雑なレイアウトを実現している。
もっと多機能にスムーズにレイアウト調整をしようとすると、線形計画問題とかでシンプレックス法などを扱うことになると思われる。AIに聞いてみたらサンプルコードを出してきたのでひょっとしてお願いしたら作ってくれるかもしれない。
お絵描きツールについて
GUIというとボタンとかラベルとかリストボックスみたいな部品があって、それを並べて操作する感じが基本であるわけだ。そういった物の場合、ウィンドウ上にすべて並べられているので、ウィンドウのコードにすべての処理を書くことが多い。
でもアプリケーションはそんなコントロールを並べただけの単純なやつばっかりということは当然無くて、オリジナルの描画やオリジナルの操作などが必要になることは良くあるわけだ。こうなってくるとすべてのコードがウィンドウ上に書かれていると複雑になりやすい。
解決策としてはカスタムコントロールを作って責務を分割していくというのが一つの手ではあるが、カスタムコントロールが作りやすいライブラリでないと少し難しい。
ということで、お絵描きツールでは画面に3つのパネルが存在しているので、それぞれを別のコントロールとして作成してみた。おかげでmain.goではNew何某でコントロールを作成してAddChildするだけになっている。
プログラミングモデルとしてはユーザーが自分でコントロールを作成しながらアプリを作っていくような感じが良さそうに思える。オレGUIでカスタムコントロールを作るのが簡単かというと少しだけ疑問が、無くはない。
あとは簡単な物なので特筆すべきことは無いだろう。
おしまい
通常、GUIユーザーはボタンなどのコントロールを簡単に作って簡単に配置して、イベントも簡単に利用できることを期待する。
今回のお絵描きツールみたいにカスタムコントロールをイチから構築する場合、コントロールの構築方法を学ぶ必要があり、例えそれが簡単だったとしても想定外の学習コストになる。これは良くない。
既存のコントロールを簡単に拡張する方法を確立して、コードの分割が手軽にできるようにする必要がありそうだ。俺自身わざわざコントロールを作っていくのはちょっと面倒だし、今後の課題だろう。
そんな感じで。
Discussion