見出し画像

Go+WalkでWindowsアプリを作ってみる


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

最近業務でGo言語(golang)に触れる機会がありました。私がこれまで主に使っていたプログラミング言語は、C/C++やC#などが主で、あまり最近の(?)プログラミング言語には触れてこなかったのですが、Goはまだ新しく、名前も良く(個人的感覚)、プログラミング言語年収ランキングで1位を獲得したこともあることから、ちょっと気になっていました(ややミーハーですがw)。

Goは、デスクトップアプリやコマンドラインツールをはじめ、WebアプリのバックエンドAPIの開発や、モバイルを含んだクロスプラットフォームで動作するアプリの開発など、いろいろな用途に使われている言語ということで、この機会にGoがどんな言語でどんなアプリ開発経験(主にどれだけ快適に楽しく書けるか)が得られるのかを試してみたいなと思いたちました。

ということで、全く初心者の私が今回から数回に分けていろいろなアプリを作ってみながら、Go言語をマスターしていく様子を綴っていきたいなと思います(マスターできずに挫折するかもですが・・・)。

なお、Go言語の文法については、ネットに多くの情報がありますので、本記事および次回以降の記事では、文法や基本的なコードの書き方についての説明は行わず、具体的なアプリの開発ついて書いていきたいと思います。

Go言語とは

まずはそのGo言語がどんなプログラミング言語なのか、簡単に書いておきます。

Go言語はGoogleで作られたプログラミング言語で、設計されたのは2009年とまだ若い言語です。C言語に似たコンパイル言語で、メモリ安全性を確保し(C言語プログラムを書くときによく問題になるメモリ破壊とかの問題が起きない!?)、ガーベージコレクションを搭載し、静的型付けを行う、という点が気になる特徴です(もっとたくさん特徴はあるようですが、おいおい学んでいくことになるかと思います)。

また、繰り返し作業の記述はすべて"for"で書き("while"などは存在しない)、(バグを生みやすい)Generic型がなく、クラスの継承機能もなく、例外処理もない(似たような処理は書くことができる)など、複雑化してしまっているここ最近のプログラミングシーンをシンプルなものに戻そうという思想があるようで、書きやすく読みやすい言語になっているようです。

私の感覚では、最近の某J〇vaS〇riptなどのややこしくなってしまっている言語に比べて短い時間でマスターできるのでは、と思いました。このため、初めて勉強する言語としても向いているのかなとも感じています(まだそう結論付けるほど理解していませんが)。

また、Goのコンパイラーやそのソースコードはすべてオープンソースとして公開されていて、フリーで利用可能。また、サポートしているOSもMacOS、各種Linux、WindowsそしてAndroidおよびiOSとなっており、クロスプラットフォームなアプリ開発も可能となっています。

しかし、新しい言語のためか、初歩的な文法の解説をかいたページはネット上に見つかるものの、あまり具体的なアプリを作る方法を書いたページは多くないように思われます。そこで、これからの記事では具体的なアプリを開発しながらGo言語の習得に向かっていく形で進められたらな、と思います。

Go+WalkでWindowsアプリを作ってみる

さて、そんなGo言語を使ってアプリを作っていくこのシリーズ初回の今回は、肩慣らし的に(?)Windows上でlxn/walkというUIツールキットを使った簡単なGUIアプリの例として、タイマーアプリを作ってみたいと思います(「Hello World」ではつまらないので・・・)。

walkは、Windows用に作られたGo言語用GUIフレームワークで、無料で利用できます。ネットで検索するとたくさんの記事が見つかりますので、ユーザー数も多いのではないかなと思われます。なお、以下がGitHubのURLです。

Goのインストール

まずはGoのインストールです。WindowsへのGo言語環境のインストールは、オフィシャルのダウンロードサイトから、msi形式のインストーラーをダウンロードして、実行すればOKです。インストール時間も短く、手軽ですね。
https://golang.org/dl/

画像1

インストールが終わったら、下のようにコマンドプロンプトから「go version」と打って、バージョン情報が帰ってくることを確認してください。

> go version
go version go1.16.5 windows/amd64

プロジェクトディレクトリの準備

さっそくアプリ開発を始めようと思いますが、まずはアプリ用の空のディレクトリ(walktimer)を作成します。

mkdir walktimer
cd walktimer

そして、"go mod init"で、プロジェクトの初期化(作成?)のような作業を行います(何となくよくexamples.com/プロジェクト名 の形を目にするので真似しています)。

go mod init examples.com/walktimer

walkのインストール

ここでwalkのインストールを行っておきます。パッケージのインストールは"go get"コマンドで、GitHubのURLを指定することで行います。

go get github.com/lxn/walk

このGitHubからコードを取ってきてくれる点は、とても最近の言語っぽい感じがしますね。

なお、インストール済みのパッケージの一覧は、「go list ...」で表示できます。

コード作成

