Electronでデスクトップウィジェットを作るまで

# はじめに
HTML/CSS/JSを使ってデスクトップアプリケーションが作成できる[Electron](http://electron.atom.io/)。
本記事ではデスクトップウィジェット作成するまでの流れを、簡単な時計アプリを題材として解説します。
![作成するアプリのスクリーンショット](https://qiita-image-store.s3.amazonaws.com/0/83758/ed49f959-8e7f-57ed-c246-bff0b4d19e4e.png)

※ この時計アプリのソースコードとパッケージは、GitHubにて公開しています。
GitHub: [SallyAcolyte/tutorial_clock](https://github.com/SallyAcolyte/tutorial_clock)

**対象読者**
- HTML/CSS/JSで簡単なアプリは作れるが、Electronは初心者な方
- Electronで透過ウィンドウ/フレームレスウィンドウを扱う際のポイントが知りたい方
- 自分好みなデザイン/機能を持つウィジェットを作成したい方

※ Windows環境を前提としていますが、基本的な流れは他環境でも同様です。

# チュートリアル
## Electronの入手
Electronの本体は、以下からダウンロードできます。
[Releases · atom/electron](https://github.com/atom/electron/releases)

※ Windows 64bit環境の場合は、`electron-v[バージョン]-win32-x64.zip`を利用します。

アプリを実行する際に利用しますので、任意の場所に展開して下さい。

## Hello World
まずは、お決まりの「Hello World」を表示するアプリを作成します。
**`app`というフォルダを作成し、以降のファイルはそのフォルダ内に入れて下さい。**

### package.json
始めに`package.json`を作成します。
名前やバージョンなど、アプリに関する情報を記載するファイルです。
`main`で指定されたファイルが、アプリの実行時に読み込まれます。

````package.json
{
  "name" : "tutorial_clock_digital",
  "version" : "0.0.1",
  "main" : "main.js"
}
````

### main.js
次に`main`で指定した`main.js`を作成します。
ウィンドウ周りの処理などを実装しますが、まずはそのまま貼り付ければOKです。
後ほど「// ウィンドウ作成時のオプション」部分に追記します。

````main.js
var app = require("app");
var BrowserWindow = require("browser-window");

var mainWindow = null;

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

app.on("ready", function() {
    mainWindow = new BrowserWindow({
        // ウィンドウ作成時のオプション
    });

    // index.html を開く
    mainWindow.loadUrl("file://" + __dirname + "/index.html");

    mainWindow.on("closed", function() {
        mainWindow = null;
    });
});
````

### index.html
最後に、`index.html`を作成します。
実際にアプリのウィンドウ内に表示されるファイルです。
通常のHTMLでアプリのインタフェースを作成します。

````index.html
<h1>Hello World</h1>
````

### 実行
```
* ここまででのファイル構成

app
├index.html
├main.js
└package.json
```

これで、必要なファイルは揃いました。
先ほど用意したelectron.exeに、作成したappフォルダを「フォルダごと」ドロップするとアプリが起動します。
![アプリの起動（ドロップ）](https://qiita-image-store.s3.amazonaws.com/0/83758/45302070-8b27-101a-85f4-bef002b75606.png)

または、コマンドプロンプト等から引数として渡してもOKです。
![アプリの起動（コマンドプロンプト）]
(https://qiita-image-store.s3.amazonaws.com/0/83758/47a2054b-8f2d-12e2-1f98-7d1fe00a2555.png)

アプリのウィンドウが開き、`index.html`が表示されます。
![Hello World](https://qiita-image-store.s3.amazonaws.com/0/83758/4183d90d-76b8-7b15-0cbc-bed225dbeb22.png)

## 時計の作成
ここから、実際に動作させる時計を作成します。
HTML/CSS/JSで、普通のブラウザで動くように作成すればOKです。
※ Electron特有の要素は無いので解説は割愛します。

````index.html
<meta charset="utf-8">
<title>clock</title>

<div id="digital_clock"><!-- ここに時刻が入る --></div>

<script src="./clock.js"></script>
````
````clock.js
// 時計の描画処理をスタート
clock();

function clock () {
	// 現在日時を取得
	var d = new Date();

	// デジタル時計を更新
	updateDigitalClock(d);

	// 次の「0ミリ秒」に実行されるよう、次の描画処理を予約
	var delay = 1000 - new Date().getMilliseconds();
	setTimeout(clock, delay);
}

function updateDigitalClock (d) {
	var AA_str = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
	var YY = d.getFullYear().toString().slice(-2);
	var MM = d.getMonth() + 1;
	var DD = d.getDate();
	var AA = d.getDay();
	var hh = d.getHours();
	var mm = d.getMinutes();
	var ss = d.getSeconds();

	// 桁あわせ
	if(MM < 10) { MM = "0" + MM; }
	if(DD < 10) { DD = "0" + DD; }
	if(hh < 10) { hh = "0" + hh; }
	if(mm < 10) { mm = "0" + mm; }
	if(ss < 10) { ss = "0" + ss; }

	var text = YY + '/' + MM + '/' + DD + ' (' + AA_str[AA] + ')<br>' + hh + ':' + mm + ':' + ss
	document.getElementById("digital_clock").innerHTML = text;
}
````

先ほどと同じようにアプリを実行して下さい。
![時計の作成](https://qiita-image-store.s3.amazonaws.com/0/83758/0a2a4e5f-74cd-4424-b422-3919a31da42a.png)

## 補足: 開発者ツールの利用
Electronには、Google Chromeと同様の開発者ツールが含まれています。
起動中のアプリで、`Ctrl + Shift + i`を押すと表示されます。

開発に役立つ様々な機能がありますが、中でも必ず利用するのがコンソールです。
**JavaScriptが意図したとおりに動作しない場合、開発者ツールのConsoleタブに何らかのエラーメッセージが出力されていないか確認して下さい。**
![DevTools_console](https://qiita-image-store.s3.amazonaws.com/0/83758/fbbc7afc-8494-062f-773d-61247ae2e7f6.png)

## 透過ウィンドウの作成
次にデスクトップウィジェットに欠かせない、「透過ウィンドウ」を作成します。
ウィンドウ枠が無く、背景が透過したウィンドウです。

作成するには、`main.js`でウィンドウ作成時のオプションを指定します。

````main.js
// 一部抜粋
app.on("ready", function() {
	mainWindow = new BrowserWindow({
        // ウィンドウ作成時のオプション
		"width": 180,
		"height": 70,
		"transparent": true, 	// ウィンドウの背景を透過
		"frame": false, 	// 枠の無いウィンドウ
		"resizable": false 	// ウィンドウのリサイズを禁止
	});
````

**※ 透過ウィンドウでは、ウィンドウのリサイズがサポートされません。**
リサイズを行いたい場合、背景透過は行わずに`"frame": false`だけを指定して下さい。

参考資料: [electron/frameless-window.md at master · atom/electron](https://github.com/atom/electron/blob/master/docs/api/frameless-window.md#transparent-window)

### 透過ウィンドウに必要なCSS
透過ウィンドウを作成する際に、よく利用されるCSSがいくつかあります。
利用しなくてもアプリは動作しますが、実用上の問題があります。

- 背景色/背景画像
    - 透過ウィンドウでは「何もない部分」はクリック出来ないため、背景を指定します
    - rgba(r, g, b, a)による色指定を行うことで、透明度を持った背景色が指定できます
- `-webkit-app-region: drag;`
    - 要素をウィンドウのタイトルバーのように扱う指定です
    - いずれかの要素にこの指定を行わないと、ドラッグによるウィンドウ移動が出来ません
- `-webkit-app-region: no-drag;`
    - `drag`を指定した要素内にボタンなど操作可能な要素を配置する場合、`no-drag`を指定して上書きします
- `-webkit-user-select: none;`
    - テキストの選択を無効化します
    - 主にインタフェース部分に指定します

これらのCSSを指定するため、新たに`clock.css`を作成します。

````clock.css
body {
    background-color: rgba(24, 24, 24, .7);
    color: #fff;
    -webkit-app-region: drag;
    -webkit-user-select: none;
}
````

また、`index.html`から`clock.css`を読み込みます。（2行目を追加）

````index.html
<meta charset="utf-8">
<link href="./clock.css" rel="stylesheet">
<title>clock</title>

// 以下省略
````

アプリを起動すると、ウィンドウの枠が無くなり背景が透過されます。
また、右クリックするとウィンドウのメニューが表示され、タイトルバーと同じ扱いであることが分かります。
![透過ウィンドウ](https://qiita-image-store.s3.amazonaws.com/0/83758/65a3d3e2-a29a-1eed-3c7f-a7c2daa64a86.png)

## 見た目の調整
### CSS
透過したウィンドウに合わせ、CSSで見た目を整えます。
Electronでは表示にChromium(Chrome)を利用しているため、Chromeに合わせた記述を行います。
画像やフォントは相対パスによる指定のほか、URLによる外部の参照も可能です。

````clock.css
@import url(http://fonts.googleapis.com/css?family=Iceland);

body {
	overflow: hidden;
	margin: 0;
	padding: 0;
	border: 5px solid rgb(42, 42, 42);
	background-color: rgba(24, 24, 24, .7);
	box-shadow: 0 0 8px 3px #000 inset;

	-webkit-app-region: drag;
	-webkit-user-select: none;
}

#digital_clock {
	font-family: "Iceland";
	font-size: 25px;
	line-height: 22px;
	margin-top: 9px;
	text-align: center;
	color: #fff;
	text-shadow: 1px 1px 3px #000;
}
````
![見た目の調整](https://qiita-image-store.s3.amazonaws.com/0/83758/f3a81822-7f73-6c48-d12c-596bc0888e16.png)
※ フォントはGoogle Fontsの[Iceland](https://www.google.com/fonts/specimen/Iceland)を利用しています。

### 起動時のチラつき対策
Electronでは、アプリ起動からCSSが読み込まれるまでの間に、一瞬だけ白紙のページが表示されてしまいます。
動作上の問題はありませんが、黒配色のアプリではチラついて見えるため、ウィンドウが表示されるタイミングを変更します。

まず、`main.js`のウィンドウ作成時のオプションに、`"show": false`を追加します。
この指定を行うと、アプリ起動時にウィンドウが表示されなくなります。

````main.js
// 一部抜粋
app.on("ready", function() {
	mainWindow = new BrowserWindow({
		// ウィンドウ作成時のオプション
		"width": 180,
		"height": 70,
		"transparent": true, 	// ウィンドウの背景を透過
		"frame": false, 		// 枠の無いウィンドウ
		"resizable": false, 	// ウィンドウのリサイズを禁止
		"show": false 			// アプリ起動時にウィンドウを表示しない
	});
````

次に、`clock.js`の先頭にウィンドウを表示する記述を追加します。
現在のウィンドウは`require("remote").getCurrentWindow()`でオブジェクトとして取得でき、様々な操作が可能です。
参考資料: [electron/browser-window.md at master · atom/electron](https://github.com/atom/electron/blob/master/docs/api/browser-window.md#instance-methods)

````clock.js
// ウィンドウのオブジェクトを取得
var win = require("remote").getCurrentWindow();

// ウィンドウを表示
win.show();

// 時計の描画処理をスタート
clock();

function clock () {
	// 現在日時を取得
	var d = new Date();
// 以下省略
````

**※ ウィンドウの表示処理よりも前にエラーが発生した場合、アプリ起動後に何も表示されなくなります。**

## ウィンドウ位置の記憶
ウィンドウが表示される位置は、指定なき場合デスクトップの中央になります。
ウィジェットが中央に表示される意味はあまり無いので、前回終了時の位置を記憶し、起動時に復元する処理を追加します。

`clock.js`の先頭を以下のように変更します。

````clock.js
// ウィンドウを開く
openWindow();

// 時計の描画処理をスタート
clock();

function openWindow () {
	// ウィンドウのオブジェクトを取得
	var win = require("remote").getCurrentWindow();

	// ウィンドウ位置を復元
	if (localStorage.getItem("windowPosition")) {
		var pos = JSON.parse(localStorage.getItem("windowPosition"));
		win.setPosition(pos[0], pos[1]);
	}

	// クローズ時にウィンドウ位置を保存
	win.on("close", function() {
		localStorage.setItem("windowPosition", JSON.stringify(win.getPosition()));
	});

	// ウィンドウを表示
	win.show();
}

function clock () {
	// 現在日時を取得
	var d = new Date();
// 以下省略
````

ウィンドウから`getPosition()`で位置が`[x, y]`として得られ、`setPosition(x, y)`で指定した位置に移動できます。
参考資料: [electron/browser-window.md at master · atom/electron](https://github.com/atom/electron/blob/master/docs/api/browser-window.md#instance-methods)

ウィンドウ位置のような設定情報を保存したい場合、ファイルへ書き出す方法もありますが、今回はlocalStorageを利用しています。
localStorageはChromeなどの主要ブラウザに実装されている機能です。
参考資料: [Web Storage－HTML5のAPI、および、関連仕様](http://www.htmq.com/webstorage/)

localStorageには文字列のみが格納できるため、JSON.stringifyを利用しJSONに変換した後格納しています。

## タスクトレイに格納
ウィジェットのような常駐アプリは、タスクトレイに格納すると邪魔になりません。

![タスクトレイに格納](https://qiita-image-store.s3.amazonaws.com/0/83758/634da765-31da-f5fb-b270-28c51e6e4ccb.png)

タスクトレイに格納するためには、アイコンを用意する必要があります。
今回は、こちらの画像を利用しました: ![icon.png](https://qiita-image-store.s3.amazonaws.com/0/83758/47df70dc-2edc-2323-37c9-f1be0977bf69.png)
`app`フォルダに`icon.png`として配置します。

そして、`main.js`に以下の`// タスクトレイに格納`部分を追加します。

```main.js
// 一部抜粋
app.on("ready", function() {
	mainWindow = new BrowserWindow({
		// ウィンドウ作成時のオプション
		"width": 250,
		"height": 70,
		"transparent": true, 	// ウィンドウの背景を透過
		"frame": false, 		// 枠の無いウィンドウ
		"resizable": false, 	// ウィンドウのリサイズを禁止
		"show": false,			// アプリ起動時にウィンドウを表示しない
		"skip-taskbar": true 	// タスクバーに表示しない
	});

	// index.html を開く
	mainWindow.loadUrl("file://" + __dirname + "/index.html");

	mainWindow.on("closed", function() {
		mainWindow = null;
	});

	// タスクトレイに格納
	var Menu = require("menu");
	var Tray = require("tray");
	var nativeImage = require("native-image");

	var trayIcon = new Tray(nativeImage.createFromPath(__dirname + "/icon.png"));

	// タスクトレイに右クリックメニューを追加
	var contextMenu = Menu.buildFromTemplate([
		{ label: "表示", click: function () { mainWindow.focus(); } },
		{ label: "終了", click: function () { mainWindow.close(); } }
	]);
	trayIcon.setContextMenu(contextMenu);

	// タスクトレイのツールチップをアプリ名に
	trayIcon.setToolTip(app.getName());

	// タスクトレイが左クリックされた場合、アプリのウィンドウをアクティブに
	trayIcon.on("clicked", function () {
		mainWindow.focus();
	});
	// タスクトレイに格納 ここまで
});
```
併せて、`// ウィンドウ作成時のオプション`で`"skip-taskbar": true`を指定すると、ウィンドウがタスクバーに表示されなくなります。

参考資料:
[electron/tray.md at master · atom/electron](https://github.com/atom/electron/blob/master/docs/api/tray.md)
[electron/menu-item.md at master · atom/electron](https://github.com/atom/electron/blob/master/docs/api/menu-item.md)

## 配布用パッケージの作成
最後に、配布のためのパッケージ化を行います。
作成したアプリにElectron本体を組み込み、単体で実行可能にします。

パッケージ化は**electron-packager**を利用すると簡単です。

### electron-packagerの導入
**Node.jsを導入していない場合は、以下からインストールして下さい。**
ダウンロード先: [Download | Node.js](https://nodejs.org/en/download/stable/)

コマンドプロンプトから、`npm install -g electron-packager`を実行します。
![electron-packager](https://qiita-image-store.s3.amazonaws.com/0/83758/9a25fd6e-d6a7-90ff-f39e-28e46370c498.png)

### パッケージの作成
electron-packagerは、コマンドプロンプトから以下のように利用します。

```
> electron-packager [appフォルダ] [アプリ名] --platform=[対象OS] --arch=[対象環境] --version=[Electronのバージョン] --asar
```

※ `--asar`はappフォルダをasarでアーカイブ化するオプションで、省略可能です。

(例) Windows 64bit向けのアプリケーションを作成する場合

```
> electron-packager .\app tutorial_clock --platform=win32 --arch=x64 --version=0.35.1 --asar
```
![パッケージの作成](https://qiita-image-store.s3.amazonaws.com/0/83758/0e2ae560-58e5-45c8-aa4c-4f8b9dfa420d.png)

この例の場合、成功すると、`tutorial_clock-win32-x64`というフォルダが作成されます。
フォルダ内の`tutorial_clock.exe`を起動すると、作成したアプリが起動します。

作成したアプリを配布する際は、この`tutorial_clock-win32-x64`をzipなどで圧縮すると良いでしょう。

### 補足: アイコンについて
上記の例では、`tutorial_clock.exe`のアイコンはElectronのアイコンになりますが、`--icon=[アイコンファイル]`を付け加えることで、任意のアイコンを指定できます。

```
> electron-packager .\app tutorial_clock --platform=win32 --arch=x64 --version=0.35.1 --asar --icon=icon.ico
```

Windows向けアプリでは画像ファイルをico形式に変換する必要があります。
いくつかの方法がありますが、以下の様なサービスを利用すると手軽です。
[様々なファビコンを一括生成。favicon generator](http://ao-system.net/favicongenerator/)

## 完成
お疲れ様でした。以上でチュートリアルは終了です。

![完成](https://qiita-image-store.s3.amazonaws.com/0/83758/2d67a7da-384b-8bfd-7547-75ea510c33b4.png)


今回は単なる時計でしたが、中身の処理が複雑になっても基本的な流れは同じです。
インターネット上の様々なAPIを利用すれば、すぐに実用的なウィジェットが作成できるのではないでしょうか。

----

# 付録
## アナログ時計
チュートリアル上は複雑になるため省略しましたが、この時計アプリにはアナログ時計を追加したバージョンがあります。
![アナログ時計](https://qiita-image-store.s3.amazonaws.com/0/83758/3e789e9a-3884-f4d5-85d9-e1dbe62c5cad.png)


### 画像素材
60x60のPNG画像として、背景と針を準備しました。

![clock_bg.png](https://qiita-image-store.s3.amazonaws.com/0/83758/2fac52d7-eb5b-5b09-8fde-4eabe23294dc.png)![clock_hh.png](https://qiita-image-store.s3.amazonaws.com/0/83758/786647e5-44bf-06b5-6e50-a81edd9e6f22.png)![clock_mm.png](https://qiita-image-store.s3.amazonaws.com/0/83758/49d7e784-a488-7c53-6716-36b9735ebb57.png)![clock_ss.png](https://qiita-image-store.s3.amazonaws.com/0/83758/efcd7abe-6a25-b526-e895-4b1b508f5bba.png)

### 実装
背景と針の画像を`position: absolute;`で重ねて表示し、
CSS3の`transform: rotate()`をJavaScriptから操作してそれぞれの針を回転させます。

※ 以下、要点のみを抜粋しています。

```index.html
	<div class="analog">
		<img class="pin" id="pin_bg" src="./img/clock_bg.png">
		<img class="pin" id="pin_hh" src="./img/clock_hh.png">
		<img class="pin" id="pin_mm" src="./img/clock_mm.png">
		<img class="pin" id="pin_ss" src="./img/clock_ss.png">
	</div>
```
```clock.css
.analog {
	position: absolute;
	left: 5;
	float: left;
	width: 60px;
}

.analog .pin {
	position: absolute;
}
```
```clock.js
function updateAnalogClock (d) {
	// 秒針
	var ss_deg = d.getSeconds() * 6;
	document.getElementById("pin_ss").style.transform = "rotate(" + ss_deg + "deg)";

	// 分針
	var mm_deg = d.getMinutes() * 6 + d.getSeconds() / 10;
	document.getElementById("pin_mm").style.transform = "rotate(" + mm_deg + "deg)";

	// 時針
	var hh_deg = d.getHours() * 30 + d.getMinutes() / 2;
	document.getElementById("pin_hh").style.transform = "rotate(" + hh_deg + "deg)";
}
```

ソースコードの全文はGitHubにて公開していますので、ご自由にお使い下さい。
[tutorial_clock/app_option at master · SallyAcolyte/tutorial_clock](https://github.com/SallyAcolyte/tutorial_clock/tree/master/app_option)

## その他のポイント
### URLをブラウザで開く
アプリ内にリンクを作成すると、ブラウザではなくElectron内部で開いてしまいます。
ブラウザで開くには以下のように`require("shell").openExternal(url)`を利用します。

```
var shell = require("shell");
shell.openExternal("https://www.google.com/");
```

### jQueryを利用する
Electronでは通常のJSライブラリを利用できますが、jQueryを利用する場合は少々対策が必要になります。
参考資料: [ElectronでjQueryがundefinedになる - Qiita](http://qiita.com/pirosikick/items/72d11a8578c5c3327069)

----

ご質問・ご指摘等は、コメント欄か[@SallyAcolyte](https://twitter.com/intent/tweet?screen_name=SallyAcolyte)までお気軽にお寄せ下さい。