Electronアプリで、アプリアイコンにドロップされたファイルを処理する

Finder上のアプリアイコンにファイルをドロップされた時、
Dock上のアプリアイコンにファイルをドロップされた時、
Electron製のアプリで、そのファイルを受け取って処理する機能の実装方法です。

(Macの話だったり、私製アプリのコードの断片が入ってたり、
Typescriptだったり、パラメータが雑だったりするけど、うまく読み替えて欲しい。)


アプリのデータの持ち方にもよりますけど、
・アプリが起動中にファイルをドロップする
・アプリが起動していない時にファイルをドロップする
で、処理方法を変える必要があると思う。

Info.plistでアプリがファイルを受け取れるように設定する

  • Mac環境の話
  • ファイルをアプリアイコンに落とした時に反応するように、アプリのInfo.plistを更新します。
  • 次のようなplistファイルを作って、アプリのInfo.plistに組み込みます。
  • この設定を入れないと、ファイルをドロップしても、アプリが反応しない。
  • extend.plist
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
  <dict>
    <key>CFBundleDocumentTypes</key>
    <array>
      <dict>
        <key>CFBundleTypeName</key>
        <string>All Files</string>
        <key>LSHandlerRank</key>
        <string>Owner</string>
        <key>LSItemContentTypes</key>
        <array>
          <string>public.text</string>
          <string>public.data</string>
          <string>public.content</string>
        </array>
      </dict>
    </array>
  </dict>
</plist>
  • このextend.plistを、electron-packagerコマンド実行時に組み込んで、Info.plistにパラメータを追加します。
  • extend-infoで指定します。
electron-packager . YourAppName --platform=darwin --arch=x64 --extend-info=extend.plist



ファイルドロップのイベントを受け取る

  • "will-finish-launching"のタイミングで、"open-file"イベントをハンドリングする処理を登録します。(ここより前で拾うとマズイのかな?試さないけど)
  • main processのjavascript内に実装します。
  • preventDefault()した後に、ファイルをハンドリングする処理を入れます。
import {app} from 'electron';

// receive drop file to app icon event
let launchArgs = null;
app.on('will-finish-launching', () => {

  app.on('open-file', (event, filePath) => {
    event.preventDefault();

    if (myApp.mainWindow) {
      myApp.mainWindow.webContents.send('dropTextFile', filePath);
    } else {
      launchArgs = { filePath: filePath };
    }
  });

});

...(略)...


アプリアイコンにファイルをドロップする際、

  • アプリが起動済みである場合と、
  • アプリがまだ起動していない場合がありますよね。


アプリが起動済みで、BrowserWindowのインスタンスが既に作られているのであれば、
このようなファイルが渡されたよ、とイベントを飛ばせば事足ります。

if (myApp.mainWindow) {
  myApp.mainWindow.webContents.send('dropTextFile', filePath);
}

BrowserWindowインスタンス初期化時に、ファイルの情報を渡す

  • 起動していないアプリに、ファイルがドロップされた場合は、BrowserWindowのインスタンスはまだ作られていません。
  • ウィンドウのインスタンスが出来てから、イベントを投げて、ファイルの情報を渡してあげましょう。
  • (何らかの変数越しにパラメータを渡しても良いですけれど)
function showMainWindow(launchArgs): void {
  const myApp = this;
  let _launchArgs = launchArgs;

  mainWindow = new BrowserWindow({
    width: 800,
    height: 800,
    x: x,
    y: y,
    show: false, // show at did-finish-load event
  });
  mainWindow.loadURL(`file://${__dirname}/contents-main.html`);

  // main window event
  mainWindow.webContents.on('did-finish-load', () => {
    // receive drop file to app icon event
    if (_launchArgs && _launchArgs.filePath) {
      const filePath = _launchArgs.filePath; _launchArgs = null; // for window reload
      mainWindow.webContents.send('dropTextFile', filePath);
    }

    // show
    mainWindow.show();
    mainWindow.focus();
  });
  • "did-finish-load"のタイミングで、BrowserWindowインスタンスにイベントを投げています。
  • ドロップされたファイルの情報をクリアしておかないと、BrowserWindowを再読込した時に、また読み込まれてしまいますよ。
// main window event
mainWindow.webContents.on('did-finish-load', () => {
  // receive drop file to app icon event
  if (_launchArgs && _launchArgs.filePath) {
    const filePath = _launchArgs.filePath;
    _launchArgs = null;      // リロードされた時のために変数の中身を消しておく
    mainWindow.webContents.send('dropTextFile', filePath);
  }

  // show
  mainWindow.show();
  mainWindow.focus();
});

renderer processに渡されたファイルを読み込む

  • renderer processで渡されたファイルを読み込みます。
  • 起動済みのアプリにイベントを投げられた場合、起動前のアプリにイベントを投げられた場合で、どちらも同じインターフェイスで処理出来ると良いですね。
  • ここではipcRendererを使って処理しています。
// dropTextFile event
ipcRenderer.on('dropTextFile', (event, filePath: string) => {
  fs.readFile(filePath, 'utf-8', (err: Error, data: string) => {
    if (err) {
      MessageService.error('テキストファイルを読み込めませんでした。', err);
      return;
    }

    const win = remote.getCurrentWindow();
    win.focus();
    $scope.yinput.source = data;
    $timeout(() => { $scope.$apply(); });
  });
});
  • アプリアイコンにファイルを落とした時は、Electronアプリにはフォーカスが当たっていません。
  • このままでは、Electronアプリの画面のデータが更新されない、などの問題が起きます。
  • focus()を実行して、ウィンドウにフォーカスを当てましょう。
var remote = require('electron').remote;

const win = remote.getCurrentWindow();
win.focus();

makeSingleInstance

  • アプリが複数立ち上がっていると、うまく動かない場合があると思います。
  • (そういう記述を見た。いちいち検証しないよ。私のアプリではシングルインスタンスを有効にしないしね。)
  • makeSingleInstance()を実行して、アプリを複数起動できないようにして対処します。
app.makeSingleInstance((argv: string[], workingDirectory: string) => {});
  • 最新のバージョンでは、makeSingleInstance()は廃止され、代わりにrequestSingleInstanceLock()を使うことになりました。
  • https://electronjs.org/docs/api/app
const locked = app.requestSingleInstanceLock()

おわり

おわりだよ