白猫のメモ帳

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

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!
    }
}

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

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

サジェストでajaxのリクエストをしまくらないように調整する

こんにちは。

体調を崩していたり、プライベートがいろいろゴタゴタしていまして、
随分久しぶりになってしまいました。

今回はJavaScriptのお話。

サジェストって

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

こういうやつです。
1文字入力するごとに候補がでてくるやつ。

基本的には「onkeyup」のイベントを拾って、ajaxでサーバーサイドに処理を投げたりします。
もちろんクライアントサイドで処理しても良いですよ。
(「keydown」は直前に入力した内容がvalueで取れませんので「keyup」)

ただ、1文字入力するごとにっていうことは、
「ねこ」って打とうとしたときに、
「n」「ね」「ねk」「ねこ」って4回リクエストが飛んでしまうんですよね。

リソースが潤沢で、しっかりとキャッシュとかすれば気にならないかもしれないですが、
できることなら余計なものは飛ばしたくないです。

ちょっと待つ

というわけで、ちょっと待つというシンプルな手法を取ります。
ユーザが連続して入力中の間は処理を待っていて、入力が終わっただろうというタイミングでリクエストを投げます。

ちょっと待つといえばsetTimeoutですね。

var timeout;
function anythingSearch() {
    clearTimeout(timeout);
    timeout = setTimeout(function() {
        // なんか検索処理
    }, 200);
}

が、なんか変なスコープに変数作るの嫌ですよね。
(無名関数で囲い込めばいいじゃんというのは聞かなかったことにします)

でもそういえばsetTimeoutの戻り値ってタイムアウトの関数自体ではなくて、なんかIDみたいなやつなのでした。
MDNにも以下のような記載があります。

戻り値 timeoutID は、setTimeout() を呼び出して作成したタイマーを識別する正の整数値です。
この値は、タイムアウトをキャンセルするために clearTimeout() へ渡すことができます。

ということは要素自体のdata属性に入れとけばよいのでは?

function anythingSearch() {
    clearTimeout(this.dataset.timeout);
    var timeout = setTimeout(function() {
        // なんか検索処理
    }, 200);
    this.dataset.timeout = timeout ;
}

とふと思ったのでした。
現場からは以上です。