JavaScript
Electron

[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(...) テストケースをまとめたテストスイート