見出し画像

Go+Wailsでデスクトップアプリを作ってみる


こんにちは、株式会社SHIFT、自動化エンジニアの水谷です。
相変わらず時間を見つけてはオープンソースで開発されている便利なパッケージを使いながらGo言語を楽しく勉強しているのですが、今回はWailshttps://wails.app/)というデスクトップアプリ開発用パッケージを使ってみたいと思います。

※(宣伝)SHIFTエンジニアが学びながら解説するGo言語というマガジンができました。過去の記事もこちらから読めますので、もし本記事に興味をもっていただけましたら、マガジンの方も是非見ていただければと思います。

さて、Wailsは一般的なデスクトップアプリ開発用パッケージとは異なり、Webアプリを開発するような感覚で、バックエンドをGo、フロントエンドをHTML/JavaScript/CSSで開発し、ビルドコマンドがこれらを1つの実行形式のバイナリにまとめてくれるという代物です。

Webアプリを実行形式バイナリ化する、と言うとElectronを思い浮かべる方も多いかと思いますが、WailsはバックエンドがGoであるだけでなく、内部でブラウザを動かさずに動作する点や、フロントエンドからGoで作成した関数が(シームレスに、とまではいかないものの)自然な感じで呼び出せることが特徴のようです。

今回も引き続きマンデルブロー集合の計算コードを使って、Windows上でWailsを試してみたいと思います。

画像1


Wailsのインストール

まずはWailsのインストールからですが、Wailsはcgo(GoからCで作られたコードを呼び出す仕組み)を使っていることもあり、gccが必要となります。Windows環境にインストールする場合は、以前のwxGoGoCVの記事などを参考にしてMinGWあるいはTDM-GCCなどをインストールしてgccコマンドが使えるようにしておいてください。

また、フロントエンド側の雛形作成やビルドにnpmも内部で使用するようなので、node.jsをインストールするなどして、npmコマンドが使えるようにしておきましょう。

では、"go get"を実行してWailsのインストールに進みます。

>go get -u github.com/wailsapp/wails/cmd/wails

たくさんの関連パッケージも合わせてダウンロードされますので、少し時間がかかります。

これが終わりましたら、"wails"というコマンドラインアプリが使用できるようになりますので、"setup"オプションで実行して基本設定を行います。

