Go
GoDay 1

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

More than 1 year has passed since last update.

はじめに

この記事は、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では、描画に関してBufferTextureという概念が出てきます。
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
}

BufferTextureにアップロードされ、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
}

WindowUploaderを実装していますので、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

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

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は見ての通り、BufferTextureWindowを作るためのインタフェースで、具体的な実装はgldriverwindriverx11driverなどで提供されています。

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の大きさのBufferdrawGradient関数を使ってグラデーションを描画しています。
グラデーションの描画には、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)

サンプルコードでは、作成したTexturetを使わずに直接BufferWindowにアップロードしています。
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を使った良い例に成っているのではないかと思います。

Kobito.6uicnF.png

goban

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

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

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

image

終わりに

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

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

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