8
10

More than 1 year has passed since last update.

Go言語とwebviewで簡単デスクトップアプリを作ろう

Last updated at Posted at 2021-10-18

Go言語でデスクトップアプリを作るのは、なかなかハードルが高いと感じています。それで、ブラウザベースの「webview」を使うと、手軽にUIが作れます。(ちなみに、日本語プログラミング言語「なでしこ3」でWin/Mac向け配布パッケージを作ったので、その技術的調査のまとめです。)

webview/webview を使う場合

webview/webviewはSafari/EdgeなどOSに最初からインストールされているブラウザのコンポーネントを利用してHTMLを表示するパッケージです。これを使うことで、配布サイズがElectronなどのアプリに比べて小さくなるのが特徴です。Win/Mac/Linuxで同じように使えます。

非常によくできたパッケージです。ただし、欠点があって、Windowsでうまく動かないことが多いです。また、macOSではalertなどのダイアログが表示されません。

使い方は以下の感じです。最初にパッケージをインストールします。

init.sh
go mod init desktop
go get github.com/webview/webview

WebViewを使うには、以下のようなプログラムを作ります。

main.go
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で自作する必要があります。ただし、非同期に実行されるために、連続で実行する場合は注意が必要です。

main.go
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をインストールします。

init.sh
go get github.com/jchv/go-webview2

go-webview2を利用するには、以下のプログラムを作ります。

main.go
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)にソースコードを切り替えることができます。

main_mac.go
//go:build darwin || linux
// +build darwin linux
...
main_win.go
//go:build windows
// +build windows
...

これでなんとか良い感じにWin/Macで分岐できて便利です。

しかし、WebView2のランタイムのインストールが結構面倒です。また、当然ながら、Win/Macでソースを切り替えるのが面倒です。そこで同一ソースでプログラムを動かす方法として「lorca」を使う方法があります。

Chromeをランタイムに使う「lorca」

lorcaはChromeがインストールされていれば動くので便利です。上記、WebView2のランタイムのインストールは比較的面倒なので、Chromeを動作ランタイムと考えて使えば、インストールも容易なのでかなり便利です。WebView2の動作が安定するまでは、lorcaを使う

init.sh
go get github.com/zserge/lorca

プログラムは以下の通りです。めちゃくちゃ簡単。

main.go
package main

import (
    "github.com/zserge/lorca"
)

func main() {
    // ブラウザを起動
    ui, _ := lorca.New("https://nadesi.com", "", 800, 600)
    defer ui.Close()
    <-ui.Done()
}

webviewとローカルサーバーを動かす

上記だけだとブラウザだけです。そこで、テクニックとして、ローカルサーバーを動かして、WebViewで表示するようにします。

main.go
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()
}

参考

8
10
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
10