白猫のメモ帳

JavaとかC#とかJavaScriptとかHTMLとか機械学習とか。

Electronでデスクトップアプリを作りたい(メインプロセスとレンダラープロセス)

こんばんは。

すっかり寒くなってきましたね。
毎日お布団から出たくないです。

前回はElectronでHello Worldを表示するだけのプログラムを作りましたが、
今回はもう少し仕組みについて掘り下げてみます。

2つのプロセス

Electronでアプリを作成しようとして最初に混乱するのが、メイン/レンダラーの2つのプロセスについてかもしれません。
かくいう私もしっかりと罠にハマりました。

ネットで検索するとサンプルはたくさん出てくるので、それをそのままコピペしたら動いたけど、
ちょっといじったら変なエラーが出るとかまぁありがちですよね。

electronjs.org

ちょっと公式のドキュメントを覗いてみましょう。

In Electron, the process that runs package.json's main script is called the main process. The script that runs in the main process can display a GUI by creating web pages. An Electron app always has one main process, but never more.

Since Electron uses Chromium for displaying web pages, Chromium's multi-process architecture is also used. Each web page in Electron runs in its own process, which is called the renderer process.

package.jsonの「main」で指定されている、アプリケーション自体を実行しているNode.jsのプロセスがメインプロセスで、これはElectronアプリには常に1つしかありません。
一方でレンダラープロセスはChromiumを利用した画面表示用のプロセスで、メインプロセスから複数呼び出すことができます。で、

In web pages, calling native GUI related APIs is not allowed because managing native GUI resources in web pages is very dangerous and it is easy to leak resources. If you want to perform GUI operations in a web page, the renderer process of the web page must communicate with the main process to request that the main process perform those operations.

とあるように、レンダラープロセス側であまり処理をするのは良くないようなので、
基本的にはレンダラープロセスで描画や操作をさせて、必要に応じてメインプロセスに処理を依頼することで、
ブラウザではできないような処理をさせるという感じでしょうか。
若干ニュアンスは違うかもしれませんが、Webアプリのサーバサイドとクライアントサイドみたいな。

ちなみにElectronのAPIは種類によってメインプロセスのみで使えるもの、レンダラープロセスのみで使えるもの、両方で使えるものが分かれています。

electronjs.org

プロセス間の通信

2つのプロセスをうまく使い分けながら処理をすることが必要なことはわかりましたが、
デスクトップアプリではWebアプリのように1リクエストごとにサーバサイドの処理→クライアントサイドの処理という流れがあるわけではありません。
そのため、プロセス間の通信が必要になってきます。

プロセス間の通信にはIPC(InterProcess Communication)を利用すると便利です。

レンダラープロセスで利用するのはipcRenderer。
ここではテキストボックスの中でEnterキーが押されたときにメインプロセスに「renderer2main」というチャネル(イベントみたいなもの?)で通信を行います。
また、メインプロセスから「main2renderer」というチャネルで受信したときには値を画面上に書き込みます。

少し気をつけたいのがレンダラープロセスは複数同時に起動することができますが、レンダラープロセス間の通信はできません。
ipcRendererが送信先のプロセスを指定していないことからもわかるように、常にメインプロセスに対して通信を行います。

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>sample</title>
  </head>
  <body>
    <input type="text" id="req" value="">
    <p id="res"></p>
  </body>
  <script>
    const {ipcRenderer} = require('electron');
    document.getElementById("req").addEventListener("keyup", function(e) {
      if (e.keyCode === 13) {
        ipcRenderer.send('renderer2main', this.value);
        this.value = "";
      }
    });
    ipcRenderer.on('main2renderer', function(event, arg) {
      document.getElementById("res").innerHTML += arg + "<br>";
    });
  </script>
</html>

一方でメインプロセスで利用するのはipcMain。
この場合だと、「renderer2main」というチャネルを受け取ったときに、
「main2renderer」というチャネルでレンダラープロセスに送り返します。

メインプロセスからレンダープロセスに通信する場合には相手が決まっている必要があるので、
BrowserWindowのwebContentsに対して送信します。(「did-finish-load」はレンダラープロセスのロードが完了したら)

const { app, BrowserWindow, ipcMain } = require('electron');

app.on('ready', function() {

    let win = new BrowserWindow({
        width: 300,
        height: 300,
        webPreferences: {
            nodeIntegration: true
        }
    });

    win.loadFile('index.html');

    win.webContents.on('did-finish-load', function () {
        this.send('main2renderer', "init");
    })
});

app.on('window-all-closed', function() {
    if (process.platform !== 'darwin') {
        app.quit();
    }
});

ipcMain.on('renderer2main', (event, arg) => {
    event.reply('main2renderer', arg + arg);
    //event.sender.send('main2renderer', arg + arg); こっちでもいい
});

今回はメインプロセスの処理はただ2回繰り返して送り返しているだけなのでレンダラープロセスでも処理できますが、
たとえばファイルの読み書きやAPIをつつくといった処理を行うこともメインプロセスならできます。
(レンダラープロセスで絶対できないかと言うとまたややこしいのですが、基本はメインプロセスでやりますよね…)

f:id:Shiro-Neko:20191109234736p:plain

でまぁ、特に意味のないアプリが出来上がったわけです。

・メインプロセスとレンダラープロセスがある
・プロセス間の通信にはIPCを使うと便利

はい、ということです。

Electronでデスクトップアプリを作りたい(準備)

こんにちは

ラグビーってルールとか全然わからないのですが、ついつい見てしまいます。
今日の日本戦はぜひ頑張っていただきたいです。

Electronってなんだ

デスクトップアプリを作る方法は言語ごとに色々とあると思うのですが、
そもそものUIコンポーネントが微妙だったり、少し凝ったUIを作ろうとするとやたら難しかったりと、
機能を作る前に疲れてしまうことがよくあります。

ElectronはレンダリングWebブラウザの「Chromium」を使うことで、
HTML・CSSJavaScriptなどのWeb技術でデスクトップアプリケーションをつくることができる技術です。
しかも、クロスプラットフォーム
AtomやSlackやVisual Studio CodeもElectronで作っているらしいですね。

準備

環境

Windows10 64bit

Node.jsのインストール

Electronアプリを作るにはNode.jsを利用する必要があります。
インストーラ版を使ってインストールしても良いし、バイナリ版を展開してパスを通しても良いでしょう。お好みで。

nodejs.org

f:id:Shiro-Neko:20191020105109p:plain

Visual Studio Codeのインストール

エディタは好きなものを使えばいいとは思うのですが、VSCodeが使いやすそうだったのでこれを使います。

code.visualstudio.com

f:id:Shiro-Neko:20191020105453p:plain

インストールしたらおもむろに日本語化します。
左メニューの「Extensions」から「Japanese Language Pack for Visual Studio Code」をインストールしましょう。
再起動とか求められるので仰せのままに。

f:id:Shiro-Neko:20191020105840p:plain

ついでにJavaScriptの構文チェックができる「ESLint」と、
Node.jsのインテリセンスが利用できる「Node.js Modules Intellisense」とかも入れておくとなお良しです。

f:id:Shiro-Neko:20191020110355p:plain

プロジェクトを作る

適当な場所にディレクトリを作成してワークスペースにしましょう。
ディレクトリをVSCodeで開くと空っぽなのでエクスプローラには何も表示されません。
ターミナルが表示されていないかと思うので、「Ctrl+@」で表示させます。

f:id:Shiro-Neko:20191020111334p:plain

準備ができたらターミナルに以下のコマンドを入力します。
npmは「Node Package Manager」の略でパッケージ管理システムの1種です。

npm init -y

「init」はこのディレクトリをNode.js用に初期化しますよというコマンド、
「-y」オプションはいろいろ聞かれる質問を全部「yes」でスキップするオプションです。

ちょっと待つと「package.json」というファイルができます。
私の環境ではこんな感じになりました。

{
  "name": "test",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC"
}

mainはエントリポイントなので、別の名前がよい場合には変えましょう。

で、おもむろにindex.jsを作成し、

console.log("Hello World!");

とか入力して保存したあとに、
コンソールに以下のように入力しましょう。

node index.js

とりあえずNode.jsでHello Worldできました。
f:id:Shiro-Neko:20191020113211p:plain

Electronアプリにする

まずはElectronをインストールしましょう。

npm install electron

「-g」をつけると書いてある記事もよく見かけますが、
グローバルインストールする必要がなければつけなくて構いません。
(ローカルインストールするのが良いか、グローバルインストールするのが良いかについてはここでは触れません)

また、「--save」をつけないとpackage.jsonに依存性が書き込まれないという話もありますが、
最新のNode.jsの場合にはデフォルトでこのオプションは有効なようなので不要です。
(package.jsonではなくpackage-lock.jsonが生成されましたが、このへん掘り下げると長いので気にしない)

index.jsを以下のように編集し、

const { app, BrowserWindow } = require('electron');

app.on('ready', function() {

    let win = new BrowserWindow({
        width: 800,
        height: 600,
        webPreferences: {
            nodeIntegration: true
        }
    });

    win.loadFile('index.html');
});

