クリップボード履歴の管理とクリップボードを定期的にクリアするアプリを Electron + Vue + Vuetify で作りました

# はじめに

クリップボード履歴の管理とクリップボードを定期的にクリアするアプリを [Electron](https://www.electronjs.org/) + [Vue](https://vuejs.org/) + [Vuetify](https://vuetifyjs.com/) で作りました。

作ったアプリは[こちら](https://regular-clipboard-cleaner.aprifield.com/)です。
Electron で作成しましたので、Windows、macOS のどちらでも動作します。

ソースコードは [GitHub](https://github.com/aprifield/regular-clipboard-cleaner/) 上で公開しているので詳細はそちらを参照していただくとして、この記事では主に今回のアプリ開発のポイントを記載していきます。

## アプリの目的

在宅勤務が増えた昨今、個人 PC で働く機会も増えたと思いますが、うっかり個人的な情報などをクリップボード経由で流出させてしまうミスを防ぐ目的で作りました。
(ネットで検索して見つけたアプリが動かなかったので自作しました。。。)

## アプリのおすすめポイント

作成したアプリにはクリップボード履歴からの貼り付け機能があり、貼り付け時のテキストを JavaScript を利用して任意にカスタマイズすることが可能です。
履歴を選択したとき任意のキーを押しているかどうかで分岐することが可能で、キーの組み合わせの数だけカスタマイズの種類を選択することが可能となります。
よかったら使ってみてください。

# 開発プロジェクトの作成

アプリは [Electron](https://www.electronjs.org/) + [Vue](https://vuejs.org/) + [Vuetify](https://vuetifyjs.com/) で作成しています。
プロジェクトのテンプレートは [Vue CLI](https://cli.vuejs.org/) で構築しています。
執筆時の Vue CLI バージョンは `4.5.14` です。
そのほかのアプリで利用しているバージョンについては [package.json](https://github.com/aprifield/regular-clipboard-cleaner/blob/main/package.json) を参照してください。

Vue プロジェクトの作成から Vuetify と Electron をインストールする手順は [Vuetify の公式サイト](https://vuetifyjs.com/ja/getting-started/installation/) にまとめられているその通りに実行します。

以下のコマンドで Vue プロジェクトを作成します。
Vuetify がまだ Vue3 を正式にサポートしていないので、Vue2 を利用します。

```
vue create regular-clipboard-cleaner
```

Vue プロジェクトを作成したら Vuetify をインストールします。

```
cd regular-clipboard-cleaner
vue add vuetify
```

Vuetify をインストールするときは Advanced install オプションを選択したほうがよさそうです。
Advanced install オプションを選択する理由は、アイコンフォントをアプリに組み込むためです。
これを選択しないと index.html にアイコンフォントのパスが指定され、アプリ起動時に必要に応じてタウンロードされることになります。
オフライン環境を想定しないならデフォルトのままで大丈夫です。

```
? Choose a preset: Configure (advanced)
? Select icon font Material Design Icons
? Use fonts as a dependency (for Electron or offline)? Yes
```

Vuetify をインストールしたら electron-builder をインストールします。

```
vue add electron-builder
```

プロジェクトの準備はこれだけです。
以下のコマンドでアプリを起動して開発していきます。

```
npm run electron:serve
```

以下のコマンドを実行すると Windows インストーラーを作ることが可能です。
Mac で実行すればディスクイメージが作られます。

```
npm run electron:build
```

# アイコンの作成

Electron のデフォルトのアイコンだと味気ないのでオリジナルのアイコンを作成します。

といっても今回はフリーのアイコンを少し加工しただけです。
アイコンはベースは、マテリアルデザインの [clipboard-text-clock-outline](https://icon-sets.iconify.design/mdi/clipboard-text-clock-outline/) とし、オリジナリティーを出したいのでこのアイコンの時計の部分を Font Awesome の[ほうきのアイコン](https://fontawesome.com/v5.15/icons/broom) の先の部分を取り出して結合しました。
画像編集には、オンライン SVG エディターである [Vectr](https://vectr.com) を利用させてもらいました。

出来上がった画像は [electron-icon-builder](https://github.com/safu9/electron-icon-builder) を使って Electron 用にビルドしました。
ビルドしたアイコンは [build/icons](https://github.com/aprifield/regular-clipboard-cleaner/tree/main/build/icons) に配置します。

この状態で `npm run electron:build` を実行すればアプリのアイコンとしてビルドされます。

# アプリの開発

ここからはクリップボード履歴の管理とクリップボードを定期的にクリアするアプリを開発するために必要な観点をソースコードを交えて記載していきます。

この記事で記載しているソースコードは、記載したい観点ごとに抜粋していますのでコピペしても動かないかもしれません。
参考にしたい場合は GitHub に公開しているソースほうを参照してください。

## 常駐アプリ

バックグラウンドで監視するアプリなので常駐させる必要があります。
Vue CLI で [electron-builder](https://www.electron.build/) をインストールすると以下のようなサンプルソースが生成されます。
デフォルトだとすべてのウィンドウを閉じるとアプリを終了するのでそうしないように `app.quit()` の処理を削ります。

```typescript
app.on('window-all-closed', () => {
  if (process.platform !== 'darwin') {
    // app.quit(); // デフォルトだとすべてのウィンドウを閉じるとアプリを終了する
  }
});
```

https://github.com/aprifield/regular-clipboard-cleaner/blob/main/src/background.ts

## タスクトレイ

前述のコメントアウトによりウィンドウを閉じてもアプリを起動し続けますが、任意のタイミングで終了したいときにタスクを KILL するわけにはいかないのでタスクトレイを利用します。

Electron の `Tray` を new するだけでタスクトレイが利用可能となり、Windows なら右下に、macOS なら右上にアイコンが表示されます。

タスクトレイを実装する上でのポイントは `Tray` インスタンスをブロックの外で参照し続けることです。
こうしないとアイコンが消えます。
[公式サイト](https://www.electronjs.org/docs/faq#my-apps-tray-disappeared-after-a-few-minutes) に書いてある通りです。

```typescript
import { app, Menu, Tray } from 'electron';

let tray: Tray | null = null;

app.whenReady().then(() => {
  tray = new Tray(
    path.join(
      __static,
      process.platform === 'win32' ? 'icon.ico' : 'icon-16x16.png'
    )
  );
});
```

https://github.com/aprifield/regular-clipboard-cleaner/blob/main/src/background/app-tray-helper.ts

もう 1 つのポイントとして Windows の場合は ICO 形式を指定することです。公式サイトに書いてある通りですが、確かに ICO 形式を指定したほうがきれいに表示されます。

> Windows では、最適な視覚効果を得るために ICO 形式のアイコンファイルを使用することが推奨されています。

macOS の場合は 16x16 の png を指定しつつ高解像度向けディスプレイ向けにファイル名に `@2x` をつけた画像を用意するとよいらしいです。一応 `icon-16x16@2x.png` というファイルを配置しているのですが、効果があるかどうか未検証です。

## 自動起動

PC 起動のたびに常駐アプリを起動するのは手間なので自動起動するようにします。
1 つのメソッド(`app.setLoginItemSettings`) を実行するだけでよいです。

```typescript
import { app } from 'electron';
const isDevelopment = process.env.NODE_ENV !== 'production';

export function setOpenAtLogin() {
  if (isDevelopment) {
    return; // 開発中は自動起動に登録しない
  }

  app.setLoginItemSettings({
    openAtLogin: true // 自動起動を無効化する場合は false を指定する
  });
}

setOpenAtLogin();
```

https://github.com/aprifield/regular-clipboard-cleaner/blob/main/src/background/app-login-helper.ts

## 多重起動防止

バックグラウンドで監視するアプリは複数起動する必要はないし、複数起動すると困ることもあるので多重起動を禁止します。
アプリ起動時に最初に実行されるソースで `app.requestSingleInstanceLock()` を実行すると初回起動かどうか判断できるのでそれを利用します。
最初に起動していたアプリからは新しく起動されたかどうかを判断できるので、必要に応じて処理を入れます。

```typescript
import { app } from 'electron';
const gotTheLock = app.requestSingleInstanceLock();

if (!gotTheLock) {
  app.quit(); // すでに起動中なので終了する
} else {
  app.on('second-instance', () => {
    // 先に起動しているアプリの場合はここで新しく起動された時の処理を入れる
  });
}
```

https://github.com/aprifield/regular-clipboard-cleaner/blob/main/src/background.ts

## クリップボードの監視と定期クリア

Electron にはクリップボードにコピーしたときのイベントを検知するような機能ななさそうです。
そのため、`setInterval` で定期実行して処理します。
クリップボードの読み書きの API は Election が提供してくれています。

```typescript
import { clipboard } from 'electron';

function startMonitoring() {
  // このサンプルだと1秒ごとにクリップボードをクリアする
  return setInterval(() => {
    clipboard.clear();
  }, 1000);
}
```

https://github.com/aprifield/regular-clipboard-cleaner/blob/main/src/background/clipboard-cleaner.ts

## クリップボードの監視と履歴の保存

前述の通り、クリップボードのコピーイベントを検知することはできないので、定期的にクリップボードを読み込み、読み込んだテキストと前回保存したテキストが異なる場合に履歴を保存するようにしています。

```typescript
import { clipboard } from 'electron';

function startMonitoring() {
  // このサンプルだと1秒ごとにクリップボードを読み込み、テキストが異なる場合に履歴を保存する
  const historyItems = getHistoryItems();
  const text = clipboard.readText();
  return setInterval(() => {
    if (!historyItems[0] || historyItems[0].text !== text) {
      setHistoryItems(historyItems);
    }
  }, 1000);
}
```

https://github.com/aprifield/regular-clipboard-cleaner/blob/main/src/background/clipboard-cleaner.ts

Electron はネイティブアプリなのでファイル操作は簡単にできますが、ファイルの格納場所などいろいろと考慮してゼロから作るのは手間ですのでライブラリを利用させてもらいます。

今回利用したライブラリは [electron-store](https://github.com/sindresorhus/electron-store) です。
このライブラリを利用してよかったと思うことは、ファイルの格納場所はお任せできること、暗号化できること、初回読み込み時のデフォルト値を定義できることです。

```typescript
import Store from 'electron-store';

const clipboardStore = new Store<{ clipboard: HistoryItem[] }>({
  name: '.clipboard', // ファイル名
  fileExtension: '', // ファイル拡張子
  encryptionKey: 'LzCHFd8929C4W1EEN6hPCsPYtIVTBjx7', // ファイルの暗号化キー(保存したファイルを流出するようなことは考えていないのでハードコーディング)
  defaults: { clipboard: [] } // デフォルト値
});

export function getHistoryItems() {
  return clipboardStore.get('clipboard');
}

export function setHistoryItems(historyItems: HistoryItem[]) {
  clipboardStore.set('clipboard', historyItems);
}
```

https://github.com/aprifield/regular-clipboard-cleaner/blob/main/src/background/electron-store-helper.ts

## ショートカット

クリップボード履歴を即座に表示できるようにショートカットを利用します。
タスクトレイからマウス操作で表示することも可能ですが、基本的にはショートカットを利用するかと思います。

ショートカットの登録は `globalShortcut.register` を実行するだけで、引数に割り当てたいキーとショートカットを実行したときのコールバックを登録するだけです。

```typescript
import { app, globalShortcut } from 'electron';

export function registerShortcut() {
  const result = globalShortcut.register('CommandOrControl+Shift+C', () => {
    // ショートカット実行時の処理
  });
}

app.whenReady().then(() => {
  registerShortcut();
});
```

https://github.com/aprifield/regular-clipboard-cleaner/blob/main/src/background/global-shortcut-helper.ts

## 貼り付け

クリップボード履歴を選択したら貼り付けをできるようにします。
貼り付け機能(キーストロークの送信機能)は Electron の標準機能にはないので、これについてもライブラリを利用させてもらいます。

今回利用したライブラリは [robotjs](https://github.com/octalmage/robotjs) です。
ドキュメント上は macOS でも動作しそうに見えますが、なぜかアプリがクラッシュするので Windows 限定となります。

```typescript
import robot from 'robotjs';
robot.keyTap('v', 'control');
```

https://github.com/aprifield/regular-clipboard-cleaner/blob/main/src/background.ts

robotjs は環境ごとにビルドが必要で高速に動作しますがビルド環境を作るのが手間です。

ライトに対応するなら [node-key-sender](https://github.com/garimpeiro-it/node-key-sender) というものもあります。
こちらは jar を実行する構成なので Java が必須という点と、インストーラーとしてバンドルしにくいという点ではマイナスな印象です。

node-key-sender は Java から実行するので起動がもたつく問題があり、この問題を改善するために node-key-sender をまねて
[DotNetKeySender](https://github.com/aprifield/DotNetKeySender) というものを作ってみました。
こちらは .NET Framework を利用したアプリです。これでも初回起動時はもたつくので最終的には robotjs を利用させてもらっています。

# まとめ

ここまで記載した内容で、クリップボード履歴の管理とクリップボードを定期的にクリアするアプリの大枠が構築できるのではないかと思います。
あとは画面構築をがんばるだけですが Web 標準の知識があれば構築できますのでここでは省略します。

ほかにも以下のようなことを記載しようかと思いましたが、もろもろ省略します。
これらを考慮して実装していますので参考にしたい場合は GitHub に公開しているソースほうを参照してください。

- Windows のタスクバーや macOS の Doc の制御について
- Windows と macOS のメニューバーの違いについて
- メインプロセスとレンダラープロセスのやり取りについて
- ダークテーマのサポートについて
- 多言語対応(英語と日本語のみ)について

作ってみての感想ですが、
手軽にネイティブアプリが作成できるという点で Electron は優秀だと感じます。
ただ、たいした実装でないにもかかわらずファイルサイズが大きいという点ではマイナスな印象です。
今回のアプリですらインストーラーのファイルサイズが 60MB を超えます。
WebView2 を利用するというアプローチがあり、これを利用することで容量が削減できそうなので今後に期待です。
Electron の公式サイトに [WebView2 と Electron の比較記事](https://www.electronjs.org/ja/blog/webview2) がありましたので載せておきます。