Ubuntu Weekly Recipe

第351回GoとQMLとGUIアプリ

Go言語、流行ってますね。今回はGo言語とQMLを組み合わせて、簡単にGUIアプリケーションを作れるgo-qmlについて紹介しましょう。

go-qmlとは?

go-qml(qmlパッケージ⁠⁠」はGo言語からQMLを使うためのGoパッケージです。Goプログラムの中でUIとしてQMLを利用できたり、QMLの拡張をGo言語で作成することができます。開発者はCanonical社員のGustavo Niemeyerで、Debian/Ubuntu上で最新のGo言語を導入するためのgodebやJujuのGo対応なども行っている人物です。

Go言語やQMLについてはRecipeの読者なら名前ぐらいは聞いたことがあるでしょうし、よく使っているという方もそれなりにいらっしゃると思いますので釈迦に説法になりそうではありますが、両者についても簡単に説明しておきます。

Go言語はシンプルな仕様と豊富な標準パッケージを兼ね備えたプログラミング言語で、容易にクロスプラットフォームなバイナリを構築・配布できます。UbuntuのJujuはもちろんのこと、Dockerをはじめとするさまざまなプロジェクトで利用されています。

QMLは、これまたクロスプラットフォームなアプリケーション開発プラットフォームであるQtで使われているJavaScriptとCSSを組み合わせたような構文の言語です。Qt自体はC++で開発されていますが、UIの部分をQMLで記述することでより柔軟かつ効率的にGUIアプリケーションを作成できます。Qt/QMLは非常に多くの環境で利用されており、UbuntuでもKDEだけでなくUbuntu Touchでも重要なコンポーネントの1つとなっています。

今回紹介するgo-qmlは、QMLの部分はそのままにQMLをC++で拡張していた部分をGo言語で置き換えるためのパッケージです。これにより、QMLとGo言語の良いとこ取りをしたアプリケーション開発が行えるのです[1]⁠。

ちなみにgo-qmlは例外条項付きのLGPLv3ライセンスされています。通常のLGPLなライブラリを静的にリンクした場合、リンクしたアプリケーション側の対応するコードを何らかの形で伝えるなどの方法を取る必要があります(LGPLv3の4のd⁠⁠。Go言語の場合、パッケージはすべて静的にリンクするためこれが適用されるのですが、go-qmlは上記の例外条項によって4のdや4のeなどを行わなくても良いようになっています。これによりGo言語で作った他のバイナリと同様に、バイナリ単体での配布が可能です[2]⁠。また、同じくLGPLでライセンスされているQtについては動的にリンクされます[3]⁠。

開発環境の準備

では最初に開発環境を構築しましょう。go-qmlはQMLを使うため、QMLエンジンとしてのQtも必要です。逆に言うと、Go 1.2以上とQt 5.x以上さえ正しくインストールできる環境であれば、ここで説明するようなUbuntuでなくても他のOSやディストリビューションでも同様に利用できます。実際、go-qmlのページには、Mac OS XやWindowsのインストール方法も記載されています。

Goのインストール

Goのインストール方法はいくつか存在します。一番簡単なのがパッケージ管理システムからインストールする方法です。

$ sudo apt install git bzr golang-go

ここでgitとbzrもインストールしているのは、後に出てくる「go get」コマンドでgitやbzrを使ってソースコードを取得することがあるためです。

もしクロスプラットフォームなコンパイル環境も構築したいのであれば、それぞれのプラットフォームのライブラリパッケージもインストールしましょう。たとえばUbuntu上で、Windowsの32ビット版バイナリを作成したい場合は「golang-go-windows-386」パッケージをインストールします。またgodocやoracleなどを使いたい場合は「golang-go.tools」パッケージをインストールしてください。

Ubuntuのパッケージ管理システムからインストールできるGoは、14.04、14.10ともにバージョン1.2です。go-qmlを試すだけであれば1.2でも問題ありませんが、他の要因で1.3以上を使いたい場合はgodebを使ってインストールすると良いでしょう。

$ wget https://godeb.s3.amazonaws.com/godeb-amd64.tar.gz
$ tar xvf godeb-amd64.tar.gz
$ ./godeb -h
$ ./godeb install

32ビット版のUbuntuを使っている場合は、⁠amd64」の部分を「386」に置き換えてください。⁠i386」ではなく「386」です。今回は展開したディレクトリで実行していますが、/usr/local/binなどにコピーして実行しても問題ありません。

Qtのインストール

Ubuntuのパッケージ管理システムを使ってQtをインストールする場合、Ubuntu 14.04 LTSだと5.2.xが、Ubuntu 14.10だと5.3.xがインストールされます。go-qmlを動かすにあたってはどちらでも問題ありません。もし5.3.xに統一したい場合は、14.04に次のようにPPAを追加したうえで、Qtの開発パッケージをインストールしてください。

$ sudo add-apt-repository ppa:ubuntu-sdk-team/ppa
$ sudo apt update
$ sudo apt install qtdeclarative5-dev qtbase5-private-dev \
    qtdeclarative5-private-dev libqt5opengl5-dev \
    qtdeclarative5-qtquick2-plugin

けっこうたくさんのパッケージがインストールされますので、モバイル通信を使っている場合は注意しましょう。

go-qmlのインストール

最後にgo-qmlのインストールです。これは、Goの他の外部パッケージと同じように「go get」コマンドで取得します。⁠go get」コマンドを使う場合は、あらかじめ環境変数GOPATHを設定しておきましょう。GoパッケージはこのGOPATH以下にインストールされます。ユーザーがアクセス可能な領域であればどこを指定してもかまいません。

$ mkdir ~/gocode
$ export GOPATH=$HOME/gocode
$ go get gopkg.in/qml.v1

これで~/gocode以下に、go-qmlのコードがダウンロードされたことがわかります。なおプロキシ内部の環境の場合は、http_proxyやhttps_proxyを適切に設定しておいてください。

GOPATHはgo getでインストールしたパッケージを利用してビルドするときにも必要になります。一度作成したら、~/.bashrcなどに上記のexport文を設定しておくと良いでしょう。

Goのテスト

go-qmlを試す前に、Goでビルドできるかどうか試しておきましょう。まず次のような内容で、yahallo.goを作成します。

package main

import "fmt"

func main() {
    fmt.Println("やっはろー")
}

ビルドするだけならbuildコマンドを、ビルドして実行するならrunコマンドを実行します。

$ go run yahallo.go
やっはろー

クロスプラットフォームについて

Goはクロスアーキテクチャだけでなくクロスプラットフォームビルドにも対応しています。たとえば「golang-go-windows-386」パッケージをインストールしておけば、64ビット版のUbuntuで、32ビット版Windows向けバイナリも作成できるのです。

$ sudo apt install golang-go-windows-386
$ GOOS=windows GOARCH=386 go build yahallo.go
$ file yahallo.exe
yahallo.exe: PE32 executable (console) Intel 80386 (stripped to external PDB), for MS Windows

あとはこのyahallo.exeファイルだけをWindowsにコピーして、コマンドプロンプトなどで実行すればちゃんと動きます。ちょっとUbuntu上でWindowsの実行ファイルを作らなくてはいけなくなったときなどに、Goはとても有力な選択肢の1つとなるはずです。

go-qmlそのものはクロスプラットフォーム・クロスアーキテクチャなビルドには対応していません。これはGoとは別に各プラットフォームのQtライブラリを用意しなくてはいけないためです。残念ですが各プラットフォームのバイナリを作成するには、各プラットフォームでビルド環境を構築してください。ただし原則的に、同じソースコードがそのまま動くはずです。

サンプルアプリケーションのビルドと実行

では、実際に何かアプリケーションを作ってみましょう。必要なのは以下の3つの知識です。

Go言語
Goの公式サイトその日本語訳には、Goを学習するためのドキュメントや各種パッケージのリファレンス、言語仕様が掲載されていますので、Goの初心者ならまずはこれらを眺めながら使い方を覚えて行くことになるでしょう。godocコマンドも便利です。またWEB+DB PRESS Vol.82でもGo言語の特集を行っているので、そちらも参考になるでしょう。
QML
UIの部分を担当するQMLは、Qt Projectに各種QMLタイプのリファレンスやサンプルコードが載っています。また日本語の情報としては、Qt QuickではじめるクロスプラットフォームUIプログラミングやその追補版的位置づけのQt Quickを使いこなすクロスプラットフォームUIプログラミングもおすすめです。
qmlパッケージ
qmlパッケージを使う場合、main関数の内外でいくつか初期化処理を行う必要があります。またGo側から各種QMLタイプのメソッドやプロパティ、シグナルにアクセスする方法や逆にQML側にGo側の変数やタイプを公開する方法を把握しておく必要があります。go-qmlのドキュメントを一通り読んだうえで、ソースコードのexampleフォルダーの中身を見ておくと良いでしょう。

Webページを表示する

WebViewを使ったサンプルを作成してみます。WebViewはQMLタイプの1つでWebKitエンジンを使ったWebブラウザー機能をアプリケーションに提供します。次のような内容のファイルをwebview.goという名前で作成してください。

package main

import (
    "fmt"
    "gopkg.in/qml.v1"
    "os"
)

const webview = `
import QtQuick 2.0
import QtWebKit 3.0

WebView {
    width: 1024
    height: 768
}
`

func main() {
    if len(os.Args) != 2 {
        fmt.Fprintf(os.Stderr, "Usage: %s <url>\n", os.Args[0])
        os.Exit(1)
    }
    if err := qml.Run(run); err != nil {
        fmt.Fprintf(os.Stderr, "Error: %v\n", err)
        os.Exit(1)
    }
}

func run() error {
    engine := qml.NewEngine()
    component, err := engine.LoadString("webview.qml", webview)
    if err != nil {
        return err
    }
    win := component.CreateWindow(nil)
    root := win.Root()
    root.On("loadingChanged", func() { fmt.Println("Page Loaded") })
    root.Set("url", os.Args[1])
    win.Show()
    win.Wait()
    return nil
}

あとはコマンド上で、ビルドして実行してみます。

$ go run webview.go http://gihyo.jp
図1 WebViewなのでリンクをクリックすればページ遷移もできる
図1 WebViewなのでリンクをクリックすればページ遷移もできる

具体的な処理内容を見ていきましょう。まずはmain()の中で呼ばれているqml.Run()です。これは内部でQGuiApplicationインスタンスを作成し、引数のrun()を呼び出すイベントループをゴルーチンとして立ち上げつつ、QGuiApplication::exec()を呼び出します。go-qmlの(というよりはMac OS Xの)制約により、qml.Run()はmain()と同じゴルーチンの中で呼び出さなければなりません。よって、この処理はどのアプリケーションでも同じように記述すると思っておけば良いでしょう。なおqmlパッケージのさまざまなメソッドは、qml.Run()が呼ばれるまでは処理を停止します。

アプリケーションのメインルーチンとなるのがrun()の内部です。最初に行っているqml.NewEngine()はQQmlEngineを保持する構造体を作成しています。

engine.LoadString()はQMLデータを読み込むメソッドです。go-qmlは2つの方法でQMLデータを読み込めます。1つはサンプルのようにLoadString()を使って文字列から読む方法、もう1つはLoadFile()を使ってファイルから読む方法です。内部的に前者はQQmlComponent::setData()を、後者はQQmlComponent::loadUrl()を呼び出しています。どちらもQObject(QQmlComponent)を返します。今回はあらかじめ文字列定数webviewにQMLを指定してあるのでLoadString()を使っています。

component.CreateWindow()は作成済みのQQmlEngineを使ってQQuickViewのインスタンスを作成し、それを返しています。第一引数にはQMLコンテキストを渡すことが可能です。win.Root()はQQuickView::rootObject()相当の機能で、ここではQMLのルートオブジェクト(つまりWebViewタイプ)が返ることになります。

root.On()ではオブジェクトの「loadingChanged」シグナルを指定した関数にconnect()しています。root.Set()はオブジェクトの「url」プロパティを変更しています。これによりWebViewではコマンドの引数に指定したURLがロードされ、OnLoadingChangedが呼ばれた結果、端末に「Page Loaded」が表示されることになります。

win.Show()はウィンドウの表示で、win.Wait()はウィンドウが閉じられるまで待つメソッドです。ウィンドウが閉じられるとrun()関数から戻り、qml.Run()で立ち上げたゴルーチンが終了し、QGuiApplication::exit()が呼び出されます。

Goの型をQMLで利用する

qml.RegisterTypes()を利用するとGoの型をQMLの中で利用できます。前述のサンプルのwebviewの部分を次のコードで置き換えてください。

const webview = `
import QtQuick 2.0
import QtWebKit 3.0
import Matcher 1.0

WebView {
    width: 1024
    height: 768

    Matcher {
        id: matcher
        objectName: "matcher"
        text: ""
    }

    onLinkHovered: {
        matcher.check(hoverUrl)
    }
}
`

type Matcher struct {
    Text string
}

func (m *Matcher) Check(url string) {
    if len(m.text) > 0 && strings.Contains(url, m.Text) {
        fmt.Println(url)
    }
}

さらにrun()を次のように変更します。

func run() error {
    qml.RegisterTypes("Matcher", 1, 0, []qml.TypeSpec{{
        Init: func(m *Matcher, obj qml.Object) {},
    }})
    engine := qml.NewEngine()
    (中略)
    root.Set("url", os.Args[1])
    matcher := root.ObjectByName("matcher")
    matcher.Set("text", "ubuntu")
    win.Show()
    (後略)

また、import文にstringsパッケージも追加しておきます。

この状態でwebview.goをビルドして実行すると、⁠ubuntu」が含まれるリンクにマウスカーソルを合わせたときに、端末にそのURLが表示されるようになります。

Goで作成した型をQML側に表示させるにはqml.RegisterTypes()を使います。最初の3つの引数はQMLでインポートするときのモジュール名とメジャー番号、マイナー番号です。4つ目の引数に公開したいGoの型のQMLでインポートされる際の初期化関数をそれぞれ記述していきます。今回はMatcher型を公開したいものの、初期化コードは不要なため空の関数を登録しています。

webview変数で指定しているQMLの中ではMatcherタイプとして登録されたGoの構造体やメンバーにアクセスできます。このときメンバー名やメソッドの最初の文字は小文字になることに注意してください[4]⁠。run()関数の後半では、このMatcherオブジェクトをObjectByName()で取得し、そのtextプロパティを「ubuntu」に変更しています。

さらにMatcher構造体にはCheck()というメソッドを定義しています。これは渡された文字列の中に、Matcher.Textで指定した文字が存在するかどうかをチェックし、存在するなら標準出力にそのURLを出力しています。QML側からもこのメソッドにはアクセス可能で、onLinkHoveredシグナルハンドラの中でhoverdUrlをCheck()に渡しているので、リンクにマウスカーソルを合わせる度にURLを確認できるのです。

公式のサンプル集

go-qmlのコードは「go get」実行時に「$GOPATH/src/gopkg.in/qml.v1/」以下に展開されています。前述したように、ここのexampleフォルダーにはいくつかのサンプルコードがありますので、適当な場所にコピーしてそれぞれビルドして動作を試してみると良いでしょう。

図2 gopherサンプルでは、OpenGLを用いてGopherの3Dモデルを表示できる
図2 gopherサンプルでは、OpenGLを用いてGopherの3Dモデルを表示できる

その他の情報

Ubuntu SDKもgo-qmlを使ったアプリケーションの開発に対応しています。Ubuntu SDKを使えば、Ubuntu Touch用のClickパッケージとして作成できるうえに、schrootを使ったクロスアーキテクチャなビルドにも対応しています。

QMLはQt Creatorを使って編集するといろいろ便利ですが、Goのほうは普段使っているエディターのほうが良いかもしれません。たとえばVimならvim-go-extraと言ったプラグインが用意されているようです。ちなみにシンタックスハイライトだけで良いのであれば、以下のようにUbuntuのパッケージを使うという手もあります。

$ sudo apt install vim-syntax-go
$ vim-addon-manager install go-syntax

QMLファイルを別ファイルにする場合、Goでビルドしたバイナリとは別にQMLファイルもセットで配布する必要があります。UIファイルがテキストファイルのままだと、いろいろとデバッグ用途には便利な反面、配布時に一手間増えてしまいます。そこでQMLファイルをqrcにしてGoのバイナリに取り込んでしまう方法を、go-qmlの開発者のブログで紹介しています。同じブログにはgo-qmlを使ったアプリケーション開発コンテストの結果もありますので、サンプルの1つとして参考になるでしょう。

GoではなくRubyを使いたい場合はruby-qmlというものが存在するようです。

おすすめ記事

記事・ニュース一覧