さて、タイマーアプリのコードを作成します。下のような画面のシンプルなタイマーで、分と秒の値をボタンで増減でき、あとはスタート/ストップボタンと、リセットボタンがあるだけのものです。

画像2

とりあえずVSCodeなどのIDEでフォルダを開いて、walktimer.goというファイルを作成します(VSCodeではなく、AtomなどのIDEを使ってもよいと思います)。

walkでのトップレベルウィンドウの作成は、MainWindowという構造体(というよりクラスに近いもの)を作成することで行います。そして、そのRun()メソッドをコールすることで、画面に表示されるようです。また、そのMainWindowの中にボタンなどのコンポーネントを配置し、そのハンドラーなどもその場でどんどん定義してしまいます。

ということで、MainWindowを作成するコードがほとんどを占めるソースコードになっていますねw

package main
import (
	"fmt"
	"log"
	"time"
	"github.com/lxn/walk"
	. "github.com/lxn/walk/declarative"
)
func main() {
	mw := &MyMainWindow{}
	mw.minutes, mw.seconds = 3, 0
	mw.minutesOriginal, mw.secondsOriginal = 3, 0
	mw.countingDown = false
	if _, err := (MainWindow{
		AssignTo: &mw.MainWindow,
		Title:    "Walk Timer",
		Size:     Size{640, 600},
		Layout:   VBox{},
		Children: []Widget{
			Composite{
				Layout: HBox{},
				Children: []Widget{
					Composite{
						Layout: VBox{},
						Children: []Widget{
							PushButton{
								Text:     "▲",
								Font:     Font{PointSize: 24},
								AssignTo: &mw.minutesUp,
								OnClicked: func() {
									if mw.minutes < 99 {
										mw.minutes++
									}
									mw.minutesLabel.SetText(fmt.Sprintf("%02d", mw.minutes))
								},
							},
							Label{
								Font:     Font{PointSize: 192},
								AssignTo: &mw.minutesLabel,
								Text:     "03",
							},
							PushButton{
								Text:     "▼",
								Font:     Font{PointSize: 24},
								AssignTo: &mw.minutesDown,
								OnClicked: func() {
									if mw.minutes > 0 {
										mw.minutes--
									}
									mw.minutesLabel.SetText(fmt.Sprintf("%02d", mw.minutes))
								},
							},
						},
					},
					Label{
						ColumnSpan: 1,
						Font:       Font{PointSize: 192},
						Text:       ":",
					},
					Composite{
						Layout: VBox{},
						Children: []Widget{
							PushButton{
								Text:     "▲",
								AssignTo: &mw.secondsUp,
								Font:     Font{PointSize: 24},
								OnClicked: func() {
									if mw.seconds == 59 {
										if mw.minutes != 99 {
											mw.minutes++
											mw.seconds = 0
											mw.minutesLabel.SetText(fmt.Sprintf("%02d", mw.minutes))
										}
									} else {
										mw.seconds++
									}
									mw.secondsLabel.SetText(fmt.Sprintf("%02d", mw.seconds))
								},
							},
							Label{
								Font:     Font{PointSize: 192},
								AssignTo: &mw.secondsLabel,
								Text:     "00",
							},
							PushButton{
								Text:     "▼",
								AssignTo: &mw.secondsDown,
								Font:     Font{PointSize: 24},
								OnClicked: func() {
									if mw.seconds == 0 {
										if mw.minutes != 0 {
											mw.minutes--
											mw.seconds = 59
											mw.minutesLabel.SetText(fmt.Sprintf("%02d", mw.minutes))
										}
									} else {
										mw.seconds--
									}
									mw.secondsLabel.SetText(fmt.Sprintf("%02d", mw.seconds))
								},
							},
						},
					},
				},
			},
			Composite{
				Layout: HBox{},
				Children: []Widget{
					PushButton{
						Text:     "Reset",
						AssignTo: &mw.resetButton,
						Font:     Font{PointSize: 32},
						OnClicked: func() {
							mw.minutes = mw.minutesOriginal
							mw.seconds = mw.secondsOriginal
							mw.minutesLabel.SetText(fmt.Sprintf("%02d", mw.minutes))
							mw.secondsLabel.SetText(fmt.Sprintf("%02d", mw.seconds))
						},
					},
					PushButton{
						Text:     "Start",
						AssignTo: &mw.startstopButton,
						Font:     Font{PointSize: 32},
						OnClicked: func() {
							mw.countingDown = !mw.countingDown
							if mw.countingDown {
								mw.startActions()
							} else {
								mw.stopActions()
							}
						},
					},
				},
			},
		},
	}).Run(); err != nil {
		log.Fatal(err)
	}
}
func (mw *MyMainWindow) startActions() {
	mw.startstopButton.SetText("Stop")
	mw.resetButton.SetEnabled(false)
	mw.minutesUp.SetEnabled(false)
	mw.minutesDown.SetEnabled(false)
	mw.secondsUp.SetEnabled(false)
	mw.secondsDown.SetEnabled(false)
	mw.minutesOriginal = mw.minutes
	mw.secondsOriginal = mw.seconds
	mw.timer = time.NewTicker(time.Second)
	go mw.countDown()
}
func (mw *MyMainWindow) stopActions() {
	mw.startstopButton.SetText("Start")
	mw.resetButton.SetEnabled(true)
	mw.resetButton.SetEnabled(true)
	mw.minutesUp.SetEnabled(true)
	mw.minutesDown.SetEnabled(true)
	mw.secondsUp.SetEnabled(true)
	mw.secondsDown.SetEnabled(true)
	mw.timer.Stop()
}
func (mw *MyMainWindow) countDown() {
	for {
		<-mw.timer.C
		mw.seconds--
		if mw.seconds == -1 {
			mw.seconds = 0
			if mw.minutes == 0 {
				mw.stopActions()
				break
			} else {
				mw.minutes--
				mw.seconds = 59
				mw.minutesLabel.SetText(fmt.Sprintf("%02d", mw.minutes))
			}
		}
		mw.secondsLabel.SetText(fmt.Sprintf("%02d", mw.seconds))
	}
}
type MyMainWindow struct {
	*walk.MainWindow
	minutesLabel    *walk.Label
	secondsLabel    *walk.Label
	startstopButton *walk.PushButton
	resetButton     *walk.PushButton
	minutesUp       *walk.PushButton
	minutesDown     *walk.PushButton
	secondsUp       *walk.PushButton
	secondsDown     *walk.PushButton
	minutes         int
	seconds         int
	minutesOriginal int
	secondsOriginal int
	countingDown    bool
	timer           *time.Ticker
}

