はじめに
先日、Electronを使って、LCCという簡単な文字列カウンタを作りました。この記事では、このアプリのソースコードについて、簡単に解説します。
Electronとは
Electronとは、Web技術(Javascript + HTML + CSS)を使って、クロスプラットフォーム(Mac/Windows/Linux)なデスクトップアプリを開発できる実行環境で、ChromiumやNode.jsをベースに作られています。Electronを使ったアプリとしては、AtomエディタやVisual Studio Codeなどがあります。
開発したアプリ
LCC (Lightweight Character Counter)
対応OS
Mac/Windows
バイナリ
以下のページからダウンロードできます。
↓
https://github.com/cotrpepe/LCC/releases/tag/v0.1.0
ソースコード
ソースコードは、Githubにアップしました。
↓
https://github.com/cotrpepe/LCC
アプリの操作方法
- カウントしたい文字列をクリップボードにコピーする
- 文字数が表示される
ソースコードの解説
LCCのバージョン
v0.1.0のコードを基に解説していきます。
開発環境
- Mac OS X 10.10.5
- node.js v0.12.7
- electron-prebuilt 0.31.0
- electron-packager 5.0.2
- mocha 2.2.5
ディレクトリの構成
Electronアプリは、以下の2種類のプロセスから成ります。
プロセス名 | 担当 |
---|---|
mainプロセス | 全てのWebページとrendererプロセスの管理、ネイティブGUI APIとのやり取りを担当する |
rendererプロセス | 個々のWebページを担当する |
そのため、mainプロセスで動作するコードと、rendererプロセスで動作するコードとで、ディレクトリを分けました。
LCC/
└── src/
├── main/
└── renderer/
アプリの起動
package.json
のmain
で、一番最初に起動するスクリプトを指定します。
...
"main": "./src/main/main.js",
...
アプリ起動直後に、以下のメソッドが一番始めに実行されます。
app.on('ready', function() {
...
});
HTMLファイルのロード
main-window-init.js
のcreateMainWindow()
の中で、メインウィンドウのためのHTMLファイルをロードします。
app.on('ready', function() {
mainWindow = require('./main-window-init.js').createMainWindow();
...
});
HTMLファイルをロードするには、browser-window
モジュールのloadUrl()
を使います。
var path = require('path');
var BrowserWindow = require('browser-window');
module.exports.createMainWindow = function() {
...
var mainWindow = new BrowserWindow({
...
});
...
var htmlPath = path.join(__dirname, '/../../main.html');
mainWindow.loadUrl('file://' + htmlPath); // HTMLファイルのロード
...
};
メニューの作成(Mac only)
Macの場合にのみ、メニューを作成します。
app.on('ready', function() {
...
if (process.platform == 'darwin') {
require('./menu.js').createMenu();
}
...
});
Menu.buildFromTemplate()
を使ってメニューを作成します。label
にメニュー名を、submenu
に入れ子にするメニューを指定します。
また、setApplicationMenu()
を使って、作成したメニューを登録します。
var Menu = require('menu');
var menuItem = require('./menu-item.js');
module.exports.createMenu = function() {
var menu = Menu.buildFromTemplate([ // メニューの作成
{
label: 'File',
submenu: [
menuItem.aboutWindowMenu(),
...
],
},
...
]);
Menu.setApplicationMenu(menu); // メニューの登録
};
menu-item.js
でメニュー項目を作成しています。label
にメニュー名を、click
にクリックしたときの処理を書きます。
var app = require('app');
...
module.exports.aboutWindowMenu = function() {
return {
label: 'About ' + app.getName(),
click: function () {
require('./about-window.js').openAboutWindow();
}
};
};
...
about-window.js
のopenAboutWindow()
を実行すると、以下のようなダイアログが表示されます。
ダイアログを表示するには、dialog
モジュールのshowMessageBox()
を使います。
var app = require('app');
var dialog = require('dialog');
var pjson = require('../../package.json');
module.exports.openAboutWindow = function() {
dialog.showMessageBox({
type: 'info',
buttons: ['ok'],
title: 'About ' + app.getName(),
message: app.getName(),
detail: pjson.description + '\nv' + app.getVersion()
});
};
トレイアイコンの作成
app.on('ready', function() {
...
trayIcon = require('./tray.js').createTray();
...
});
トレイアイコンを作成する際も、Menu.buildFromTemplate()
を使ってメニューを作成します。トレイアイコンにメニューを登録するには、tray
モジュールのsetContextMenu()
を使います。
var app = require('app');
var path = require('path');
var Tray = require('tray');
var Menu = require('menu');
var menuItem = require('./menu-item.js');
module.exports.createTray = function() {
var iconPath = path.join(__dirname, '/../../image/tray_icon.png');
var trayIcon = new Tray(iconPath);
var contextMenu = Menu.buildFromTemplate([ // メニューの作成
menuItem.aboutWindowMenu(),
menuItem.helpMenu(),
{ type: 'separator' },
menuItem.quitMenu()
]);
...
trayIcon.setContextMenu(contextMenu); // メニューの登録
return trayIcon;
};
ショートカットの登録
var shortcut = require('./shortcut.js');
...
app.on('ready', function() {
...
shortcut.registerQuit();
});
ショートカットを登録するには、global-shortcut
モジュールのregister()
を使います。
var app = require('app');
var globalShortcut = require('global-shortcut');
module.exports.registerQuit = function() {
var shortcut = process.platform == 'darwin' ? 'Command+Q' : 'Ctrl+Q'; // Mac向けとWindows向けでショートカットを切り替える
var ret = globalShortcut.register(shortcut, function() { // ショートカットの登録
app.quit();
});
...
};
Rendererプロセスの開始
Rendererプロセスでは、renderer-main.js
が一番最初に呼び出されます。
<!DOCTYPE html>
<html>
...
<body>
...
<script>require('./src/renderer/renderer-main.js');</script>
</body>
</html>
クリップボードの監視
renderer-main.js
の中で、500msec毎にクリップボードを監視しています。クリップボードの内容が変更された場合は、文字数をカウントして表示します。
var clipboard = require('clipboard');
...
var prevLen = string.getLengthInCrossPlatform(clipboard.readText());
...
setInterval(function() { // 500msec毎に繰り返し
var curLen = string.getLengthInCrossPlatform(clipboard.readText());
if (prevLen != curLen) { // クリップボードが更新されていた場合
prevLen = curLen;
mainWindow.update(curLen); // 表示される文字数のアップデート
}
}, 500);
数字の桁区切り
文字数を表示する際に、3桁ごとに区切り文字(123456789 → 123,456,789
)を入れるために、以下の正規表現を使っています。
module.exports.addThousandsSeparator = function(number) {
...
return number.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
};
この正規表現は、Stack OverflowのElias Zamariaさんの回答を参考にしています。
補足)上記の正規表現の意味
ここで例として、123456789(123,456,789)という数字を取り上げます。
(\d{3})+(?!\d)
の部分は、「数字が後ろに続かない、3の倍数分連続している数字」を表しており、これは789
と456789
と123456789
にマッチします。
↓↓↓ ↓↓↓↓↓↓ ↓↓↓↓↓↓↓↓↓
(\d{3})+(?!\d) → 123456789 and 123456789 and 123456789
つまり、\B(?=(\d{3})+(?!\d))
は、\B(?=789)
と\B(?=456789)``\B(?=123456789)
になります。
\B(?=(\d{3})+(?!\d)) → \B(?=789) and \B(?=456789) and \B(?=123456789)
\B(?=789)
は、「後ろに789
がある、\B(単語内の文字と文字の間 = 今回の場合、数字と数字の間)」なので、6
と7
の間にマッチします。
↓
\B(?=789) → 123456 789
上記のマッチを,
で置換するので、以下のようになります。
↓
123456 789 → 123456,789
同様に、\B(?=456789)
は、「後ろに456789
がある、\B(今回の場合、数字と数字の間)」なので、3
と4
の間にマッチします。
↓
\B(?=456789) → 123 456789
上記のマッチを,
で置換するので、以下のようになります。
↓
123 456789 → 123,456789
また、\B(?=123456789)
は、「後ろに123456789
がある、\B(今回の場合、数字と数字の間)」を表しますが、これにマッチする箇所は存在しません。
\B(?=123456789) → マッチする箇所なし
従って、最終的に123,456,789
という文字列が出来ます。
改行の取り扱い
Windowsでは、改行コードが\r\n
です。これを1文字としてカウントするため、文字列長を求める前に、\r\n
を\n
に変換しています。
module.exports.getLengthInCrossPlatform = function(str) {
return str.replace(/\r?\n/g, '\n').length;
};
テストコード
一部のコードについては、mochaというnode.js用のテスティングフレームワークを使って、単体テストを行いました。
テストコードは以下のようになります。
var assert = require('assert');
var ss = require('../src/renderer/string-style.js');
describe('string-style.js', function() {
describe('addThousandsSeparator()', function() {
it('shouldn\'t add the thousands separator to zero', function() {
assert.equal('0', ss.addThousandsSeparator(0));
});
...
});
describe('getScaledFontSize()', function () {
it('should return scaled font size', function () {
...
});
...
});
});
記述 | 意味 |
---|---|
it(...) |
テストケース |
describe(...) |
テストケースをまとめたテストスイート |