>wails setup
_       __      _ __
| |     / /___ _(_) /____
| | /| / / __ `/ / / ___/
| |/ |/ / /_/ / / (__  )  v1.16.7
|__/|__/\__,_/_/_/____/   https://wails.app
The lightweight framework for web-like apps

Welcome to Wails!

Wails is a lightweight framework for creating web-like desktop apps in Go.
I'll need to ask you a few questions so I can fill in your project templates and then I will try and see if you have the correct dependencies installed. If you don't have the right tools installed, I'll try and suggest how to install them.

What is your name (mizutani):
What is your email address (mizutaniyuichi@rg-automated.jp):

Wails config saved to: C:\Users\yuichi.mizutani.03\.wails\wails.json
Feel free to customise these settings.

Detected Platform: Windows
Checking for prerequisites...
Program 'gcc' found: C:\TDM-GCC-64\bin\gcc.exe
Program 'npm' found: C:\Program Files\nodejs\npm.cmd

Ready for take off!
Create your first project by running 'wails init'.

ここで、ユーザー名やメールアドレスなどを入力すると、初期設定が行われます(ユーザー名などはあとで変更可能)。

続いてwailsコマンドを"init"オプションで実行してプロジェクトを作成します。

>wails init
Wails v1.16.7 - Initialising project

The name of the project (My Project): wailsMandel
Project Name: wailsMandel
The output binary name (wailsmandel):
Output binary Name: wailsmandel
Project directory name (wailsmandel):
Project Directory: wailsmandel
Please select a template (* means unsupported on current platform):
 1: Angular - Angular 8 template (Requires node 10.8+)
 2: React JS - Create React App v4 template
 3: Svelte - A basic Svelte template
 4: Vanilla - A Vanilla HTML/JS template
 5: * Vue3 Full - Vue 3, Vuex, Vue-router, and Webpack4
 6: * Vue3 JS - A template based on Vue 3, Vue-router, Vuex, and Webpack5
 7: Vue2/Webpack Basic - A basic Vue2/WebPack4 template
 8: Vuetify1.5/Webpack Basic - A basic Vuetify1.5/Webpack4 template
 9: Vuetify2/Webpack Basic - A basic Vuetify2/Webpack4 template
Please choose an option [1]: 2
Template: React JS
> Generating project...
Project 'wailsMandel' initialised. Run `wails build` to build it.

ここで、プロジェクト名を入力して(今回は"wailsMandel"としました)、その後フロントエンドをどのフレームワークを使って作るかを選択します。メジャーなAngular、React、Vueはすべてサポートされており、さらに、SvelteやVuetifyも選択できるようです(ちなみに、私はこれらがどんなものか知りません)。

今回は2番の"React JS"を選びました。

これでHello World的なアプリのソースコードがテンプレートから作成されます。さっそく"wails build"でビルドできるかどうか試してみます。

wailsmandel>wails build
Wails v1.16.7 - Building Application

> Skipped frontend dependencies (-f to force rebuild)
> Building frontend...
> Ensuring Dependencies are up to date...
> Packing + Compiling project...
*** Please note: Windows builds use mshtml which is only compatible with IE11. For more information, please read https://wails.app/guides/windows/ ***
Awesome! Project 'wailsMandel' built!

ビルドが成功すると、buildフォルダに実行形式バイナリファイル(この場合は"wailsMandel.exe")が生成されますので、実行してみます。

画像2

このようにアプリがウィンドウで表示され、特に問題なく実行できました。これで準備は完了です。

フロントエンドを作る

では、アプリ本体を実装していきたいと思います。まずは、バックエンドの方は前回Echoで作ったものをそのまま使い(つまり、バックエンドとはWeb APIで通信する)、フロントエンドだけをテンプレートから変更して、期待通り動作するかどうか見てみたいと思います。

前々回の記事でReactで作ったフロントエンドを作っていますので、(手抜きをして?)その時の"App.js"をそのまま持ってきて置き換えてみます。ビルドしてみると、あっさりビルドが通ります。コード変更なしで、デスクトップアプリ化ができたのでは、と期待してしまうのですが……。

残念ながら、実行して見ると下のようなJavaScriptエラーが表示されます。

画像4

ここで重要な事実に気づいたのですが、それはWailsはWindows上においてはMSHTMLを使ってUIをレンダリングしていることです。MSHTMLは、IE11のレンダリングエンジンであり、つまりWailsは(Windowsにおいては)IE11と同様のいろいろな制約があることを意味します。その制約の1つがこの"fetch()"が使えないことです。

これに対する対応策は、ネットで検索すればすぐに見つかり、(不足しているfetchを追加するためのPolyfillを)以下の1行をApp.jsの先頭に追加すればよいことがわかりました。

import "fetch-polyfill";

これで動くか、と思いきや残念ながらそうではありませんでした。次に出てきたエラーがこちらです。

画像3

ネットで調べてみても、これに対する解決方法は見つけられず、いろいろコードを変更しながら調査してみると、どうやらMaterial-UIのコンポーネントを使っているとエラーが出るように見えました。

それでは、と、Semantic UIという別のUIパッケージを使ってみても同じエラーが出ました。

ちょっと行き詰ってしまったので、Material-UIを使うことをあきらめて、通常の<input>タグや<button>タグを使ってフォームを作ることにし、とりあえずエラー回避を行いました(ちょっとくやしい!)。

そのコードがこちらです。

import "es6-promise/auto";
import "fetch-polyfill";
import React, { useState } from "react";
import './App.css';

function App() {
 const [image, setImage] = useState("");
 const [realMin, setRealMin] = useState("-2.0");
 const [realMax, setRealMax] = useState("1.0");
 const [imaginaryMin, setImaginaryMin] = useState("-1.5");
 const [imaginaryMax, setImaginaryMax] = useState("1.5");
 const [isFirstTime, setIsFirstTime] = useState(true)
 const [isDisabled, setIsDisabled] = useState(false)

 const handleGenerate = () => {
   const queryParams = new URLSearchParams({realmin: realMin, realmax: realMax, imaginarymin: imaginaryMin, imaginarymax: imaginaryMax})
   var apiurl = 'http://localhost:8080/GetMap?'
   fetch(apiurl + queryParams , {method: 'GET', mode: 'cors'})
     .then((res) => { 
       return res.text() 
     })
     .then((data) => {
         setImage("data:image/png;base64," + data)
     })
     .catch((reason) => {
         console.log(reason);
     })
     .finally(()=> {
       setIsDisabled(false)
     })
 }

 if (isFirstTime) {
   setIsFirstTime(false)
   handleGenerate(null)
 }

 return (
   <div id="app" className="App">
     <img width="800" height="800" src={image} alt="..." />

     <table align="center">
       <tr>
         <td>RealMin:</td>
         <td><input type="text" defaultValue={realMin} name="realmin" onChange={(e) => setRealMin(e.target.value)}/></td>
         <td>RealMax:</td>
         <td><input type="text" defaultValue={realMax} name="realmax" onChange={(e) => setRealMax(e.target.value)}/></td>
         <td>
           <button onClick={() => handleGenerate()} type="button">
             Generate
           </button>
         </td>
       </tr>
       <tr>
         <td>ImaginaryMin:</td>
         <td><input type="text" defaultValue={imaginaryMin} name="imaginarymin" onChange={(e) => setImaginaryMin(e.target.value)}/></td>
         <td>ImaginaryMax:</td>
         <td><input type="text" defaultValue={imaginaryMin} name="imaginarymax" onChange={(e) => setImaginaryMax(e.target.value)}/></td>
       </tr>
     </table>

   </div>
 );
}

export default App;

"wails build"でビルドを行い実行してみると、なんとか動くようになりました。

画像5

Material-UIやSemantec UIをWails上で使う方法もあるのかもしれませんが、まだ見つけられていませんので、とりあえずこれで進めたいと思います。

バックエンドをポーティングする

続いて、別バイナリとして動かしていたバックエンドをアプリに取り込みます。

さきほど"wails init"コマンドを実行した際に"main.go"というファイル名のソースコードが追加されており、ここにはmain()関数が存在しますので、これを見てみましょう。

func main() {

	app := wails.CreateApp(&wails.AppConfig{
		Width:  1024,
		Height: 7688,
		Title:  "wailsMandel",
		JS:     js,
		CSS:    css,
		Colour: "#131313",
	})
	app.Bind(Basic)
	app.Run()
}

main()関数で行っているのは、"app := wails.CreateApp()"を呼び出して、アプリのインスタンスを作成することと、app.Bind()を呼び出して、Goで作成した関数をJavaScriptからコールできるようにバインドする作業、それからapp.Run()を呼び出してアプリを実行することの3つです。

app.Bind()では、引数にGoの関数名(あるいはStructでも可)を指定するだけで、自動的に引数や戻り値の型調整を行ってくれるようで、とても便利です。今回は"GetMap()"という名前の(Goで作った)関数を下のようにBindしました。

    app.Bind(GetMap)

GetMap()は、次のように定義しています。

func GetMap(realMinS string, realMaxS string, imaginaryMinS string, imaginaryMaxS string) string

これをJavaScriptから呼ぶには、例えば以下のように書けばOKということになります。

window.backend.GetMap("-2.0, "-1.5", "1.0", "1.5")

これは、本当に動くのかな?と思ってしまうほど簡単ですね。とても便利!

あとは、前回のコードをそのままmain.go内にコピペして、GetMap()を実装します。

func CalcGR(img *image.RGBA, rmin float64, rmax float64, imin float64, imax float64, offset int, nGR int, fchannel chan int) {
	for y := 0; y < 800; y++ {
		for x := offset; x < 800; x += nGR {
			var C = complex(rmin+(rmax-rmin)*float64(x)/800, imin+(imax-imin)*float64(y)/800)
			var V = C
			var it uint8 = 0
			for ; it <= 254; it++ {
				V = V*V + C
				if cmplx.Abs(V) > 2 {
					break
				}
			}
			img.Set(x, y, color.RGBA{(255-it)/2 + 64, uint8(cmplx.Abs(V) * 80), uint8(cmplx.Abs(V) * 30), 255})
		}
	}
	fchannel <- 1
}

func GetMap(realMinS string, realMaxS string, imaginaryMinS string, imaginaryMaxS string) string {
	realMin, err1 := strconv.ParseFloat(realMinS, 64)
	realMax, err2 := strconv.ParseFloat(realMaxS, 64)
	imaginaryMin, err3 := strconv.ParseFloat(imaginaryMinS, 64)
	imaginaryMax, err4 := strconv.ParseFloat(imaginaryMaxS, 64)
	if err1 != nil || err2 != nil || err3 != nil || err4 != nil ||
		realMin >= realMax || imaginaryMin >= imaginaryMax {
		return "incorrect parameter"
	}

	img := image.NewRGBA(image.Rect(0, 0, 800, 800))
	nGR := 4
	fchannel := make(chan int)
	defer close(fchannel)
	r := make([]int, nGR)
	for i := range r {
		go CalcGR(img, realMin, realMax, imaginaryMin, imaginaryMax, i, nGR, fchannel)
	}
	for range r {
		<-fchannel
	}

	var buff bytes.Buffer
	png.Encode(&buff, img)
	encodedString := base64.StdEncoding.EncodeToString(buff.Bytes())
	return encodedString
}

これでとりあえずバックエンド側も完成。

あとはフロントエンド側でfetch()を呼び出していた行を、このGetMap()を呼び出すように変更すれば完成です。

なお、Bindされた関数はPromiseを返すので、以下のように".then"で画像をsetImage()に渡せばOKです。

   window.backend.GetMap(realMin, realMax, imaginaryMin, imaginaryMax).then( function(result) {
     setImage("data:image/png;base64," + result)
   })

ビルドして実行してみると、期待通り動作しました(ちょっと気分を変えるために色の付け方を変えていますw)。

画像6

計算範囲を変えて"Generate"ボタンを押すと、このような感じに自己相似形をもつフラクタルな画像も見ることができました。

画像7

Formのところでちょっと引っかかりましたが、かなりあっさりWebアプリのデスクトップアプリ化ができてしまいました。Wails素晴らしい!

Ubuntu上での動作

Wailsはクロスプラットフォームなパッケージということで、軽くUbuntu 20.4上でもビルドして、実行してみました。

npm以外にもGTKなどのインストールが必要で、少しWailsのインストールに手間がかかりましたが、ビルドにかかる時間やアプリの起動時間はWindows上で行うよりも短く、Ubuntu上で開発する方がやや快適かな、と感じました。

画像8

バックの色がなぜか違います(というか、Windowsの方がCSSで指定された色になっていないような気がします)が、無事動作しました!

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

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

★ 過去記事をまとめたマガジン「SHIFTエンジニアが学びながら解説するGo言語」もご参照ください。

お問合せはお気軽に
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/