Electron製アプリでアイコンバッジを実装する(Mac,Windows対応版)

# アイコンバッジ
Electronでアプリを作っていると、新着のお知らせなどがあった場合にアプリアイコンにバッジを付けたくなります。
Electronにはそれを実現するAPIが用意されているので、基本的には簡単なのですがWindowsでは非常に面倒だったのでまとめておきます。
ちなみにバッジには任意の数値を出すことを想定しています。

Linuxはデスクトップ環境を用意したことがないので対象外です。

## Mac
とても簡単です。

mainプロセス内で `app.setBadgeCount(X)`を呼ぶだけです。

```js:main.js


const electron = require('electron')
const app = electron.app
const BrowserWindow = electron.BrowserWindow

const path = require('path')
const url = require('url')

let mainWindow

function createWindow () {
  mainWindow = new BrowserWindow({width: 800, height: 600})

  mainWindow.loadURL(url.format({
    pathname: path.join(__dirname, 'index.html'),
    protocol: 'file:',
    slashes: true
  }))

  // set badge count
    if (process.platform === 'darwin') {
        app.setBadgeCount(10)
    }
}

```

これを実行すると下記のようになります。
![image](https://qiita-image-store.s3.amazonaws.com/0/2341/0d461067-5afa-39c8-70da-70876ed4c2d9.png)


ただ、このメソッドの引数には数値しか受け取れません。
たとえば、99件までは数値を表示し99件以降は99+として表示したい場合にはこのメソッドは使うことは出来ません。

その為に任意の文字列を設定出来るメソッドが用意されています。

appのなかのdockプロパティの `setBadge(string)`がそのメソッドです。
app.setBadgeではないので気をつけてください。

```js:main.js

function createWindow () {
  mainWindow = new BrowserWindow({width: 800, height: 600})

  mainWindow.loadURL(url.format({
    pathname: path.join(__dirname, 'index.html'),
    protocol: 'file:',
    slashes: true
  }))

  // set badge count
  
    if (process.platform === 'darwin') {
         app.dock.setBadge("99+");
    }
}

```

表示は下記のようになります。

![image](https://qiita-image-store.s3.amazonaws.com/0/2341/76db1383-07a8-6cd2-5b60-d38a9675519c.png)


`app.setBadgeCount`と `app.dock.setBadge`、使い分けが面倒ではあるので、基本的に `app.dock.setbadge`で良い気もします。
数値を扱いたいときは `app.dock.setBadge(""+99)`というように。


## Windows
ここからが鬼門です。

Windowsでは上記メソッドは使えません。
タスクバーに数値や文字を表示させる、というよりもタスクバーの上に画像を表示させる、といったことをします。

そのためのメソッドが`setOverlayIcon(overlay, description)` です。

https://github.com/electron/electron/blob/master/docs/api/browser-window.md#winsetoverlayiconoverlay-description-windows

引数は2つあり、一つ目に表示させる（オーバーレイ）画像、2つめは適当な説明です。（descriptionは画面読み上げ用のアクセシビリティがどうのっぽいです）

上記メソッドは、Rendererプロセスのみで使用できません（このあたりでまたコードが煩雑になって辛いです）。

現在表示しているBrowserWindowを取得し、画像パスを第一引数に指定することで表示することが出来ます。

画像は下記サイトより生成したファイルを利用しました。
http://www.flaticon.com/free-icon/exclamation-mark-sign_12136

```js:renderer.js


const electron = require('electron');
const app = electron.app;
const remote = electron.remote;

 window.onload = function(){
      if (process.platform == 'win32') {
          var mainWindow = remote.getCurrentWindow()
          mainWindow.setOverlayIcon(__dirname+'/icon.png', "icon");
      }
 }

```

Windows上ではこのように表示することが出来ます。

![image](https://qiita-image-store.s3.amazonaws.com/0/2341/bf15b50c-f04f-ea8d-42e7-63818dae7545.png)

エクスクラメーション部分が透過なのでちょっと不格好ですが、表示することが出来ました。

ちなみに非表示にするにはnullを引数にすると非表示になります

` mainWindow.setOverlayIcon(null, "empty");`

### 任意の数値を表示したい
ここで問題なのが、事前に画像を用意する必要があるということです。

たとえば先程のMac版でやったような、任意の数値を表示するといったことをしたい場合、1〜99まで画像を用意してさらに99+といった画像を用意する必要があります。
99個くらいなら..とは思いますが、メールクライアントなら999件くらいは表示して欲しくなりますし、現実的ではなくなります。アプリケーションにバンドルする画像が増えるとアプリケーションサイズが増えてしまいますし、管理も大変です。

そこで、必要な画像は必要になったときに生成することにします。
canvasのAPIを用い、都度画像を生成しましょう。

まずはコードがこんな感じに。

```js:renderer.js
    var nativeImage = electron.nativeImage;

    //1
    var canvas = document.createElement("canvas");

    //2
    canvas.height = 140;
    canvas.width = 140;
    var ctx = canvas.getContext("2d");
    ctx.fillStyle = "red";
    ctx.beginPath();
    ctx.ellipse(70, 70, 70, 70, 0, 0, 2 * Math.PI);
    ctx.fill();
    ctx.textAlign = "center";
    ctx.fillStyle = "white";


    //3
    var text = "1";
    if (text.length > 2) {
      ctx.font = "75px sans-serif";
      ctx.fillText("" + text, 70, 98);
    } else if (text.length > 1) {
      ctx.font = "100px sans-serif";
      ctx.fillText("" + text, 70, 105);
    } else {
      ctx.font = "125px sans-serif";
      ctx.fillText("" + text, 70, 112);
    }

    //4
    var badgeDataURL = canvas.toDataURL();
    //5
    var mainWindow = remote.getCurrentWindow()
    mainWindow.setOverlayIcon(badgeDataURL, "icon");

```

1. canvas要素を生成し
2. 赤い丸を書きます。
3. 桁数によってフォントサイズを調整しつつ文字を入れます。sans-selifじゃなくてもいいです。
4. キャンバスの中身をtoDataURLメソッドでURL形式に出力
5. setOverlayIconで表示


が、実行してみると下記のようなエラーになってしまいます。

```

C:\Users\Administrator\Documents\sample\node_modules\electron\dist\resources\electron…:217 Uncaught Error: Could not call remote function 'setOverlayIcon'. Check that the function signature is correct. Underlying error: Error processing argument at index 0, conversion failure from data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIwAAACMCAYAAACuwEE+AAAK+ElEQ…ZBeGjKmGGu7lzA36rRGE1JdeEXoWDGlablemh+iI2s6vr8B3KGUA4FAg25AAAAAElFTkSuQmCC
Error: Could not call remote function 'setOverlayIcon'. Check that the function signature is correct. Underlying error: Error processing argument at index 0, conversion failure from data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIwAAACMCAYAAACuwEE+AAAK+ElEQ…ZBeGjKmGGu7lzA36rRGE1JdeEXoWDGlablemh+iI2s6vr8B3KGUA4FAg25AAAAAElFTkSuQmCC
    at callFunction (C:\Users\Administrator\Documents\sample\node_modules\electron\dist\resources\electron.asar\browser\rpc-server.js:235:11)
    at EventEmitter.<anonymous> (C:\Users\Administrator\Documents\sample\node_modules\electron\dist\resources\electron.asar\browser\rpc-server.js:342:5)
    at emitMany (events.js:127:13)
    at EventEmitter.emit (events.js:201:7)
    at WebContents.<anonymous> (C:\Users\Administrator\Documents\sample\node_modules\electron\dist\resources\electron.asar\browser\api\web-contents.js:231:13)
    at emitTwo (events.js:106:13)
    at WebContents.emit (events.js:191:7)

```



NativeImage形式である必要があるので、dataURLでは駄目なようです。
なので、下記の様にコードを変えます。

NativeImageのクラスメソッドで `createFromDataURL`といったものがあるので利用してNativeImageを出力します。

```js:renderer.js


        //4
        var badgeDataURL = canvas.toDataURL();
        var img = nativeImage.createFromDataURL(badgeDataURL);
        //5
        var mainWindow = remote.getCurrentWindow()
        mainWindow.setOverlayIcon(img, "icon")

```



ですが、再度実行しても下記のようなエラーを出力します。

```

C:\Users\Administrator\Documents\sample\node_modules\electron\dist\resources\electron…:217 Uncaught Error: Could not call remote function 'setOverlayIcon'. Check that the function signature is correct. Underlying error: Error processing argument at index 0, conversion failure from #<Object>
Error: Could not call remote function 'setOverlayIcon'. Check that the function signature is correct. Underlying error: Error processing argument at index 0, conversion failure from #<Object>
    at callFunction (C:\Users\Administrator\Documents\sample\node_modules\electron\dist\resources\electron.asar\browser\rpc-server.js:235:11)
    at EventEmitter.<anonymous> (C:\Users\Administrator\Documents\sample\node_modules\electron\dist\resources\electron.asar\browser\rpc-server.js:342:5)
    at emitMany (events.js:127:13)
    at EventEmitter.emit (events.js:201:7)
    at WebContents.<anonymous> (C:\Users\Administrator\Documents\sample\node_modules\electron\dist\resources\electron.asar\browser\api\web-contents.js:231:13)
    at emitTwo (events.js:106:13)
    at WebContents.emit (events.js:191:7)

```

下記リンクでも同様のエラーが報告されていますが、特に進展はありません。
Electronのバグなのか、そもそもserializeしてない画像は表示できないのが仕様なのかはちょっとわかりません。
https://discuss.atom.io/t/nativeimage-not-working-in-setoverlayicon-in-electron/37648


ローカルに保存されている画像ファイルが表示出来ることは前述でわかっているので、生成したファイルを保存してそれを参照すれば良さそうです。

canvasをbufferに書き出すには `electron-canvas-to-buffer` モジュールを利用します。
https://www.npmjs.com/package/electron-canvas-to-buffer
canvasBufferにcanvasを食わせて画像形式を指定します。

生成したbufferをfs.writeFileSyncによってファイルに書き出します。
Syncにしてるのには特に理由がないのでasyncにしてcallbackでsetOverlayIconしても良いと思います。

```js:renderer.js
        const fs = require('fs');
        const canvasBuffer = require('electron-canvas-to-buffer')

        var buffer = canvasBuffer(canvas, 'image/png')
        fs.writeFileSync(__dirname+'/test.png', buffer)

```

上記コードを実行すると
フォルダ直下にファイルが出力されます。


![image](https://qiita-image-store.s3.amazonaws.com/0/2341/4fe74355-8013-d54e-f76e-1aa4440465d9.png)


あとはこの画像をsetOverlayIconに指定すれば任意の値を表示することが可能になりました。

![image](https://qiita-image-store.s3.amazonaws.com/0/2341/efe72710-7294-c3e4-00ee-641881cff7ae.png)

![image](https://qiita-image-store.s3.amazonaws.com/0/2341/fed975c8-5fd2-6041-2615-c77a3ecfa1eb.png)

![image](https://qiita-image-store.s3.amazonaws.com/0/2341/4b6ce6fa-96b0-1547-5d82-dbfa1ba2f0c2.png)

## 注意点
上記コードでは `__dirname+'/test.png' ` というように画像ファイルを出力し参照しています。
ですが、electron-packagerなどでパッケージングしてしまうと、__dirnameも配下などはasarとして固められてしまうのでファイルが書き出せません（このあたりは詳しくないので間違っているかもしれません）
`npm start`などで実行してるときは気づきませんが、パッケージングしてから気づきました。

これを避けるために、適当にアクセス出来るパスにファイルを保存しましょう。
Rendererプロセス内では、`remote.app.getPath('temp')`から、tempフォルダを得られるのでここに保存することにしましょう。
tempはちょっと...という方は下記リンクから要件にあうフォルダを選んでください。r/wの権限には気をつけてください。
http://qiita.com/progre/items/2718f4ad20eecf27d599

得たパスを、保存フォルダとして使ってください。


```js:renderer.js

        var buffer = canvasBuffer(canvas, 'image/png')
        var iconSavePath = remote.app.getPath('temp')+ '/gen_icon.png'
        fs.writeFileSync(iconSavePath, buffer);
        var mainWindow = BrowserWindow.getCurrentWindow();
        mainWindow.setOverlayIcon(iconSavePath ,"count");


```

ファイル生成処理は多少はパフォーマンスに難ありだとは思うので、表示したい件数に変更があったときのみ処理が実行するなどの工夫はしたほうがいいでしょう。


## 実行テスト環境
macOS Sierra 10.12.2
Windows Server2016(AWS ec2 instance)
Node.js 6.5.0
Chromium 53.0.2785.143
Electron 1.4.14

## サンプルプロジェクト
今回使用したコードは下記プロジェクトにまとめてあります。
electron-quick-startをベースにしています。
https://github.com/dmnlkGarbage/electron-dock-icon-sample


## 参考リンク
https://github.com/electron/electron/tree/master/docs
http://mylifeforthecode.com/setting-a-custom-taskbar-icon-for-an-electron-window/
https://gist.github.com/johnthedebs/c22a04fcd08e598e69b8
https://discuss.atom.io/t/nativeimage-not-working-in-setoverlayicon-in-electron/37648
http://qiita.com/progre/items/2718f4ad20eecf27d599