EY-Office ブログ

WebUI をReactと組み合わせ試してみた

みなさんは WebUI をご存知でしょうか?下の画像はWebUIのホームページですがトップに書かれているキャッチフレーズを訳すと、

どんなウェブブラウザでもGUIとして使える、バックエンドはお好みの言語で。 ポータブル、軽量、フルOS APIアクセス。

WebUIはC, C++, Python, JavaScript, Go…などの言語で書かれたホストプログラム(バックエンド)にWebベース(HTML, CSS, JavaScript)のGUIをもたらすライブラリーです。
似た技術としてはElectronが有名ですが、Electronで動くアプリのコードはJavaScript/TypeScriptで書く必要がありますが、WebUIはより多数のプログラミング言語で書かれたプログラムで使えます。

WebUI https://webui.me より

WebUIの特徴

WebUI GitHubのFeaturesには以下のように書かれています。

  • ポータブル(実行時に必要なのはウェブ・ブラウザのみ)
  • 1つのヘッダーファイル
  • 軽量(数Kbのライブラリ)と小さなメモリフットプリント
  • 高速バイナリ通信プロトコル
  • マルチプラットフォーム&マルチブラウザ
    • OS: Windows, macOS, Linux
    • ブラウザー: Firefox, Chrome, Edge…
  • プライベート・プロファイルによる安全性

WebUIの機能

WebUIはシンプルなライブラリーで、機能は以下です(ホストプログラムはTypeScriptです)。

  • Window(ブラウザー)の起動
    • window = new WebUI()
  • 画面にHTML文字列またはHTMLファイルを表示
    • window.show('./index.html')
  • HTML要素のイベントにホストプログラムの関数を割り当てる
    • ホストプログラム側: window.bind("exit", (e) => WebUI.exit())
    • 画面のHTML側: <button id="exit">Exit</button>
  • ホストプログラムから画面のJavaScriptを実行
    • response = await window.script("return 2*2;")
  • 画面のJavaScriptからホストプログラム内の関数を実行
    • ホストプログラム側: window.bind("function1", ({arg}) => { ... })
    • 画面のJavaScript側: result = await webui.call('function1', arg1, arg2)
  • イベント待ち
    • await WebUI.wait()
  • 画面終了
    • WebUI.exit()

詳しいドキュメントは WebUI Documentationにあります。

ReactでUIを作ってみた

いつものジャンケンReactコードをGUIとして、TypeScript(Deno)やC言語で書かれたホストプログラムを動かすことにしました。

現在のWebUIはライブラリーのみで、開発環境等はないのでReactはViteで作りビルドされたJavaScript, HTML等をWebUI環境にコピーして動作確認しました。

Reactコード

  • 設定 vite.config.ts
    • ① アセットパスの先頭を / から./ に変更
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'

export default defineConfig({
  plugins: [react()],
  base: './'                // ← ①
})
  • HTMLファイル index.html
    • ① WebUIのJavaScriptを埋め込むscriptタグを追加
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Vite + React + TS</title>
    <script src="webui.js"></script>  <!--  ← ① -->
  </head>
  <body>
    <div id="root"></div>
    <script type="module" src="/src/main.tsx"></script>
  </body>
</html>
  • Reactアプリ App.tsx
    • ① webuiオブジェのType定義。暫定的な定義です、正しい方法が判らずライブラリー内から一部をコピーしました
    • ② ホストプログラムのjyanken_judgment関数の呼び出し
    • ③ 終了ボタンの定義
import React, { useState } from 'react';

// ↓ ①
type DataTypes = string | number | boolean | Uint8Array;
interface WebUi {
  call(fn: string, ...args: DataTypes[]): Promise<DataTypes>;
}
declare let webui: WebUi;

enum Te { Guu = 0, Choki, Paa}
enum Judgment { Draw = 0, Win, Lose }
type ScoreType = {
  human: number,
  computer: number,
  judgment: Judgment
}

const Jyanken: React.FC = () => {
  const [scores, setScrores] = useState<ScoreType[]>([]);

  const pon = async (human: Te) => {
    const computer: Te = Math.floor(Math.random() * 3);
    // ↓ ②
    const judgment = await webui.call('jyanken_judgment', computer, human);
    const score = {human: human, computer: computer, judgment: judgment as number};
    setScrores([score, ...scores]);
  }

  return (
    <>
      <h1>じゃんけん ポン!</h1>
      <JyankenBox actionPon={te => pon(te)} />
      <ScoreBox scores={scores} />
      <button id="exit">Exit</button>  {# // ←  ③ }
    </>
   );
}

export default Jyanken;

・・・以下省略・・・

ホストプログラム TypeScript

Denoで動くTypeScriptのコードです。

  • ① 画面JavaScriptからの引数はイベント・オブジェクトのargに入っています。数値なら.number(引数の順番)メソッドで取得します
  • ② ホストプログラムの関数の戻り値が、画面JavaScriptに渡されます
import { WebUI } from "https://deno.land/x/webui/mod.ts";

const window = new WebUI();
window.show('./index.html');

window.bind("exit", () => {
  WebUI.exit();
});
window.bind("jyanken_judgment", ({arg}) => {
  const computer:number = arg.number(0);        //← ①
  const human:number = arg.number(1);
  const judgment = (computer - human + 3) % 3;
  console.log(`- jyanken_judgment : `, computer, human, judgment);
  return judgment;                             //← ②
});

await WebUI.wait();

実行は以下のようにします(--allow-all--unstableオプションが必要です)。

$ deno run --allow-all --unstable react.ts

ホストプログラム C言語

ホストプログラムをC言語で書いてみると以下のようになります

  • ① 画面JavaScriptからの引数がnumberならwebui_get_int_at()関数で取得します、整数はlong long int なんですね
  • ② 画面JavaScriptに渡す戻り値はwebui_return_int()関数で設定します
#include "webui.h"

void exit_web(webui_event_t* e) {
	webui_exit();
}

void jyanken_judgment(webui_event_t* e) {
	long long int computer = webui_get_int_at(e, 0);     // ← ①
	long long int human = webui_get_int_at(e, 1);
	long long int judgment = (computer - human + 3) % 3;
	printf("jyanken: %lld %lld %lld\n", computer, human, judgment);

	webui_return_int(e, judgment);                       // ← ②
}


int main() {
	size_t window = webui_new_window();

	webui_bind(window, "exit", exit_web);
	webui_bind(window, "jyanken_judgment", jyanken_judgment);

	webui_show(window, "./index.html");

	webui_wait();
	webui_clean();

	return 0;
}

C言語なので、実行は以下のようになります。MakefileはGitHubのexamplesにあるものを使いました。

$ make
$ ./main

まとめ

現在、C, C++, Python, Goで書かれたコマンドラインで動くソフトがあり、そこに簡易的なGUIを追加するのには WebUI は良い選択しかも知れません。

ただし現状では以下のような問題点もあり、Electronのようにネイティブアプリに近いものを作るのは難しいと思います。

  • GUI(HTML, JavaScript, CSS)を含めた開発環境がないので開発が面倒。せめて画面のライブ・リロードが欲しいがC言語等を考えると難しいかな
  • 完成したたホストプログラムとHTML, JavaScript, CSSをパッケージ化するツールが無いので、配布に工夫が必要
  • 画面はブラウザーなので、メニューバーなどブラウザーを超えた機能は作れない

とは言え、少人数で使うアプリに簡易的なGUIを作る場合などは使えそうですね。

- about -

EY-Office代表取締役
・プログラマー
吉田裕美の
開発者向けブログ