Go言語でデスクトップアプリを作るのは、なかなかハードルが高いと感じています。それで、ブラウザベースの「webview」を使うと、手軽にUIが作れます。(ちなみに、日本語プログラミング言語「なでしこ3」でWin/Mac向け配布パッケージを作ったので、その技術的調査のまとめです。)
webview/webview を使う場合
webview/webviewはSafari/EdgeなどOSに最初からインストールされているブラウザのコンポーネントを利用してHTMLを表示するパッケージです。これを使うことで、配布サイズがElectronなどのアプリに比べて小さくなるのが特徴です。Win/Mac/Linuxで同じように使えます。
非常によくできたパッケージです。ただし、欠点があって、Windowsでうまく動かないことが多いです。また、macOSではalertなどのダイアログが表示されません。
使い方は以下の感じです。最初にパッケージをインストールします。
go mod init desktop
go get github.com/webview/webview
WebViewを使うには、以下のようなプログラムを作ります。
package main
import (
"github.com/webview/webview"
)
func main() {
// ブラウザを起動
debug := true
w := webview.New(debug)
defer w.Destroy()
w.SetTitle("test")
w.SetSize(640, 400, webview.HintNone)
w.Navigate("https://nadesi.com")
w.Run()
}
macOSでalertを使いたい場合
macOSのwebviewではalertが使えないので、以下のようにBindで自作する必要があります。ただし、非同期に実行されるために、連続で実行する場合は注意が必要です。
func main() {
// ... 省略 ...
w.Bind("alert", func(text string) bool {
return messagebox(text, `"Yes"`)
})
// ... 省略 ...
w.Run()
}
func messagebox(text string, buttons string) bool {
title := GlobalInfo.Title
script := `set T to button returned of ` +
`(display dialog "%s" with title "%s" buttons {%s} default button "Yes")`
out, err := exec.Command("osascript", "-e", fmt.Sprintf(script, text, title, buttons)).Output()
if err != nil {
if exitError, ok := err.(*exec.ExitError); ok {
return exitError.Sys().(syscall.WaitStatus).ExitStatus() == 0
}
}
return strings.TrimSpace(string(out)) == "Yes"
}
Windowsでwebview2を使う
Windowsでは、webview2を使うとかなり良いです。ただし、WebView2ランタイムのインストールが必要となります。
go-webview2をインストールします。
go get github.com/jchv/go-webview2
go-webview2を利用するには、以下のプログラムを作ります。
package main
import (
webview "github.com/jchv/go-webview2"
)
func main() {
// ブラウザを起動
debug := true
w := webview.New(debug)
defer w.Destroy()
w.SetTitle("test")
w.SetSize(640, 400, webview.HintNone)
w.Navigate("https://nadesi.com/")
w.Run()
}
macOSとWindowsでソースコードを切り替える
ソースコードに以下の宣言を書くと、OSごと(macOSとWin)にソースコードを切り替えることができます。
//go:build darwin || linux
// +build darwin linux
...
//go:build windows
// +build windows
...
これでなんとか良い感じにWin/Macで分岐できて便利です。
しかし、WebView2のランタイムのインストールが結構面倒です。また、当然ながら、Win/Macでソースを切り替えるのが面倒です。そこで同一ソースでプログラムを動かす方法として「lorca」を使う方法があります。
Chromeをランタイムに使う「lorca」
lorcaはChromeがインストールされていれば動くので便利です。上記、WebView2のランタイムのインストールは比較的面倒なので、Chromeを動作ランタイムと考えて使えば、インストールも容易なのでかなり便利です。WebView2の動作が安定するまでは、lorcaを使う
go get github.com/zserge/lorca
プログラムは以下の通りです。めちゃくちゃ簡単。
package main
import (
"github.com/zserge/lorca"
)
func main() {
// ブラウザを起動
ui, _ := lorca.New("https://nadesi.com", "", 800, 600)
defer ui.Close()
<-ui.Done()
}
webviewとローカルサーバーを動かす
上記だけだとブラウザだけです。そこで、テクニックとして、ローカルサーバーを動かして、WebViewで表示するようにします。
package main
import (
"github.com/zserge/lorca"
"net/http"
"strconv"
"fmt"
"log"
"net"
"strings"
)
var PortNo int // 自動的に空きポート番号を探す
func StartServer() string {
// set handler
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
// ここで表示したいHTMLを指定
w.WriteHeader(200)
w.Header().Set("Content-Type", "text/html; charset=utf8")
w.Write([]byte("<html><body><h1>Hello</h1></body></html>"))
})
// start server
addr := "127.0.0.1:" + strconv.Itoa(PortNo)
fmt.Printf("[Server] http://%s\n", addr)
err := http.ListenAndServe(addr, nil)
if err != nil {
log.Fatal(err)
}
return addr
}
func checkPort() {
// 適当に空いているポートを探す
l, err2 := net.Listen("tcp", "127.0.0.1:0")
if err2 != nil {
// 空きポートの検索に失敗
log.Fatal(err2)
}
// ポート番号を得る
addr := l.Addr().String()
a := strings.Split(addr, ":")
pno, err3 := strconv.Atoi(a[1])
if err3 != nil {
log.Fatal(err3)
}
PortNo = pno
l.Close() // HTTPサーバーの起動前にソケットを閉じておく
}
func indexHandler(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(200)
w.Header().Set("Content-Type", "text/html; charset=utf8")
w.Write([]byte("<html><body><h1>Hello</h1></body></html>"))
}
func main() {
// ローカルサーバーを起動
checkPort()
go StartServer()
// WebViewを起動
addr := "127.0.0.1:" + strconv.Itoa(PortNo)
fmt.Printf("%s\n", addr)
ui, _ := lorca.New("http://" + addr + "/", "", 800, 600)
defer ui.Close()
<-ui.Done()
}
参考
- nadesiko3webkit ... なでしこ3で作ったゲームやツールをWin/Macで手軽に配布するプロジェクト
- マイナビ/100行未満かつGo標準ライブラリだけで作る掲示板