ボタンなどのUIコンポーネントは"Children: []Widget{}"の中で配置します。もちろんChildrenの中にChildrenを入れて階層構造にできるので、複雑なUIの配置も可能です(よほどシンプルなアプリでない限り、多かれ少なかれ階層構造を取ることになります)。

1つ1つコンポーネントについて書いていくととても長くなるので省略しますが、なんとなく想像できるコードになっているかと思います。Webアプリのフロントエンドを作っているような感じにも見えますね。 

ただし、walkではそれほど細かくコンポーネントの位置調整はできないのと、UIコンポーネントの種類も限られている(時間の増減のためにSpinコントロールが欲しかった)ので、見栄えにこだわったアプリの作成には向いていないのかな、という印象を受けました。

(補足)walkではカスタムコントロールが作成でき、WndProcもいじれるので、やる気になれば独自コンポーネントの作成もできます。

タイマーとGo関数

さて、肝心のタイマーについてですが、まず"time"ライブラリをインポートし、"mw.timer = time.NewTicker(time.Second)"と作成しています。これで1秒ごとに通知(?)が来ることになるのですが、これを受け取るのは"go mw.countDown()"と書いている"goroutine(ゴールーチン)"です。goroutineは、スレッドを簡単に作成できる仕組みで、この場合はmw.countDownメソッドが別スレッドで動作します(メモリ空間は同じ)。このメソッドでは、"<-timer.C"という行で、タイマーからの通知がくるまで待機し、通知があればその後の行を実行するような動きをしてくれます。このようにマルチスレッドなアプリが簡単に記述できるのがGoの特徴の1つと言えるでしょう。

マニフェストファイルの作成とコンパイル

さて、walkは"Microsoft.Windows.Common-Controls"を使ってUIコンポーネントを作成していますので、アプリの実行にはマニフェストファイル(.manifestファイル)が必要となります。

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
   <assemblyIdentity version="1.0.0.0" processorArchitecture="*" name="SomeFunkyNameHere" type="win32"/>
   <dependency>
       <dependentAssembly>
           <assemblyIdentity type="win32" name="Microsoft.Windows.Common-Controls" version="6.0.0.0" processorArchitecture="*" publicKeyToken="6595b64144ccf1df" language="*"/>
       </dependentAssembly>
   </dependency>
   <application xmlns="urn:schemas-microsoft-com:asm.v3">
       <windowsSettings>
           <dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2, PerMonitor</dpiAwareness>
           <dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">True</dpiAware>
       </windowsSettings>
   </application>
</assembly>

ビルドしてできてexeファイルの"ファイル名.exe.manifest"という名前で保存して、exeと一緒の置いておいてもよいのですが、以下のコマンドでバイナリ化しておけば、コンパイル時にexe内に取り込めるようです。

go get github.com/akavel/rsrc
rsrc -manifest walktimer.manifest -o rsrc.syso

ということで、ビルドしましょう。

go build

walktimer.exeが生成されれば成功です。

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

根っからのWindows人間なので、とりあえずWindowが開いて、マウス操作に応じて何かが動けばとりあえず安心するw ということで、まだ書きなれてない感満載ではありますが、自分にとってはGo言語"はじめの一歩"でした。

次回は、クラスプラットフォームなGUIアプリを作ってみようと思います。
――――――――――――――――――――――――――――――――――

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

お問合せはお気軽に

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/