見出し画像

Go+wxGoでクロスプラットフォームGUIアプリを作ってみる

こんにちは、株式会社SHIFT、自動化エンジニアの水谷です。

前回はwalkを使ってWindows用GUIアプリを作ってみましたが、今回はGoがクロスプラットフォーム対応言語ということを体験してみるために、クロスプラットフォームなGUIアプリを1つ作ってみたいと思います。

作成するアプリは、(前回のコードが長かったのでw)簡単に作れそうなSHIFTブログのRSSリーダーのようなものにしてみたいと思います。


クロスプラットフォーム対応UIツールキット

前回使用したwalk(https://github.com/lxn/walk)はWindows専用のGUIパッケージ(ツールキット)でしたが、クロスプラットフォーム対応のUIツールキットも複数存在します。ざっと調べてみると、以下のようなものが見つかりました。

gotk3(GTK+ラッパー)
therecipe/qt(Qtラッパー)
wxGo(wxWidgetsラッパー)
Fyne(オリジナル?)

あとはElectron系とか、wailsなどのブラウザ系のパッケージも見つかりますが、これらは後の回で試してみることにして、今回は上の4つのどれかを使ってみようと思います。

Fyneはオリジナルのようですが、他の3つは以前からあるUIツールキットのラッパー的なものでしょう。個人的にはどれも多少は使った経験があるのですが、Qtは商用利用時にライセンスのことを少し気にしないといけない(この記事がQtの商用利用にあたるかは微妙なところではありますが)ので、残った中では利用経験の多いwxWidgetsのGoラッパーであるwxGoで行ってみたいと思います。Fyneもちょっと気になるのですが、今回は経験のあるものを使うことで苦労を減らそう、という作戦ですw

作業ディレクトリの作成とwxGoのインストール

まずは作業ディレクトリの作成です。今回はアプリ名をwxGoRSSとしましたので、その名前のディレクトリを作成します。

mkdir wxGoRSS
cd wxGoRSS

そして、"go mod init"で、プロジェクトの初期化作業を行います(ドメイン名も付けているのですが、このように書いているサンプルが多いので真似してます)。

go mod init examples.com/wxGoRSS

続いて、wxGoのインストールですが、wxGoがwxWidgetのラッパーであることから、wxWidget本体をビルドする必要があります。ビルド自体はwxGoをインストールする際に自動的にやってくれるのですが、この時gccを使って(wxWidgetのC++で書かれたソースコードの)コンパイルが行われるため、gccの実行環境が必要となります。いくつか方法がありますが、今回はMinGWを使うことにします(TDM-GCCでも大丈夫なようです)。

※gccはGNUのコンパイラ群で、C/C++以外にも、Objective-C/C++、Fortran、Ada、Go、Dのコンパイラとライブラリが含まれています。

さて、MinGWはこちらなどからダウンロードできますので、ダウンロードしてインストールしましょう。

https://mingw-w64.org/doku.php/download

コマンドプロンプトから"gcc --version"とタイプして、バージョン情報が表示されればOK。表示されない場合はgcc.exeにパスが通っているか確認してください(MinGWのインストーラーがgcc.exeへのパスを追加してくれるかどうかは失念しました)。

gccが使えるようになったら、wxGoをインストールします。

go get -x github.com/dontpanic92/wxGo/wx

この段階でwxWidgetのC++で書かれた大量のコードがビルドされますので、かなり時間がかかります。気長に待ちましょう。

インストールが終わりましたら、下のようなコードをtest.goなどの名前で作成し、"go run test.go"を実行することで空っぽのウィンドウが表示されるかを確かめておきます。

package main

import "github.com/dontpanic92/wxGo/wx"

func main() {
  app := wx.NewApp()
  frame := wx.NewFrame(wx.NullWindow, wx.ID_ANY, "wxGo")
  frame.Show()
  app.MainLoop()
}

なお、最初のビルドもそれなりに時間がかかります。

作成するアプリの内容

今回作成するのは、このSHIFT技術ブログを読むためだけのブラウザーアプリです。

下のように、左側に記事一覧が表示され、その1つをクリックすると右側に記事が表示される、というシンプルなものです。

画像1

RSSリーダーのコード作成

それではコーディングです。今回はRSSからSHIFTブログのタイトルやURLをRSSで取得して一覧表示したいので、RSSフィードリーダーが欲しいところです。そこで探してみると、"gofeed"がみつかりました。

https://github.com/mmcdole/gofeed

ざっとドキュメントを読んでみた限りでは簡単に使えそうだったので、これを使うことにしました。インストールは以下のコマンドで行いましょう。

go get github.com/mmcdole/gofeed

あとはこのパッケージをインポートしておけば、下の1行でSHIFTブログのRSSが取得できます。

feed, err := gofeed.NewParser().ParseURL("https://note.com/shift_tech/rss")

あとは、wxWidgetsのBoxSizerなどを使って、左に記事のリストを表示し、選択されれば右にWebViewで記事本体を表示する、という動作を記述するだけです。

package main

import (
	"fmt"
	"os"
	"strings"

	"github.com/dontpanic92/wxGo/wx"
	"github.com/mmcdole/gofeed"
)

func main() {
	feed, err := gofeed.NewParser().ParseURL("https://note.com/shift_tech/rss")
	if err != nil {
		fmt.Fprintln(os.Stderr, err)
		return
	}
	var lastSelectedPanel wx.Button = nil
	app := wx.NewApp()
	frame := wx.NewFrame(wx.NullWindow, wx.ID_ANY, "SHIFT blog reader", wx.DefaultPosition, wx.NewSizeT(1600, 1000))
	panel := wx.NewPanel(frame, wx.ID_ANY, wx.DefaultPosition, wx.NewSizeT(1600, 1000))
	hBox := wx.NewBoxSizer(wx.HORIZONTAL)
	vBox := wx.NewBoxSizer(wx.VERTICAL)
	vBox.AddSpacer(4)
	webview := wx.WebViewNew(panel, wx.ID_ANY, "https://note.com/shift_tech")

	for _, item := range feed.Items {
		fmt.Fprintln(os.Stdout, item.Title)
		iPanel := wx.NewPanel(panel, wx.ID_ANY, wx.DefaultPosition, wx.NewSizeT(400, 40), wx.BORDER_RAISED)
		vBoxI := wx.NewBoxSizer(wx.VERTICAL)

		var itemTitle wx.Button
		if len(item.Title) > 100 {
			buttonText := item.Title
			buttonText = strings.Replace(buttonText, "\n", "", 1)
			if buttonText[96] < 0x80 || buttonText[96] > 0xe0 {
				buttonText = buttonText[:96] + "\n" + buttonText[96:]
			} else if buttonText[95] < 0x80 || buttonText[95] > 0xe0 {
				buttonText = buttonText[:95] + "\n" + buttonText[95:]
			} else {
				buttonText = buttonText[:94] + "\n" + buttonText[94:]
			}
			itemTitle = wx.NewButton(iPanel, wx.ID_ANY, buttonText, wx.DefaultPosition, wx.NewSizeT(400, 40), wx.BORDER_NONE)
		} else {
			itemTitle = wx.NewButton(iPanel, wx.ID_ANY, item.Title, wx.DefaultPosition, wx.NewSizeT(400, 40), wx.BORDER_NONE)
		}

		itemTitle.SetToolTip(item.Link)
		itemTitle.SetBackgroundColour(wx.NewColour("lightgray"))
		vBoxI.Add(itemTitle)
		iPanel.SetSizer(vBoxI)
		vBox.Add(iPanel, 1, wx.EXPAND)
		wx.Bind(iPanel, wx.EVT_BUTTON, func(e wx.Event) {
			p := wx.ToButton(e.GetEventObject())
			webview.LoadURL(p.GetToolTipText())
			p.SetBackgroundColour(wx.NewColour("green"))
			if lastSelectedPanel != nil {
				lastSelectedPanel.SetBackgroundColour(wx.NewColour("lightgray"))
			}
			lastSelectedPanel = p
		}, itemTitle.GetId())
	}

	hBox.Add(vBox, 0, wx.LEFT, 5)
	hBox.AddSpacer(10)
	hBox.Add(webview, 1, wx.EXPAND|wx.ALL, 5)
	panel.SetSizer(hBox)

	frame.Show()
	app.MainLoop()
}

簡単にコードを見ていきますと、まず取得したRSSの情報から、各記事の情報を取り出してパネルを作る繰り返しは、下の用なfor文でループしています。

for _, item := range feed.Items {
  // パネルの作成
}

このfor文の記述のしかたはGo特有ですね。他の言語ではforeachなどを使って記述するようなところも、Goではforです。rangeは子要素の番号と子要素を返してくるのですが、番号は不要なので"_"として捨てています(子要素はitemに入ります)。

また、左に並んでいる各パネルは、枠なしのボタンを内包しており、これをクリックしたとき(wx.EVT_BUTTONイベント発生時)のハンドラーをwx.Bind()内で下のように定義しています。

		wx.Bind(iPanel, wx.EVT_BUTTON, func(e wx.Event) {
			p := wx.ToButton(e.GetEventObject())
			webview.LoadURL(p.GetToolTipText())
			p.SetBackgroundColour(wx.NewColour("green"))
			if lastSelectedPanel != nil {
				lastSelectedPanel.SetBackgroundColour(wx.NewColour("lightgray"))
			}
			lastSelectedPanel = p
		}, itemTitle.GetId())

ここで、"webview.LoadURL()"をコールして、右側にブログ記事を表示しています。記事のURLは、ボタンのツールチップに隠し入れておくという手抜き技で楽をしていますw

あとは現在選択されているパネルが分かるよう、色を緑に変える処理と、以前に選択されていた記事があれば、それをグレーに戻す処理を入れていますが、これは見ての通りですね。

あと、ブログのタイトルが長い場合はパネル内の記事名を2行で表示するよう、改行コードをねじ込んでいますが、ここのコードはかなり適当ですw まだGoの文字列操作に慣れていない、というかUnicodeなのかASCII(DBCS)なのかもよくわかってなかったりしますので(汗)、今後の課題としたいと思います。

いろいろ足りていないところもありますが、基本的な部分はストレートに、コンパクトにかけたな、と思っているのですが、いかがでしょうか? 

Ubuntuでの動作

さて、Go言語もwxGoもクロスプラットフォームに対応していますので、Ubuntu上でもビルドしてみました。が、しかし、けっこう大変でした。

まず依存関係のライブラリが足りない旨のエラーがたくさん出ますので、地道に1つずつインストールしてつぶしていく必要がありました。さらに、ここでUbuntu 20.04の場合はlibwebkitgtk-3.0のインストールが(通常の方法では)できないので苦労します。Ubuntu上でこのパッケージを使うなら、18.04かそれ以前のバージョンの方が良いかもです。また、パッケージのインストール時のビルドが、deprecatedな関数を呼んでいるという旨のWarningで止まってしまいます(3か所ほどあります)。対処するには、(wxWidgetsラッパーの)コードを書き換えるしかなく、ちょっと手間がかかります。厳密に修正するには、ちゃんとコードを理解して、deprecatedではない関数を呼び出すようにする必要がありますが、ざっと見た感じでは今回の用途には影響がなさそうだったので、その場しのぎのコード変更で回避しました。

ということで、準備に苦労はしましたが、Windows上で行ったのと同様にwxGoをインストールしてビルドし、実行してみると、このようにほぼ同じ見た目のアプリが動作することが、なんとか確認できました。

画像2

――――――――――――――――――――――――――――――――――

ということで、wxWidgetsの使い方を思い出しながらwxGoを使ってGUIアプリを作ってみましたが、UIコンポーネントのレイアウトのしやすさと柔軟さはやっぱり良いなと思いました。

ただ、インストールに手間がかかるのと、アプリのビルドも時間がかかるのと、それからUbuntu上でちょっと使いにくいという3つが問題点になるでしょうか。ビルドに時間がかかるのは私が使用しているマシンのメモリが少ないからという理由もありそうですが、wxGoは開発が1年ほど止まっていることもありますので、wxWidgetsがどうしても使いたく、Windows環境のみでOKという場合を除くと、Fyneなどの新しいパッケージの方が手軽で安心かな、とも思いました。

さて、次回はOpenCVのGoラッパー"GoCV"を使って、USBカメラからの動画の取得や、特徴点マッチングやらYOLOやらをやってみたいと思います。

――――――――――――――――――――――――――――――――――

執筆者プロフィール:水谷 裕一
大手外資系IT企業で15年間テストエンジニアとして、多数のプロジェクトでテストの自動化作業を経験。その後画像処理系ベンチャーを経てSHIFTに入社。
SHIFTでは、テストの自動化案件を2件こなした後、株式会社リアルグローブ・オートメーティッド(RGA)に出向中。RGAでは主にAnsibleやOpenshiftに関する案件をプレーイングマネジャーとして担当しながら、Ansibleの社内教育や、外部セミナー講師も行っている。
最近の趣味は電動キックボードでのお散歩。

★3分でわかるSHIFTについて

★SHIFTの導入事例はコチラ

★SHIFTの最新イベント情報はコチラ

★SHIFTの最新コラムはコチラ

お問合せはお気軽に
https://service.shiftinc.jp/contact/

SHIFTについて(コーポレートサイト)
https://www.shiftinc.jp/

SHIFTのサービスについて(サービスサイト)
https://service.shiftinc.jp/

SHIFTの導入事例
https://service.shiftinc.jp/case/

お役立ち資料はこちら
https://service.shiftinc.jp/resources/

SHIFTの採用情報はこちら   
https://recruit.shiftinc.jp/career/