GoのGUIライブラリshinyを試す #golang


## はじめに
この記事は、2015年のGo Advent Calendarの1日目の記事です。
GoのAdvent Calendarは他にも2つあり、他の本日担当の方は以下のとおりです。

* [Go その2](http://qiita.com/advent-calendar/2015/go2):[iktakahiro](http://qiita.com/iktakahiro)さんの[dbr – Go 言語 O/R Mapper の紹介](https://eurie.co.jp/2015/12/go-lang-ormapper-dbr/)
* [Go その3](http://qiita.com/advent-calendar/2015/go3):[awakia](http://qiita.com/awakia)さんの[GoのChannelを使いこなせるようになるための手引](http://qiita.com/awakia/items/f8afa070c96d1c9a04c9)

この記事は2015年12月1日時点の情報を基に書いています。また、情報が少ない中、shinyのプロポーザルとソースコードを基に筆者なりに解釈した結果を書いていますので、間違いや勘違いを含んでいるかもしれません。間違いや勘違いを見つけた方は、ぜひコメントか編集リクエストを頂ければと思います。

なお、使用しているリビジョンは`48f611b013d6f6fbecb58f8212b1152abb23b928`です。
shinyはまだ始まったばかりのプロジェクトです。そのため、今後破壊的な修正が入ることが予想できます。
また、Go Mobileと共通のパッケージを多く使っているため、今後のGo Mobileの影響も強く受けることでしょう。
そのため、shinyを実務に使ってみるには、まだ早いでしょう。もちろん、Go Mobileも実験段階なのでまだ早いでしょう。

## shinyとは？

shinyとは、7月に[プロポーザル](https://github.com/golang/go/issues/11818)が出て、現在[golang.org/x/exp](https://github.com/golang/exp)以下で実装が進められているGoのGUIライブラリです。
Go Mobileと同様に、サブプロジェクトという扱いです。

プロポーザルにあるように、shinyは2つのレイヤーに分けて設計されています。低レイヤーの`Window`のレイヤーと、高レイヤーの`Widgets`のレイヤーです。

### Window

`Window`は、Go Mobileの`app.App`に近く、イベントチャネルからキーイベントやマウスイベントを受け取ったりします。Go Mobileについては、筆者が書いた[記事](http://klabgames.tech.blog.jp.klab.com/archives/1034818110.html)を参考にすると良いと思います。

また、`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サンプル](https://github.com/golang/exp/blob/48f611b013d6f6fbecb58f8212b1152abb23b928/shiny/example/basic/main.go)を動かしてみましょう。

まずは、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
```

以下のようなウィンドウ表示されれば、成功です。

![Kobito.xMT21T.png](https://qiita-image-store.s3.amazonaws.com/0/14952/493852f6-bd35-97de-01e1-584622f33162.png "Kobito.xMT21T.png")

再描画が走った際や、マウスクリックを行った際に、ターミナルにイベントの情報が表示されますので、ぜひ試してみてください。

```
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`が[デフォルト](https://github.com/golang/exp/blob/48f611b013d6f6fbecb58f8212b1152abb23b928/shiny/driver/gldriver/screen.go#L111)のようでした。
なお、エラーのハンドリングと開放処理（`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`の[実装](https://github.com/golang/exp/blob/48f611b013d6f6fbecb58f8212b1152abb23b928/shiny/driver/gldriver/texture.go#L45)を見る限り使っていませんでした。
第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サンプル](https://github.com/golang/exp/blob/48f611b013d6f6fbecb58f8212b1152abb23b928/shiny/example/basic/main.go)の他に2つのサンプルが良いされています。

* [tile](https://github.com/golang/exp/blob/48f611b013d6f6fbecb58f8212b1152abb23b928/shiny/example/tile/main.go)
* [goban](https://github.com/golang/exp/tree/48f611b013d6f6fbecb58f8212b1152abb23b928/shiny/example/goban)

#### tile

tileは、`Texture`をタイル状に並べるサンプルです。
また、タイル状に`Texture`を並べるだけではなく、Google Mapのようにマウスドラッグでタイルを動かすことができます。
そして、スクリーンショットを見ると、座標を描画するためにフォントを使って描画を行っていることが分かります。
そのため、`golang.org/x/image/font`を使った良い例に成っているのではないかと思います。

![Kobito.6uicnF.png](https://qiita-image-store.s3.amazonaws.com/0/14952/e6865108-d11c-c286-b9fd-8c5340f65bba.png "Kobito.6uicnF.png")

#### goban

Rob Pike氏が作った碁盤のサンプルのようです。
Goの語源が囲碁の碁からきているらしいので、さすがですね。

囲碁をAIとプレイできるわけではありませんが、クリックすると碁石を乗せる事ができます。
よく見るとクリックするたびに碁石の画像が変わる、こだわりっぷりです。
クリックするボタンを変えると、白い碁石を乗せたり、碁盤から取り除いたりできます。

このサンプルでは、マウスイベントや自分でペイントイベントを起こす方法や画像ファイルを利用する方法などが分かります。

![image](https://qiita-image-store.s3.amazonaws.com/0/14952/2a0c012f-7fe8-b2bd-de5a-4a0afe417409.png)


## 終わりに

この記事ではshinyについて、ざっとサンプルの解説を行いました。
GoでGUIライブラリは他にもありましたが、サブプロジェクトとして実装が進んでいることに期待が持てます。
GDD 2011で、GoチームのAndrewさんに「GoチームでGUIライブラリは作る予定はありますか？」という質問をして、「その予定はない」と回答してもらった記憶（細かいニュアンスは覚えてない）があります。
それを思うと、私にとってshinyの登場は感慨深いです。（ちなみに、筆者がもっている最初のGopher君のぬいぐるみは、この時質問してもらったものです。）

そして、12/6の[Go Conference 2015 Winter](http://eventdots.jp/event/573121)では、Andrewさんをお招きしてキーノートを行っていただきます。
おそらくGo Mobileに関する話が聞けるんじゃないかと思いますので、ぜひ楽しみにしててください！

さて、12/2の記事は、以下の3人の方が担当されます！

* Go:[MasashiSalvador57f](http://qiita.com/MasashiSalvador57f)さん
* Go その2:[pottava](http://qiita.com/pottava)さん
* Go その3:[atotto](http://qiita.com/atotto)さん