はじめに
この記事は、2015年のGo Advent Calendarの1日目の記事です。
GoのAdvent Calendarは他にも2つあり、他の本日担当の方は以下のとおりです。
この記事は2015年12月1日時点の情報を基に書いています。また、情報が少ない中、shinyのプロポーザルとソースコードを基に筆者なりに解釈した結果を書いていますので、間違いや勘違いを含んでいるかもしれません。間違いや勘違いを見つけた方は、ぜひコメントか編集リクエストを頂ければと思います。
なお、使用しているリビジョンは48f611b013d6f6fbecb58f8212b1152abb23b928
です。
shinyはまだ始まったばかりのプロジェクトです。そのため、今後破壊的な修正が入ることが予想できます。
また、Go Mobileと共通のパッケージを多く使っているため、今後のGo Mobileの影響も強く受けることでしょう。
そのため、shinyを実務に使ってみるには、まだ早いでしょう。もちろん、Go Mobileも実験段階なのでまだ早いでしょう。
shinyとは?
shinyとは、7月にプロポーザルが出て、現在golang.org/x/exp以下で実装が進められているGoのGUIライブラリです。
Go Mobileと同様に、サブプロジェクトという扱いです。
プロポーザルにあるように、shinyは2つのレイヤーに分けて設計されています。低レイヤーのWindow
のレイヤーと、高レイヤーのWidgets
のレイヤーです。
Window
Window
は、Go Mobileのapp.App
に近く、イベントチャネルからキーイベントやマウスイベントを受け取ったりします。Go Mobileについては、筆者が書いた記事を参考にすると良いと思います。
また、Send
メソッドを使えばイベントを発生させることもできます。そして、Draw
メソッドをつかって描画を行うことも出来ます。
type Window interface {
// Release closes the window and its event channel.
Release()
// Events returns the window's event channel, which carries key, mouse,
// paint and other events.
//
// TODO: define and describe these events.
Events() <-chan interface{}
Sender
Uploader
Drawer
// Publish flushes any pending Upload and Draw calls to the window, and
// swaps the back buffer to the front.
Publish() PublishResult
}
BufferとTexture
shinyでは、描画に関してBuffer
とTexture
という概念が出てきます。
Buffer
はメモリ上に展開されたピクセルデータで、image.RGBA
を使って表現されます。
type Buffer interface {
// Release releases the Buffer's resources, after all pending uploads and
// draws resolve. The behavior of the Buffer after Release is undefined.
Release()
// Size returns the size of the Buffer's image.
Size() image.Point
// Bounds returns the bounds of the Buffer's image. It is equal to
// image.Rectangle{Max: b.Size()}.
Bounds() image.Rectangle
// RGBA returns the pixel buffer as an *image.RGBA.
//
// Its contents should not be accessed while the Buffer is uploading.
RGBA() *image.RGBA
}
Buffer
はTexture
にアップロードされ、Window
に描画されます。
type Texture interface {
// Release releases the Texture's resources, after all pending uploads and
// draws resolve. The behavior of the Texture after Release is undefined.
Release()
// Size returns the size of the Texture's image.
Size() image.Point
// Bounds returns the bounds of the Texture's image. It is equal to
// image.Rectangle{Max: t.Size()}.
Bounds() image.Rectangle
Uploader
}
Window
もUploader
を実装していますので、Buffer
を直接Window
にアップロードすることができます。
その際には、内部でTexture
が作られます。
実際の描画は、OpenGLやX11、Windowsなどのドライバーによって提供されています。
Go Mobileと同様に、shinyを使う側は、それを気にすることなく同じインタフェースで扱うことができます。
Widgets
Widgets
については、ピュアGoで書かれたボタンなど高レベルなGUI部品のことを指します。
プロポーザルには、JavaのAWTやSWTに対するSwingみたいなものだという説明がありました。
おそらくレポジトリを見る限りはまだ実装されておらず、現段階は低レイヤーの実装を進めているようです。
動かしてみる
サンプルを動かす
shinyのリポジトリには、いくつかサンプルがあるので動かしてみましょう。とりあえず、basicサンプルを動かしてみましょう。
まずは、shinyをgo get
してきます。OS Xの場合は、コンパイル時に警告が出ますが気にせず進んでください。
$ go get golang.org/x/exp/shiny/...
go run
をし、動かしてみましょう。
$ go run $GOPATH/src/golang.org/x/exp/shiny/example/basic/main.go
以下のようなウィンドウ表示されれば、成功です。
再描画が走った際や、マウスクリックを行った際に、ターミナルにイベントの情報が表示されますので、ぜひ試してみてください。
got lifecycle.Event{From:0x0, To:0x3, DrawContext:(*gl.context)(0xc8200a0120)}
got mouse.Event{X:447.3125, Y:350.85938, Button:1, Modifiers:0x0, Direction:0x1}
...
実装を見てみる
driver.Main
それでは、サンプルコードの実装を読んでいきましょう。
shinyを構成する基本は、以下のようにGo Mobileのapp.Main
に似たdriver.Main
を使います。
package main
import (
"golang.org/x/exp/shiny/driver"
"golang.org/x/exp/shiny/screen"
)
func main() {
driver.Main(func(s screen.Screen) {
w, err := s.NewWindow(nil)
if err != nil {
handleError(err)
return
}
for e := range w.Events() {
handleEvent(e)
}
})
}
Screen
driver.Main
には、screen.Screen
というインタフェースが渡されます。
Screen
は見ての通り、Buffer
やTexture
、Window
を作るためのインタフェースで、具体的な実装はgldriver
、windriver
、x11driver
などで提供されています。
type Screen interface {
// NewBuffer returns a new Buffer for this screen.
NewBuffer(size image.Point) (Buffer, error)
// NewTexture returns a new Texture for this screen.
NewTexture(size image.Point) (Texture, error)
// NewWindow returns a new Window for this screen.
NewWindow(opts *NewWindowOptions) (Window, error)
}
Windowの作成
まずサンプルコードでは、Screen
経由でWindow
を作ります。引数を省略すると、ドライバーに依存したサイズのWindow
が作られます。gldriver
では1024x768
がデフォルトのようでした。
なお、エラーのハンドリングと開放処理(Release
)を忘れないようにしましょう。
w, err := s.NewWindow(nil)
if err != nil {
log.Fatal(err)
}
defer w.Release()
Bufferの作成
次に、Buffer
を作っています。256x256
の大きさのBuffer
にdrawGradient
関数を使ってグラデーションを描画しています。
グラデーションの描画には、Buffer
インタフェースから取得できる、*image.RGBA
を使っています。
これは標準パッケージのimage
パッケージで提供されるデータ型です。
そのため、drawGradient
関数はGoの標準のimage
パッケージを使った画像描画の方法と同じ方法で描画しています。
winSize := image.Point{256, 256}
b, err := s.NewBuffer(winSize)
if err != nil {
log.Fatal(err)
}
defer b.Release()
drawGradient(b.RGBA())
Textureの作成とBufferのアップロード
作成したBuffer
は、Texture
を作って、アップロードすることができます。
Upload
メソッドの第1引数のdp
はおそらくBuffer
をテクスチャのどこにアップロードするか座標を指定するために使うものだとは思いますが、現在のgldriver
の実装を見る限り使っていませんでした。
第2引数はアップロードするBuffer
で、第3引数はアップロードするBuffer
の領域を指定します。
なお、Texture
にアップロードされたBuffer
はすぐにWindow
に描画される訳ではなく、後で説明するDraw
メソッドを使って描画を行う必要があります。
t, err := s.NewTexture(winSize)
if err != nil {
log.Fatal(err)
}
defer t.Release()
t.Upload(image.Point{}, b, b.Bounds(), w)
イベントのハンドリング
実際の描画は、ペイントイベント(paint.Event
)が発生した際に行います。
イベントは、Go Mobileと同様にチャネルから送られてきます。
イベントチャネルは、Window
インタフェースのEvents
メソッドから取得できます。
イベント自体は、Go Mobileのイベントを使うようで、ハンドリング方法も同じでfor
でイベントチャネルからイベントを受取り、型スイッチで分岐して、それぞれのイベントをハンドリングします。
通常、画面のサイズが決定した後に描画イベントが発生します。
画面のサイズは、サイズイベント(size.Event
)から取得できます。
サンプルコードでは、sz
という変数にsize.Event
を保存していました。
shinyでは、ダブルバッファリングを行っているため、ペイントイベント(paint.Event
)が発生した後に、Window
インタフェースのPublish
メソッドを呼び出さないと実際の描画が行われません。
そのため、ペイントイベントのハンドリングの最後に必ずPublish
メソッドを呼び出してください。
描画処理
Window
インタフェースのFill
メソッド(Uploader
インタフェースのメソッド)を使うと、指定した色でWindow
を塗りつぶしてくれます。
サンプルコードでは、暗い青(blue0
)で塗りつぶした後に、明るい青(blue1
)で10px
マージンをとって内側を塗りつぶしています。
w.Fill(sz.Bounds(), blue0, draw.Src)
w.Fill(sz.Bounds().Inset(10), blue1, draw.Src)
サンプルコードでは、作成したTexture
のt
を使わずに直接Buffer
をWindow
にアップロードしています。
Upload
メソッドの引数はTexture
を介した場合と同じです。
Buffer
を直接Window
にアップロードした場合、指定したBuffer
の領域を描画する為に最適な大きさのTexture
が内部で作られます。
そして、Window
インタフェースのDraw
メソッド(Drawer
インタフェースのメソッド)を呼び出し、Window
への描画まで行います。
w.Upload(image.Point{}, b, b.Bounds(), w)
以下のようにサンプルコードでは、Buffer
を直接Window
にアップロードする例の他に、Texture
にアップロードしてから、自分でDraw
メソッドを呼び出す例もあります。
ここでは、単に描画するだけではなくアフィン変換行列(f64.Aff3
)を設定して回転と平行移動を行っています。
このように、変形して描画したい場合は、自分でTexture
を作って、Buffer
をアップロードし、Draw
メソッドで描画まで行います。
c := math.Cos(math.Pi / 6)
s := math.Sin(math.Pi / 6)
w.Draw(f64.Aff3{
+c, -s, 100,
+s, +c, 200,
}, t, t.Bounds(), screen.Over, nil)
そのフレームでの各描画処理が終わったら、上述のとおりWindow.Publish
メソッドを呼び出し、画面に描画を行います。
他のサンプルをみてみよう
現在shinyでは、basicサンプルの他に2つのサンプルが良いされています。
tile
tileは、Texture
をタイル状に並べるサンプルです。
また、タイル状にTexture
を並べるだけではなく、Google Mapのようにマウスドラッグでタイルを動かすことができます。
そして、スクリーンショットを見ると、座標を描画するためにフォントを使って描画を行っていることが分かります。
そのため、golang.org/x/image/font
を使った良い例に成っているのではないかと思います。
goban
Rob Pike氏が作った碁盤のサンプルのようです。
Goの語源が囲碁の碁からきているらしいので、さすがですね。
囲碁をAIとプレイできるわけではありませんが、クリックすると碁石を乗せる事ができます。
よく見るとクリックするたびに碁石の画像が変わる、こだわりっぷりです。
クリックするボタンを変えると、白い碁石を乗せたり、碁盤から取り除いたりできます。
このサンプルでは、マウスイベントや自分でペイントイベントを起こす方法や画像ファイルを利用する方法などが分かります。
終わりに
この記事ではshinyについて、ざっとサンプルの解説を行いました。
GoでGUIライブラリは他にもありましたが、サブプロジェクトとして実装が進んでいることに期待が持てます。
GDD 2011で、GoチームのAndrewさんに「GoチームでGUIライブラリは作る予定はありますか?」という質問をして、「その予定はない」と回答してもらった記憶(細かいニュアンスは覚えてない)があります。
それを思うと、私にとってshinyの登場は感慨深いです。(ちなみに、筆者がもっている最初のGopher君のぬいぐるみは、この時質問してもらったものです。)
そして、12/6のGo Conference 2015 Winterでは、Andrewさんをお招きしてキーノートを行っていただきます。
おそらくGo Mobileに関する話が聞けるんじゃないかと思いますので、ぜひ楽しみにしててください!
さて、12/2の記事は、以下の3人の方が担当されます!
- Go:MasashiSalvador57fさん
- Go その2:pottavaさん
- Go その3:atottoさん