NodeGUIとReact NodeGUI

Qt5のC++アドオン「NodeGUI」と、そのrenderer「React NodeGUI」のサンプルコードを読んだだけです。

対象ver

  • macOS 10.10以降
  • Window7以降
  • Ubuntu 12.04
  • 64bit OS(Intel Core i3 〜 など)

この記事では、10.14.5 MojaveのCore i5を使っています。

NodeGUIとは

公式ドキュメント

NodeGUIは、JavaScript(TypeScript) + CSS LikeなStylesheetを用いて、デスクトップアプリケーションを構築するライブラリです。

特徴

NodeGUIは、Node.jsとQtをネイティブレベルでやり取りする仕組みを提供しています。
Qtというのは、C++製のクロスプラットフォームGUIフレームワークです。
PyQtなど、数多くの言語でサポートされています。
公式ページ

Node.js + Qtでネックとなるのが、イベントを待ち受ける仕組み(イベントループ)です。

まず、QtはQCoreApplication::exec()でイベントループを実行します。
Threads Events QObjects - Qt Wiki

一方、Node.jsではlibuvを用いたイベントループが用いられます。
Node.jsでのイベントループの仕組みとタイマーについて - 技術探し

これら2つを同一スレッドで動かすことができません。
Electronでは似たような問題に対し、Node.jsのイベントループをGUI側のイベントループと統合するアプローチを行っています。
Electron Internals: Message Loop Integration | Electron Blog

NodeGUIはこれに着想を得たようで、Quodと呼ばれるNode.jsのForkを開発し、Node.jsのイベントループをQt向けにmergeしています。
この仕組みにより、Node.js - Qtは同一のスレッドで実行され、リソースの圧迫を抑えることができるようです。

試す

NodeGUIにはスターターがあるので、すぐに試すことができます。
https://github.com/nodegui/nodegui-starter

Qtは複数のウィジェットを組み合わせていくのですが、NodeGUIでも同じです。
下記のように、@nodegui/nodeguiモジュールからQMainWindowをはじめとする各機能、ウィジェットをインポートして、QMainWindowを作成することでアプリケーションを構築します。

src/index.ts
const {
  QMainWindow,
  QWidget,
  QLabel,
  FlexLayout,
  QPixmap
} = require("@nodegui/nodegui");

/**
 * ここから画面↓
 */

// pathなど使える
const path = require('path');
const imageUrl = path.resolve(__dirname, '../assets/profile_icon.jpg');

const win = new QMainWindow();

const centralWidget = new QWidget();
centralWidget.setFixedSize(300, 300)
centralWidget.setObjectName("root");

// flexLayoutを作成。
const rootLayout = new FlexLayout();
centralWidget.setLayout(rootLayout);

// 画像
const pict = new QLabel();
pict.setObjectName("pict");
const pixMap = new QPixmap(imageUrl);

pict.setPixmap(pixMap.scaled(150, 150));

// テキスト
const text = new QLabel();
text.setObjectName("label");
text.setText('NodeGUI Sample')

rootLayout.addWidget(pict);
rootLayout.addWidget(text);

win.setCentralWidget(centralWidget);
win.setStyleSheet(
  `
    #root {
      height: '100%';
      align-items: 'center';
      justify-content: 'center';
    }
    #pict {
      margin-bottom: 10px;
    }
    #label {
      font-size: 16px;
      font-weight: bold;
    }
  `
);

/**
 * ↑ここまで画面
 */

// 出力
win.show();

// winのGC回避
(global as any).win = win;
NodeGUIを使ってdkrkのアイコンを表示させた例

NodeGUIはいくつかのQt向けAPIを提供しており、また、全てのNode.jsのAPIが使用できます。
devtools(chrome://inspect)やVSCodeでのデバッグなどもサポートしています。

Nativeライブラリについては、Electron同様、ビルドが必要になるようで、それがちょっと辛いです。
nodegui-rebuildみたいなの欲しい…。

React NodeGUI

React NodeGUIは、NodeGUIでReactのrenderを行うライブラリです。
スターターがあるので、そちらをインストールしてみます。

src/index.tsx
import {
  Renderer,
  View,
  Text,
  Button,
  Window,
  Image,
  useEventHandler
} from "@nodegui/react-nodegui";
import React, { useState } from "react";
import path from "path";
import { AspectRatioMode } from "@nodegui/nodegui";

const imageUrl = path.resolve(__dirname, "../assets/sample.jpg");
const fixedSize = { width: 500, height: 500 };
const App = () => {
  const [time, setTime] = useState(new Date());
  const btnHandler = useEventHandler(
    { clicked: () => setTime(new Date()) },
    []
  );
  return (
    <Window minSize={fixedSize} maxSize={fixedSize} styleSheet={styleSheet}>
      <View id="container">
        <Button text="Update Time" on={btnHandler} />
        <Text id="result">{`${time}`}</Text>
        <Text id="result">{`Time in epoc: ${time.getTime()}`}</Text>
        <Image
          style={imageStyle}
          src={imageUrl}
          aspectRatioMode={AspectRatioMode.KeepAspectRatio}
        />
      </View>
    </Window>
  );
};

const imageStyle = `
  height: "70%";
`;

const styleSheet = `
  #container {
    flex: 1;
    flex-direction: column;
    min-height: '100%';
    align-items: 'center';
    justify-content: 'center';
    background-color: black;
  }
  #opBtn {
    font-size: 20px;
  }
  #result {
    font-size: 12px;
    flex: 1;
    color: cyan;
  }
`;

Renderer.render(<App />);

サンプルアプリは現在の日付を表示するものです。

React NodeGUIを使ってスターターのサンプルアプリを起動している例

Reactのバージョンは現時点で16.9が使用されていて、コードではHooksが用いられています。

Styleの指定はほとんどCSSに近いですが、実際はQtのStylesheetです。
そのためFlexboxの処理は、Yoga Layoutを使用しています(これはReact Nativeと同じです)。

<Window /><Image />などのコンポーネントは、Qtウィジェットの作成を隠蔽してくれます。
例えば<Image />コンポーネントは、QPixmapのScaleやQEventの登録などをやってくれるものです。

その他

思ったよりQt寄りです。
リリースされたばかりなので、Qtウィジェットのサポートはまだ少ないです。
ファイル選択ダイアログ(QFileDialog)は未搭載らしく、
image-viewのサンプルもQLineEdit(Input type="text"のようなもの)で実装されています。

最後にあまり関係ない話ですが、ライセンスが少しややこしいです。
NodeGUI自体のライセンスはMITですが、オープンソース版QtのライセンスはGPL, LGPLです。
NodeGUIはQtに直接手を加えているわけではないので、独自にMITライセンスを取得できるという見解です。
それが正しければ、普通に使う分にはMITの範囲ですがNodeGUIを越えてQtに直接変更を加えたり、Qtへの接続を追加するモジュールがある場合、GPL, LGPLの規約が適用されます。
これは今後、商用利用する際に意識する必要がありそうです。