[Electron] 文字列カウンタのソースコードの解説

More than 3 years have passed since last update.


はじめに

先日、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


アプリの操作方法


  1. カウントしたい文字列をクリップボードにコピーする

  2. 文字数が表示される

example.png


ソースコードの解説


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.jsonmainで、一番最初に起動するスクリプトを指定します。


LCC/package.js

  ...

"main": "./src/main/main.js",
...

アプリ起動直後に、以下のメソッドが一番始めに実行されます。


LCC/src/main/main.js

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

...
});


HTMLファイルのロード

main-window-init.jscreateMainWindow()の中で、メインウィンドウのためのHTMLファイルをロードします。


LCC/src/main/main.js

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

mainWindow = require('./main-window-init.js').createMainWindow();
...
});

HTMLファイルをロードするには、browser-windowモジュールのloadUrl()を使います。


LCC/src/main/main-window-init.js

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の場合にのみ、メニューを作成します。


LCC/src/main/main.js

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

...

if (process.platform == 'darwin') {
require('./menu.js').createMenu();
}

...
});


Menu.buildFromTemplate()を使ってメニューを作成します。labelにメニュー名を、submenuに入れ子にするメニューを指定します。

また、setApplicationMenu()を使って、作成したメニューを登録します。


LCC/src/main/menu.js

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にクリックしたときの処理を書きます。


LCC/src/main/menu-item.js

var app = require('app');

...

module.exports.aboutWindowMenu = function() {
return {
label: 'About ' + app.getName(),
click: function () {
require('./about-window.js').openAboutWindow();
}
};
};

...


about-window.jsopenAboutWindow()を実行すると、以下のようなダイアログが表示されます。

スクリーンショット 2015-08-23 12.03.35.png

ダイアログを表示するには、dialogモジュールのshowMessageBox()を使います。


LCC/src/main/about-window.js

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()
});
};



トレイアイコンの作成


LCC/src/main/main.js

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

...
trayIcon = require('./tray.js').createTray();
...
});

トレイアイコンを作成する際も、Menu.buildFromTemplate()を使ってメニューを作成します。トレイアイコンにメニューを登録するには、trayモジュールのsetContextMenu()を使います。


LCC/src/main/tray.js

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



ショートカットの登録


LCC/src/main/main.js

var shortcut = require('./shortcut.js');

...

app.on('ready', function() {
...
shortcut.registerQuit();
});


ショートカットを登録するには、global-shortcutモジュールのregister()を使います。


LCC/src/main/shortcut.js

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が一番最初に呼び出されます。


LCC/main.html

<!DOCTYPE html>

<html>
...
<body>
...
<script>require('./src/renderer/renderer-main.js');</script>
</body>

</html>



クリップボードの監視

renderer-main.jsの中で、500msec毎にクリップボードを監視しています。クリップボードの内容が変更された場合は、文字数をカウントして表示します。


LCC/src/renderer/renderer-main.js

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)を入れるために、以下の正規表現を使っています。


LCC/src/renderer/string-style.js

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の倍数分連続している数字」を表しており、これは789456789123456789にマッチします。

                       ↓↓↓        ↓↓↓↓↓↓     ↓↓↓↓↓↓↓↓↓

(\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(単語内の文字と文字の間 = 今回の場合、数字と数字の間)」なので、67の間にマッチします。


\B(?=789) → 123456 789

上記のマッチを,で置換するので、以下のようになります。


123456 789 → 123456,789

同様に、\B(?=456789)は、「後ろに456789がある、\B(今回の場合、数字と数字の間)」なので、34の間にマッチします。


\B(?=456789) → 123 456789

上記のマッチを,で置換するので、以下のようになります。


123 456789 → 123,456789

また、\B(?=123456789)は、「後ろに123456789がある、\B(今回の場合、数字と数字の間)」を表しますが、これにマッチする箇所は存在しません。

\B(?=123456789) → マッチする箇所なし

従って、最終的に123,456,789という文字列が出来ます。


改行の取り扱い

Windowsでは、改行コードが\r\nです。これを1文字としてカウントするため、文字列長を求める前に、\r\n\nに変換しています。


LCC/src/renderer/string.js

module.exports.getLengthInCrossPlatform = function(str) {

return str.replace(/\r?\n/g, '\n').length;
};


テストコード

一部のコードについては、mochaというnode.js用のテスティングフレームワークを使って、単体テストを行いました。

テストコードは以下のようになります。


LCC/test/string-style-test.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(...)
テストケースをまとめたテストスイート