Go&Ebitengine、シンプルなGUIでどこまでやれるかチャレンジ中
はじめに
前に簡単なGUIを作って動かしてみたのだが、このままの設計でどこまでやれるのかが気になってしまったので、色々と作ってみた。
前の記事はこれ。
追加したのはウィンドウのリサイズ、オートレイアウト、フォーカス管理、テキストボックス、といったあたりである。
こんな感じ。見た目が相変わらずショボいのは持病なので大目に見ていただけると嬉しい。
実行結果
また、最近GoogleのAIエディタAntigravityを使い始めたので、それに関する話も混ぜておこうと思う。AIは流行りなのでタイトルとかに入れておけば閲覧数も稼げるのかもしれんけど、使いこなしてるわけでもないのでこっそりと。
Githubに置く
実行はこちら。ウィンドウとボタン、チェックボックス、テキストボックスしかない。 コードはこちら。
これは何か
シンプルなGUIを作ってみようと思って試してみているところ。
シンプルなまま機能を追加できればよかったのだが、だんだん複雑になってきた。やっぱりGUIは難しい。
今回は追加した機能についての設計判断や試したこと、Ebitengineの機能やAntigrabityの所感などについてのお話。
内容
ウィンドウリサイズとオートレイアウト
まずはこのへんから。
ウィンドウのリサイズはResizableという機能を作ってコントロール構造体に追加すれば使えるようになっている。
Window構造体にResizableを追加すると、ウィンドウのエッジでカーソルがリサイズ用の形状になり、普通にWindowsのウィンドウみたいにサイズを変更することができる。
動かしてみてもらえるとわかるが制御は手抜きなのでちょっとイケてない。
そのへんを頑張っていないのは訳があって、Resizableはオートレイアウトを手軽に確認するためにリアルタイムのサイズ変更が欲しかったから作っただけだったりする。
ではオートレイアウトはと言うと、子コントロールを管理するGrouping構造体に組み込んであって、Layoutメソッドを呼ぶと実行できるようになっている。このAutoLayoutInterfaceはLayout()メソッドを持つ構造体を表し、各種オートレイアウトのパターンに合わせて後から構造体を格納する。
// Groupingは子コントロールを保持する機能
type Grouping struct {
Control Control
Children []Control
OrderChange bool
AutoLayout AutoLayoutInterface
}
もともとはAutoLayout某というパターンごとの機能を作ろうとしていたのだが、こうしてしまうとオートレイアウトパターンごとにコントロールを用意するハメになりそうだったので、機能として作るのはあきらめて、Groupingのオプションにした。
オートレイアウトの効果はウィンドウのリサイズをしてもらえばすぐわかる。また、Windowのタイトルバーとクライアント領域もオートレイアウトで調整している。自分で座標を計算するのは面倒なのでとても良いものだと思う。
ちなみにだがウィンドウを小さくするとボタンとかがはみ出す。クリッピングするにはEbitenのDrawメソッドに渡されるscreenからSubImageを作ってそこに描画すればいいと思うのだが、ブラウザ用のwasmにすると何も描画されなくなってしまう。Windows用のexeだとちゃんとクリッピング描画できる。マルチプラットフォームライブラリのEbitenでプラットフォームごとの違いが発生するのはバグと言えるのかもしれないが、こちらの使い方が間違っている可能性もあり。微妙な話である。
(追記:原因がわかったので追記しておく。画面のサイズを取得するebiten.WindowSize()が、ブラウザの場合には0,0を返す仕様だったのが原因だった。ブラウザの場合にはebiten.WindowSize()を呼ばずに固定サイズ640,480を使うよう修正して解決済。これでクリッピングが自然にできるようになった。)
CheckBoxとAntigravityについて
AntigravityではGemini 3 Pro(High)を選択している。質問や相談すると回答として実装プランを提示してきてしまう前のめりな感じだが、お願いすると文句言わずやってくれるし、Rejectしてもご機嫌斜めにならない。
最初に試したときにCheckBoxを作ってもらったのだが、世の中に広く使われているライブラリの使い方とかではなく、俺が手元でリアルタイムにでっち上げているGUIライブラリを適切に使い、独自の作法に従ってコードを書いてくれたのでかなり驚いた。CheckBox.goは作ってもらってほぼそのままの状態である。コメント含めて。こんな感じ。
func (c *Checkbox) drawCheckbox(screen *ebiten.Image) {
ox, oy := c.GetOwnerPos()
x := float32(ox + c.X)
y := float32(oy + c.Y)
h := float32(c.Height)
// ボックスの枠
vector.StrokeRect(screen, x, y, h, h, 2, color.White, false)
// チェックされている場合の中身
if c.Checked {
// 簡易的なチェックマーク(塗りつぶし)
vector.FillRect(screen, x+4, y+4, h-8, h-8, color.RGBA{0x00, 0xff, 0x00, 0xff}, false)
}
しばらく使ってみた感想としては、コードの修正依頼は少しずつがよさそうだ、ということ。たくさんまとめて修正してもらうとコードが壊れる確率が上がるし、動作がおかしいときに指摘すると追加で修正してくれるのだが、そういった場合の修正方法は場当たり的な感じに見える。特殊状態を判別する変数を増やすだとか判定を増やすだとか、とにかくコードを増やして解決しようとする。ように見える。新人プログラマーかよって思った。
なので設計レベルはこっちで考えて、実装方法が自明なめんどくさいやつをお願いしたりすると事故が少ない。clickableとdraggableを統合してMouseInteractionにするなどはお願いしてやってもらった。直してもらったコードがうまく動かないときは動くまで頑張ってもらうのではなく、おそらくはもっと根本的なところに問題があるので、いったんRejectして解決しに行く。
あとは修正してもらったらきちんとチェックして、細かく手直しする。最終的に動けばよかろうなのだ、という開発なら別にどうでもいいのだけど、GUIという複雑な仕組みを構築しようとしているので、乱雑なコードでは困る。俺のコードが乱雑ではないという意味ではなくて、俺より乱雑なコードを書かれると困るという意味だ。
設計思想や方針などを教えておけば改善できる気もするが、そもそもとして実験中であるからして、根本的なところがブレブレな事情もあり、難しいところ。
どうでもいいけど実装プランは英語で出てくるが日本語でお願いしますって言えば日本語に書き直してくれる。
フォーカスとテキストボックス
フォーカス周りはまだ定まっていないが、何となく動くという状態。このままで今後を乗り切ることができるかは心配。テキストボックスはEbitenにexp/textinputがあるのでお試し。Antigravityで日本語入力対応テキストボックス作ってとお願いしたら存在しないメソッドを呼びまくってひどいものができあがった。たぶん世の中に使用例がほぼ無い上に複雑な機能だからなのではないかと思っている。
exp/textinputにはFieldというサポート機能があり、それを使うとわりと簡単に日本語入力ができるようになる。らしいのだが、わりと苦労した。そもそもとして日本語入力自体が複雑なものなので、それを簡単に扱えそうに見えるようにしてくれていても、見た目ほど簡単ではない。
とりあえず何となく動くようにはなったが、いくつか気になったことがある。環境はGo1.25.1とEbitengine2.9.3。
・CompositionSelection()が0しか返さない。
・Selection系で使うStartとEndはField側で前後どっちでもいいようにしてくれたら嬉しい。選択開始時の位置をStartにしてあとはEndを変えるだけならとても楽になる。
・日本語入力にして「あ」ってだけ入れてEnterを押さずにBackSpaceを押すと「あ」が残る。textInputStateにだけ残ってしまっているようで、Windows用exeで発生した。Windowsでもブラウザでは発生しない。ちなみにGuiguiのテキストボックスでも発生した。
・フォーカスを当てた直後(コンマ数秒ぐらい?)に日本語入力すると変換一覧がデフォルト位置になる。ブラウザでやると入力できなくなる。
みたいな感じ。まあ実験的機能なので別に文句は無い。何が仕様で何がバグなのかわからんし。あと、ブラウザ用にしてスマホで動かすと日本語の予測変換が予想外に出たり出なかったり引っ込んだり引っ込まなかったりしてよくわからないが、これは使い方に問題があるのだろうと思っている。textinputド素人だし。
設計思想と方針と問題点
もともと、機能を分割して組み合わせることでGUIのコントロールが作れるのでは、というところからスタートしているが、この点については今も変わっていない。でも複雑なコントロールを構築しようとすると色々な問題が発生してきそうで、そろそろ無理が出てくる頃かなと。
例えば機能の構造体の生成順によって干渉するような部分はどうにかしたいのだが、UpdateとDrawの順番をわかりやすく簡潔かつ柔軟に指定する方法が思いつかない。
また機能の組み合わせ以外に、地味に中心になっているのがTouchInfo(input.goのやつ)で、Go始めて最初に作ったものなのだが、これを使ってコントロールが自分を操作するTouchInfoを保持して勝手に動作する仕組みになっている。たぶんこれも複雑さの原因の一つになってそうな気がする。使い方の問題的な感じで。
「全体的にシンプル化する案はないですか」ってAIに聞いて返ってきた回答(実装プランが返ってきた)は、実行したらズタボロだったのでそういう用途には現在のAIは向かないのかもしれない。ただ、指摘してきた問題点に関しては納得できる感じだったから、コードを書いてもらうだけではなく、もうちょっとお喋りするのがよいかもしれない。なんせコードを読んで解析できるのだから。でも相談するとすぐ実装プラン出してくるんだよな…。
ちなみに指摘された問題点は入力のハンドリングと更新の処理が一体になっているという部分で、HandleInputとUpdateに分けるべきだと言ってきたのだが、修正をお願いしたら今ある全部の処理がHandleInputに入っていた。動きもおかしくなってた。速攻でRejectした。
再検討する価値はあるかなと思ってはいる。
おしまい
AI使いこなせない。使いこなせる人はものすごい生産性になってるんだろうなと思う。ほんまか?みたいな。まあこれも使い方、使いどころの問題なのだろう。
GUIについては、一式作り上げてGUIライブラリを名乗る、みたいなのは目指していなくて(整備するの面倒だし)、ただ、フォームデザイナーみたいなのが作れたら面白いだろうなとは思っているので、究極的にはそこだ。いまのところ辿り着ける気がまるでしない。
でもまあ、もうちょっと作ってみたいとは思っていて、Antigravityに手伝ってもらえば楽もできそうだ。「スライダー、スクロールバー、リストビューなどお作りしましょうか!」とか言っていたので任せられそうではある。
そんな感じで。
Discussion