app.on('window-all-closed', function() {
    if (process.platform !== 'darwin') {
        app.quit();
    }
});

index.htmlを作成します。

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>Hello World!</title>
  </head>
  <body>
    <h1>Hello World!</h1>
    <p>Electron version <script>document.write(process.versions.electron)</script></p>
  </body>
</html>

ローカルインストールした場合には

.\node_modules\.bin\electron .

グローバルインストールなら

electron .

でアプリが起動します。

f:id:Shiro-Neko:20191020152702p:plain

デバッグしたい

せっかくなのでデバッグできるようにしましょう。

f:id:Shiro-Neko:20191020153037p:plain

デバッグタブで「構成の追加」から「Node.js」を選ぶとlunch.jsonが生成されます。

f:id:Shiro-Neko:20191020154001p:plain

構成の追加から「Node.js:Electron(メイン)」を選択すると

{
    "type": "node",
    "request": "launch",
    "name": "Electron Main",
    "runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron",
    "program": "${workspaceFolder}/main.js"
}

こんな感じの設定が追加されます。
Electronだとエントリポイントはmain.jsが一般的なのかな?
「program」を書き換えるか、index.jsをmain.jsにリネームします。

で、「Electron Main」を選択した状態でデバッガを実行すると、

f:id:Shiro-Neko:20191020155507p:plain

無事に実行できました。
これで開発ができるようになりましたね。やったー。

C#のオプション引数とオーバーライド

こんばんは。

朝晩は涼しくなってきた…と思いきや急に暑かったり翻弄されている私です。
でも、もう9月ですね。

今日はC#のオプション引数のお話。

オプション引数ってなんじゃら

void Fuga(int a, string b)
{
    Console.WriteLine($"a:{a} b:{b}");
}

たとえばこんなメソッドがあったとして。

void Fuga(int a)
{
    this.Fuga(a, default(string));
}

void Fuga(string b)
{
    this.Fuga(default(int), b);
}

void Fuga()
{
    this.Fuga(default(int), default(string));
}

こんな感じにオーバーロードを作ると、引数の省略ができますよね。

void Fuga(int a = default(int), string b = default(string))
{
    Console.WriteLine($"a:{a} b:{b}");
}

それをこうやって書くと、簡単にかけるよってやつです。

オーバーライドメソッドでもやってみる

abstract class HogeBase
{
    public abstract void Fuga(string s = "fuga");
}

class Hoge : HogeBase
{
    public override void Fuga(string s = "fuga")
    {
        Console.WriteLine(s);
    }
}

オーバーライドするときにはabstractメソッドにもoverrideメソッドにもオプション引数を設定できます。

abstract class HogeBase
{
    public abstract void Fuga(string s);
}

class Hoge1 : HogeBase
{
    public override void Fuga(string s = "fuga1")
    {
        Console.WriteLine(s);
    }
}

class Hoge2 : HogeBase
{
    public override void Fuga(string s = "fuga2")
    {
        Console.WriteLine(s);
    }
}

なんかバラバラにしてもちゃんとコンパイルできてしまいます。
雲行きが怪しくなってきました。

コンパイル時に決定されます

interface IHoge
{
    void Fuga(string s = "interface");
}

abstract class HogeBase : IHoge
{
    public abstract void Fuga(string s = "abstract class");
}

class Hoge : HogeBase
{
    public override void Fuga(string s = "class")
    {
        Console.WriteLine(s);
    }
}

なるほどこいつはポリモーフィズムってやつだなって感じでこんな定義をしてみると、

class Program
{
    static void Main(string[] args)
    {
        IHoge a = new Hoge();
        a.Fuga();   // interface

        HogeBase b = new Hoge();
        b.Fuga();   // abstract class

        Hoge c = new Hoge();
        c.Fuga();   // class
    }
}

予想外の挙動に混乱します。
コンパイル時にリテラルとして埋め込まれるので、変数の型によって既定値は決まるようです。

class HogeEx : Hoge
{
    public override void Fuga(string s = "class2")
    {
        Console.WriteLine(s + " ex!");
    }
}

ので、こうやって定義を追加すると、

class Program
{
    static void Main(string[] args)
    {
        IHoge a = new HogeEx();
        a.Fuga();   // interface ex!

        HogeBase b = new HogeEx();
        b.Fuga();   // abstract class ex!

        Hoge c = new HogeEx();
        c.Fuga();   // class ex!

        HogeEx d = new HogeEx();
        d.Fuga();   // class2 ex!
    }
}

メソッドのディスパッチはインスタンスの型で決まるにも関わらず、
既定値は変数の型で決まるというややこしい挙動になります。

便利だけどややこしいねっていうお話。