Golangができること、むしろ「得意」と言われるものはすでにたくさんあります。
- クロスコンパイルが得意だし依存が少ないバイナリができるから、いろんな環境で使えるコマンドラインツールを書くにはGoがいいよ
- パフォーマンスが高いし文字列処理もやりやすいので、高速なAPIサーバが得意。gRPCでもHTTP/2でも
- Webアプリケーション・フレームワークも増えてきていてウェブサービス作れるよ
- ビルドシステムとパッケージマネージャ内蔵なので、gitから簡単にパッケージをダウンロードしてきたり、◯makeコマンドとか◯runtとか◯owerで消耗しなくて済む
- gopher.jsでJavaScriptにもなる
逆に今まであまり良い解がなくて、「Goにはちょっと不向きだね」と言われ続けていたのがGUIです。鳴り物入りで出てきたGXUIが開発が止まってしまい、それと同じぐらいにshinyというものが開発がスタートしている、というのが現状です。shinyは本格的に動き始めている感はありますが、まだ「今日から使える」というものではないように思います。
クロスプラットフォームで言えば、下記のエントリーで紹介されているgo-qmlや、mattn-wareのgo-gtkがあり、Windowsに特化したものだとWALKというのもあります。ネイティブUIを使うライブラリとしてはgithub.com/andlabs/uiというのもありますが、大幅にリライト中だよとREADMEにあります。
とはいえ、せっかくgopher.jsもあることだし、デスクトップだけじゃなくてブラウザにも対応したいし、モバイル対応考えるとランタイムのサイズが大きい(GTKはどのぐらい?)のはちょっと避けたいし・・・と考えるとまだまだGolangのGUIはポテンシャルを秘めている(改善の余地ありな)気がしています。
本エントリーでは3分クッキング方式でGoのGUIの可能性について見ていきましょう。
GUIフレームワークとバックエンドのベクターエンジン
GUIフレームワークの構成要素というと、ボタンクラスがあって、ウインドウクラスがあって・・・というのが思い浮かぶ人が大半でしょう。だいたいどのフレームワークでも似たような感じになっていますし、GUIを開発する人が主に触れるオブジェクトはそのようなレイヤーです。ですが、GUIのフレームワークにはそれとは別の共通点もあります。バックエンドにベクターグラフィックスを扱える独立した描画エンジンがいます。
- Windows
- GDI+(昔)
- Direct2D(今)
- Mac OS X
- Quartz
- Java
- Java 2D
- Google Chrome/Android/Chrome OS
- Skia
- Qt
- QPainter
- FireFox, GTK+
- cairo
- BeOS
- BView
ボタンクラスがボタン描画のすべてを自前でやらなければならないとなる、実装の手間が大きくなってしまいます。OpenGL/DirectXなどの対象のハードウェアを使ったコードを各部品が自前ですべてやるというのも非現実的です。また、OpenGLもDirectXも、フォントを扱う機能はありません。そのような処理をまとめたのが、これらの描画エンジンです。それぞれのAPIを見比べてみると、どれもこれもびっくりするぐらい機能が似通っています。これらのエンジンがフォントのレンダリングも担っています。アンチエイリアシングもサポートしていますね。この「ステートマシンベースの描画システム」の元ネタはPostScriptあたりになるんでしょうかね?詳しくは調べてませんが。
GUIの実装をするにはこのようなレンダリングエンジンの実装は必須ですが、クローズドソースだったり、コードの規模がちょっと大きめだったりして、実装するのは大変そうです。そんな時に目に止まったのが以下のエントリーです。
さまざまなコンパクトなGUIフレームワークが紹介されていますが、重要なポイントは、数多くのGUIがバックエンドにNanoVGを使っている点です。NanoVGとは何か?に関しては以下のエントリーあたりが参考になります。
4000行程度のCで実現されたベクターグラフィックスライブラリ(ただし、TrueTypeフォントのパースや画像のロードは除く)です。これならなんとか移植できそうですよね?
はい、こちらにできあがりのものを用意しています。下記のコマンドでインストールできます。
$ go get github.com/shibukawa/nanovgo
クロスプラットフォームなgoxjs/glを使って実装したのでブラウザでも動きます。めちゃ遅いですが、Nexus 5上のChromeでも動きました。NanoVGはOpenGLでこんなことができます。
- テクスチャの読み込み
- テクスチャの表示
- 多角形のパス
- ベジェ曲線のパス
- パスストロークの描画(太さ指定や、コーナー、ジョイントの形状指定もあり)
- 画像・グラデーションでのペイント
- フォントの描画・カーソル位置等の計算
- アンチエイリアシング
ただし、フォントのフォールバックみたいなのはないし、PDFなどの他のフォーマットへの出力、3色以上の色を使ったグラデーション、破線の描画などは今はありません。あと、GolangのOpenGL事情(WebGLも含むよ)でもちょびっと触れましたが、数値型をfloat64にすればもうちょっとブラウザは速くなると思います。
一応、パスのキャッシュ周りではメモリのアロケートのムダを減らしたり、ムダなメモリコピーを減らすように心がけました。下記のエントリーで@hnakamurさんと@mattnさんに教えていただいたテクも使わせていただいております。ありがとうございました。パフォーマンスに関してはC言語版には負けないレベルになっています。
さて、GUIも
ベクターグラフィックスが扱えるようになりました。NanoVGoのサンプルのボタンとかテキストボックスは単なる絵なので、操作することはできません。せっかくなので上記で紹介されているものをGoに移植してみましょうかね。いくつか見比べてみましたが、blendishはちょっとアプリケーション側のコードが冗長で長かったので、簡単に使えそうなNanoGUIを移植してみましょう。これは6000行ほどのコードです。
すべての機能の実装はしていませんが、ほとんどのコンポーネントはとりあえず実装してあります。おまけで、テキストボックスには、Mac OS Xが標準で備えるEmacsキーバインドも入れました。これもpure goなので、ブラウザで動かすこともできます。ただ、こちらの圧倒的で無慈悲な実装コードを見ていただければお分かりいただけると思いますが、まだブラウザではテキストボックスは使えません。そのうち何かしらパッチを送りたいところ。
サポートしている機能はこんな感じですね。
- ボタン (通常、トグル、ラジオ)
- ポップアップボタン (吹き出しでサブウインドウが表示される)
- コンボボックス (ポップアップボタンの吹き出し中にボタンが並んでいる)
- イメージパネル (ポップアップボタンの吹き出し中にイメージボタンが並んでいる)
- カラーピッカー (ポップアップボタンの吹き出し中にカラーホイールがある)
- チェックボックス
- ウインドウ (OSのウインドウ内のフローティングウインドウ相当)
- スクリーン (OSのウインドウ相当)
- スライダー
- プログレスバー
- カラーホイール
- グラフ(折れ線グラフが書ける)
- イメージビュー
- テキスト入力 (通常、整数用、浮動小数点数 )
- 各種レイアウト
ちなみに、このコードをいじくりまわしながら書いたのが以下のエントリーです。
今までC++などではクラスを使っていたようなところもinterfaceさえあれば問題ないケースがほとんどです。実際、このNanoGUI.goも、コールバックのための継承をユーザに強いることはありません。Goに入ったらGoに従うのが良いとよく言われますが、そうはいっても実装継承がないと、委譲するコードばっかりになってしまってうれしくないケースもあります。例え美しくないと分かっていても実用上実装継承がしたいんだ!という。
特に、GUIのウィジェットは、親クラスが大量のメソッドを持っています。NanoGUIも50ぐらいあったかな?コンポーネント1つ書くたびに、新しい実装をしない全メソッドに委譲コードを書いていくのは辛いですよね?なるべくコンパイラのコンパイル時の型チェックの恩恵は受けたいですし。あとは、HTMLみたいなドキュメントオブジェクトモデルみたいなやつの実装も、実装継承がある方がうれしい気がします。逆にいえばそれ以外はもう継承という考えは捨ててもいい気がします。
上から下まですべて美しい世界で統一できればいいんですが、なかなかそうはいかない。いくら関数型言語大スキ!の人が「参照透明!副作用ゼロ!」と叫んだところで、関数型言語用にラップされたGUIフレームワークの中ではステートフルフルなC/C++のオブジェクト指向のコードが人知れず仕事していたりするもんなんですよね。上から下までシンプルにできればそれに越したことはないですが、中がちょっと荒れててもインタフェースがシンプルな方が個人的には好きです。
ブラウザでも動くということは、コマンドラインツールでもGUIが提供できるということです。GUIコードをJavaScriptにしておいて、ブラウザでアクセスがあったらそれを介して操作するとか、今のステータスを表示するとかできちゃいます。夢が広がりますね!まぁ、NanoGUI.goを使わないで、Xプロトコルを自分で話すのでもいいのですが・・・
クロスコンパイル
MacからWindowsのアプリがビルドしたい!というのが最初から決めていた要件です。mxeというMinGWのディストリビューションを使いました。Macに入れて動かしましたが、説明によればLinuxでもFreeBSDでも動きそうです。MacPortsのmingwが一向にバージョンアップされないのですが、これで大丈夫。
$ git clone https://github.com/mxe/mxe.git
$ cd mxe
$ make MXE_TARGETS='i686-w64-mingw32.static'
あとはこんな感じでクロスコンパイルできます。glfwなどはgo-gl/glfwにソースが入っているので、glfw3を入れておかなくても大丈夫かと思います。
$ CGO_ENABLED=1 GOOS=windows GOARCH=386 CC="i686-w64-mingw32.static-gcc" go build -ldflags="-H windowsgui" -o sample.exe
MacからLinuxとかはどうやるんでしょうかね?WindowsからMac/Linuxのクロスコンパイルもできるならやりたいですが・・・
日本語をインライン入力したい!
GUIのツールのテキストボックス、日本語が入らないとしたら使い物にはならないですよね?glfwはWindowsに関しては決定後の文字列はWM_CHARイベントで飛んでくるので、一応受け取ることはできました。Macはダメでしたが、glfw自身に出ているPR#643を適用すると一応マルチバイト文字も受け取れるようになります。どちらも、サロゲートペアを持つUTF-16としてしか取り出せないので、絵文字とかを考えるとUTF-32で受け取れるようにさらなる修正は必要かなとは思いますが・・・
とはいえ、受け取れるだけで編集途中の文字列は表示されません。つまり、どんな文字が今入力されているか、心の目で見る必要があります。IME対応というのは昔から行われており、対応付けのランクも上からon-the-spot(編集中文字列を完璧にアプリが扱える)から、over-the-spot, off-the-spot, root window方式と色々あります。詳しくはMozillaのこのページが参考になります。on-the-spotを実現するには最低限次のような機能が必要かな、と思います。
- 入力中文字列をアプリケーションにコールバックで伝える
- カーソル位置をIMEに伝達(候補ウインドウの位置を決めるのに必要な情報)
- IME ON/OFF
- IME ON/OFF状態を取得
- 入力中文字列をクリア
これを修正するには、Cのレイヤーのglfwを直すしかありません。そのためにいろいろな調査が必要です。各環境ごとにAPIは完全に違いますし。
はい、この修正を行った結果をこちらに用意してあります。
- glfwのIME対応のPR#658
- go-gl/glfwに日本語対応パッチを当てたgoパッケージ github.comshibukawa/glfw-2
- goxjs/glfwで↑を使うようにパッチを当てたgoパッケージ github.comshibukawa/glfw
今すぐアプリを書きたい場合で、日本語対応が必要な方は、go-gl/glfw、goxjs/glfwの代わりに上記のパッケージを使うとWindows/Macなら動きます。将来的にglfw v3.2がリリースされて必要なくなったら削除します。今は下記のコマンドでインストールされる依存パッケージも↑を向いています。
$ go get github.com/shibukawa/nanogui.go
現状の詳細はglfwマルチプラットフォームでのIME対応の困りごとまとめにまとめてあります。X11環境は実装したけどうまく動かず、Waylandの環境を作ってみようと思ったけど起動しなかったり、LinuxやBSDはまだまだ動いているとは言いがたい状況です。ヘルプいただけると助かります。
オリジナルのウィジェットの作成
NanoGUI.goはGoで書かれているので、Goさえ書ける人なら自分でウィジェットをどんどん作れます。実際にボタンとかのコードを見てもらうのが早いかと思います。最低限は以下のコードですね。
-
WidgetImplement
を継承した構造体を作る。 -
New新ウィジェット(parent Widget) *新ウィジェット
なコンストラクタ関数を作る。中でInitWidget
を呼ぶ -
PreferredSize(self Widget, ctx *nanovgo.Context) (int, int)
メソッドを定義して適切なサイズを返す -
Draw(ctx *nanovgo.Context)
を継承して描画させる -
String() string
を継承する(デバッグ用オプション)
必要に応じて、クリックやマウス操作のイベントハンドラをオーバーライドして使います。サイズのx, yは親要素からの相対座標なので、(0, 0, w, h)で描画しちゃうとずれちゃうので要注意です。
ポップアップする要素は、ウィジェット内でポップアップ用のウィジェットをもう1つ作って、Screen直下にぶら下げます。元のウィジェットがアクティブになったら、ポップアップ用のウィジェットを表示させる/前面に持ってくる、という感じになります。
試しにスピナーウィジェットを作ってみましょう。スピナーウィジェット自体は実体を持たず、起動されると、自分の直上のウインドウ(一部のフローティングウィンドウだけをカバーしたい場合)、もしくはスクリーン(OSのウインドウ)全体をポップアップ用ウィジェットがカバーします。イベントハンドラもブロックしたいですよね?
こういうウィジェットも、Makefileとかビルドプロセスへの組み込みを気にせずに、go getで配布できちゃうあたり、Golangのすごく良い点ですよね。簡単ですが、デバッグ用の機能もちょびっとあります
nanogui.SetDebug(true) // ウィジェット境界に赤枠描画
screen.DebugPrint() // ウィジェットのツリーをコンソールに表示
次にやりたいこと
一通り動くGUIのフレームワークはできたのですが、まだまだ改善したい所はいろいろあります。これらの機能は大幅な改造を伴うものも多く、NanoVGo/NanoGUI.goからフォークして、別のパッケージとして実装していきたいと思っているところです。GUIのウィジェット周りの構造は大きくは変えないと思うので、現状のNanoGUI.go用にウィジェットを自作した場合も、簡単に移植はできると思います。
フォント周りの改善
日本語が絡む時に必ず出てくるのが、フォントファイルのサイズの話題です。特に、NanoGUI.goの場合は自前でフォントファイルをパースしてベジェに分解してレンダリングして使っています。ブラウザ版だとそのダウンロードからして大変です。ラベルとかで予め使うとわかっている文字だけを抽出してフォントを小さくするというテクニックもありますが(ウェブフォントでは特に)、テキスト入力で日本語の入力を認めるなら、ある程度のカバレッジを持つフォントを利用する必要があります。ざっとUIで使えそうなグリフでかつTTF形式のフリーで使えるフォントを調べてみました。
- IPA Pゴシック - 6.2MB
- 小夏フォント - 5.7MB
- Nasuフォント - 5.2MB
- 源真ゴシック - 5.1MB
- VLゴシックP - 4.2MB
- さざなみフォント - 1.7MB
- M+フォント - 1.6MB
源真ゴシックあたりは評判のいいNotoSansのアレンジ版(TTF版)ですし、サイズが許すなら使ってもいいかなと思います。さきほどのスクリーンキャストも源真ゴシックを使っています。ウェブならさざなみかM+が良さそうです。日本語フォントを久々に色々調べてみましたが、昔はベースとして和田研フォントが、少し前はIPAフォントがよく使われていましたが、最近はNoto Sans(というか源ノ角ゴシック)をベースにしたフォントが増えていますね。
デスクトップの場合はOSのフォントを使うのも選択肢に入るでしょう。OSのフォントを使うことができれば、フォントに入ってないグリフが必要になったときのフォールバック処理も可能です。ただし、UIでどのフォントを使うか、フォールバックで使われるフォントの情報などは、アプリが自前でフォントフォルダを探してなんとかするのではなくOSなどが事前にデータベース化している情報を問い合わせる必要があります。node.jsですが、マルチプラットフォームでこれを実現するライブラリがあります。
一部Goに移植して実装してみたのですが、Mac OSだとOpenTypeフォントが代替フォントとして提案されます。残念ながら、今使っているフォントパーサ&レンダラーはTrueTypeフォント限定なので、このままだと使えません。先に行うべきはTrueTypeコレクションとOpenTypeに対応することです。やはりJavaScriptのコードですが、下記のコードは移植するのに良さそうです。
OpenTypeに対応するメリットとしてはもう1つあって、ウェブフォントが利用できるようになる点です。ウェブフォントであれば、ブラウザでキャッシュされるので、NanoGUI.goを使うアプリが増えてくれば、キャッシュを使いまわしてダウンロードをしないで済むようにするということもできます。
後は絵文字ですね。絵文字はフォールバックの仕組みさえできてしまえば、NotoEmojiのようなTTFフォントとして作られたフォントを入れれば大きな変更を加えなくても表示は可能ですが、これだと単色でしか表示できません。どうせならカラーの絵を表示したいですよね。これもバックエンドの修正が必要です。
あ、あとは禁則処理ですね。Harfbuzzがデファクトスタンダードっぽいけど、これ移植するの大変そう・・・
NanoVGoレイヤーを含む性能改善
nanovgも、nanoguiも、キャッシュなどせずに毎フレーム愚直にすべて計算しなおしています。ダーティフラグを持たせて、再計算の必要がなければ一度計算したパスの分解情報を再利用したり、GUIのレイアウトの再利用をすると良さそうだな、と思っています。
あとは、テキストの描画で、三角形を2つ毎回作ってGPUに描画させているのですが、GPU本来の性能を引き出すWebGL頂点データ作成法で書かれているようにTRIANGLE_STRIPを使えばテキストの場合の頂点数が2/3に節約されて良さそうな気がします。文字のUVを事前にGPUに転送しておけば、頂点インデックスを使って文字描画をさらに高速化できそうです・・・
また、今はnanovg外部にAPIとして公開されていない、UV値を直接指定した多角形描画の機能も使えるようなると、アイコンや画像表示のパフォーマンスが上がったり、9パッチが実現しやすくなったりするでしょう。今は画像は一度ペイント要素として展開されてから描画されます。ペイント要素は、一度ステンシルバッファに描画領域の形状をレンダリングし、描画領域全体を覆う要素を作って、ステンシルバッファを見ながら切り抜いて描画する、といった2パス描画になっています。よく使うRect描画ぐらい、1パスでやりたいですよね。フォント描画用のrenderTriangleをちょっと修正するだけなので難しくはないでしょう。フォントの所で触れた絵文字もこの流れで1パスで書ける要素に追加したいですね。
あとは、モバイル対応をするなら、スリープから戻ってきた時にテクスチャを再度GPUに転送しなおす処理も必要ですね。
ネイティブ機能の吸収レイヤー
どうしてもOpenGLのレイヤだけではできないこともあります。例えばOS Xのメニューバーにメニューを登録する機能だったり、ファイルやフォントの選択ダイアログだったりといったものです。まだ移植はしてませんが、NanoGUIもダイアログだけはネイティブのダイアログを使うようになっています。
ブラウザ対応する場合のことを考えるとAPIは少し考えないといけないところ。ブラウザだとボタンが押された時しかファイル選択ダイアログが出せなかったりしますし。透明な<input type="file">
を並べておいて、クリックされたらダイアログ表示とかでしょうかね?オリジナルのNanoGUIと違って、FileDialogButton
みたいな専用のボタンを作る必要がありそうです。保存ダイアログは、環境によってはクリックイベントを介さずにできそうですが、ポータブルな方法はまだなさそうです。
まとめ
OpenGL上に作られたGUIフレームワークの実装と、今後やっていきたいことなどを紹介してきました。今までのGUIライブラリに比べてまだまだ未熟なところもありますが、いくつか平均以上なところもあると思います。
- パフォーマンスは比較的高いし、ウィジェットも既に使える
- ブラウザ含めてクロスプラットフォームが実現できている(日本語入力はMac/Windowsのみ)
- フォント描画もきれいだし、glfwのおかげでRetina対応もできてる
- 出来上がりのバイナリファイルサイズが比較的小さいし、クロスコンパイルもできてる(mac->windows)
- Win/Mac/Linuxはcgo使っていて毎回フルコンパイル走っちゃっているが、なんだんかんだで30秒でビルドは終わる
- go getでいろんなウィジェットを交換できる
ここでやっているフォント周りの話とか日本語入力の話などは、例えShineyなどのフレームワークがメジャーになっていくとしてもそちらに貢献できる話かなぁと思っています。あと、Golang以外で似たようなことをしたいと思っている人にもきちんと動くサンプルとして役に立つと思います。
本当は、GUIフレームワークが作りたかったのではなくて、自分が作りたいもののアイディアがあって、それを実現するには既存のフレームワークはどれも合わず、自分で実装し始めたというのが本当のところ。来年はそっちの作りたかったものにもっとフォーカスしたいです。そういう意味で、例え他の人がまったく触らなかったとしても、僕自身はしばらく飽きるまで/他のデファクトが出るまでは触り続けると思います。すでに色々メソッドを足したりもしていますが、ちょっとずつ機能は増えていくと思います。
さて、とうとう明日は最終日ですね。cubicdaiyaさん、gureguさん、erukitiさんです。
今後の計画(1/6/2016追記)
多少改造を加えていて、ぼちぼち安定はしてきている気はします。VScrollPanel周りがちょっと特殊で使いにくくてというのはありますが。
オリジナルと同じ名前のままどんどん機能拡張していくのもあまりいいことではないと思うので、こちらはバグ修正とか程度に押さえて、フォークしてあたらしいリポジトリで魔改造していく予定です
ベクターグラフィックスの方はOpenTypeフォント対応から地道にやっていくし、GUIフレームワークもテンプレートエンジン対応とか、仮想なんちゃら的なウェブ側で進んでいる機能を入れたりと大幅に改造して原型は留めない予定なので、また同じように動くようになるまでは時間がかかると思いますけどね。