[Rust] web-viewでGUIアプリをつくる


Rust で web-view を用いて Win10 向け GUI アプリを開発する方法を試してみました. この方針の利点として

  • Rust + ウェブ技術の知識で GUI アプリが開発できる.
    • Electron と違い, レンダリングエンジンとして OS が提供するものを使用するためバイナリサイズが小さい.

という点があげられます. 本記事のコードを開発・実行した環境は Windows 10 v1809, Rust 1.40 stable-x86_64-pc-windows-msvc です. なお Mac でも動くかもしれませんが試していません. また Linux で動かすには Webkit2gtk が必要です.

Hello world

まず最初は Hello world してみます. 新規プロジェクト hello-webview を作成し, Cargo.toml

[dependencies]
web-view = { version = "0.6", features = ["edge"] }

を指定します. フィーチャーフラグ edge はレンダリングエンジンに MSHTML (IE) ではなく EdgeHTML を使用することを意味します.

次に src/main.rs を次のように作成します. cf. examples/pageload.rs

#![windows_subsystem="windows"]

fn main() {
    web_view::builder()
        .title("Hello world!")
        .content(web_view::Content::Html(HTML))
        .size(320, 240)
        .user_data(())
        .invoke_handler(|_, _| Ok(()))
        .run()
        .unwrap();
}

const HTML: &str = r#"<!DOCTYPE html>
<html>
    <body>
        Hello world from Rust!
    </body>
</html>"#;

最初の行は DOS 窓を非表示にするためのものです. たくさん並んでいるメソッドは以下で順番に説明します. これをコンパイル, 実行すれば WebView が起動し, Hello world が表示されます.

なお high-DPI 環境ではぼやけるかもしれません. その場合実行ファイル (./target/debug/hello-webview.exe) を右クリックし, 「プロパティ-互換性-高DPI設定の変更-高DPIスケールの動作を上書きします」のチェックマークをつければ解決します (拡大縮小の実行元: アプリケーション). この設定はコンパイルし直しても有効なままのようです.

カウンター

次の例として, ウィンドウにボタンを表示し, それをクリックした回数をカウントし表示するカウンターアプリをつくってみます. cf. examples/timer.rs

ブラウザ側のイベントの取得

まずブラウザ側で起こったイベントを扱う方法を見てみます. HTML ソースを

<!DOCTYPE html>
<html>
    <body>
        <div id="display">0</div>
        <button onclick="external.invoke('count')">count</button>
    </body>
</html>

と書き換えます. これを表示すると 0 という数字と count と書かれたボタンが表示されるはずです. ボタンをクリックすると onclick 属性で指定された JS コードが走ります. この JS コードは invoke という外部で定義された関数を呼び出そうとします.

invoke の実装は Rust 側で行います: 上で登場した invoke_handler というメソッドの引数であるクロージャが呼び出される関数です. なので上の Rust コードで該当する部分を

.invoke_handler(|_webview, arg| {
    if arg == "count" {
        eprintln!("Count!");
    }
    Ok(())
})

という形に書き替えてみます. このクロージャは, 第 2 引数が "count" なら標準エラーに "Count!" と表示し, そうでなければ何もしません. これを (最初に設定した #![windows_subsystem = "windows"] というアトリビュートを外した上で) 実行すると

というウィンドウが表示され, count ボタンをクリックすると次のように出力されます.

プログラムの内部状態の更新

カウンタープログラムはカウントした回数を保持している必要があります. そこでまず内部状態を表す struct を定義します.

struct UserData {
    count: u32,
}

そして, web_view のビルダー .user_data() にこの struct を渡すように変更します.

.user_data( UserData { count: 0 } )

内部状態には invoke_handler に渡すクロージャの第 1 引数 webviewwebview.user_data() または webview.user_data_mut() という形でアクセスします (前者が不変な参照, 後者が可変な参照). そこで上の invoke_handler を次のように書き換えれば, count ボタンが押されるたびに内部状態が 1 ずつ更新されるようになります.

.invoke_handler(|webview, arg| {
    if arg == "count" {
        webview.user_data_mut().count += 1;
        eprintln!("Count! {}", webview.user_data().count);
    }
    Ok(())
})

画面の表示内容の書き換え

最後に, 内部状態を更新する度に画面に表示されている内容を書き換える必要があります. これは webview.eval() メソッドによって適当な JS コードを呼び出すことによって達成されます. なおこのメソッドは Result<T, web_view::Error> を返すので, そのままクロージャの戻り値にします.

.invoke_handler(|webview, arg| {
    if arg == "count" {
        webview.user_data_mut().count += 1;
        eprintln!("Count! {}", webview.user_data().count);
        webview.eval(&format!("update({})", webview.user_data().count))
    } else {
        Ok(())
    }
})

あとは対応する JS 関数を HTML 側で定義すれば完成です.

<!DOCTYPE html>
<html>
    <body>
        <div id="display">0</div>
        <button onclick="external.invoke('count')">count</button>

        <script>
            function update(count) {
                document.getElementById('display').innerHTML = count;
            }
        </script>
    </body>
</html>