Help us understand the problem. What is going on with this article?

[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(...) テストケースをまとめたテストスイート
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした