[Electron] ダイアログで指定したファイルに保存する

前回はファイルダイアログで読み込みを行いましたが、今回は保存を行います。

今回もOSの機能を利用するダイアログの表示とファイルを読み込む部分はメインプロセスが担当し、それ以外の部分をレンダラープロセス(Chromium上で動いている箇所)が行うことにします。レンダラーでもOSの機能を利用できますがメインプロセスに任せた方が役割分担がはっきりして個人的に気持ち良いためです。

※「読み込み」が行いたい方はこちらの記事を参照ください。

デモ動画

「保存」ボタンを押すとファイルダイアログが開き、指定した場所にテキストファイルの内容を保存します。

ソースコード

実際に稼働するソースはGitHubからも確認できます。 github.com

メインプロセス

前回の読み込みを行った際とあまり変わりません。

ファイルダイアログを開いて保存する処理は48行目以降のipcMain.handle()で行っています。 ipcMain.handle()はレンダラープロセスからの通信を受け取るための物で、第1引数にイベント名、第2引数に具体的な処理を書いた関数を指定します。ファイルに保存する文字列はレンダラーからもらっています。

今回もElectronのチュートリアルと異なる点は背景色を変更しています。

  • 1行目でipcMain, dialogを追加で読み込んでいます。
  • 2行目でファイル処理を行いますのでfsモジュールを追加
  • 4行目でウィンドウを管理する変数をグローバルで持ちます
const { app, ipcMain, BrowserWindow, dialog } = require('electron')
const fs = require('fs')

let mainWin;

/**
 * ウィンドウを作成する
 */
function createWindow () {
  // ウィンドウを新たに開く
  mainWin = new BrowserWindow({
    width: 800,
    height: 600,
    webPreferences: {
      nodeIntegration: true
    }
  })

  // ファイルを開く
  mainWin.loadFile('public/index.html')
}

// 初期化が終了したらウィンドウを新規に作成する
app.whenReady().then(createWindow)


// すべてのウィンドウが閉じられたときの処理
app.on('window-all-closed', () => {
  // macOS以外はアプリを終了する
  if (process.platform !== 'darwin') {
    app.quit()
  }
})

// アプリがアクティブになった時の処理
// (macOSはDocのアイコンがクリックされたとき)
app.on('activate', () => {
  // ウィンドウがすべて閉じられている場合は新しく開く
  if (BrowserWindow.getAllWindows().length === 0) {
    createWindow()
  }
})

/**
 * [IPC] 指定ファイルを保存する
 *
 */
ipcMain.handle('file-save', async (event, data) => {
  // 場所とファイル名を選択
  const path = dialog.showSaveDialogSync(mainWin, {
    buttonLabel: '保存',  // ボタンのラベル
    filters: [
      { name: 'Text', extensions: ['txt', 'text'] },
    ],
    properties:[
      'createDirectory',  // ディレクトリの作成を許可 (macOS)
    ]
  });

  // キャンセルで閉じた場合
  if( path === undefined ){
    return({status: undefined});
  }

  // ファイルの内容を返却
  try {
    fs.writeFileSync(path, data);

    return({
      status: true,
      path: path
    });
  }
  catch(error) {
    return({status:false, message:error.message});
  }
});

レンダラープロセス

「保存」ボタンが押されたら32行目のipcRenderer.invoke()でメインプロセス内の処理を呼び出します。

レンダラープロセスでは保存するための文字列を渡し、あとは結果を待つだけです。もしユーザーがキャンセルした場合にはundefined、何らかの理由で保存できなかった場合は{status:false}が入っているのでそれぞれ適当な処理を行います。

<!DOCTYPE html>
<html>
<head>
  <meta charset=&quot;UTF-8&quot;>
  <meta http-equiv=&quot;Content-Security-Policy&quot; content=&quot;script-src 'self' 'unsafe-inline';&quot; />
  <title>Electron Smaple - FileSave</title>
  <style>
    body{ background-color:white; }
    #btn-save{ margin:12px; }
    #text{ width:100%; height:400px; font-size:14pt; line-height:140%; padding:5px;}
    #message{ display:none; }
  </style>
</head>
<body>

<form>
  <button type=&quot;button&quot; id=&quot;btn-save&quot;>保存</button>
  <span id=&quot;message&quot;></span>
</form>
<textarea id=&quot;text&quot;></textarea>

<script>
const {ipcRenderer} = require('electron');
const text = document.querySelector(&quot;#text&quot;);

window.addEventListener('load', ()=>{
  text.focus();
})

document.querySelector('#btn-save').addEventListener('click', () => {
  // メインプロセスを呼び出し
  ipcRenderer.invoke('file-save', text.value)
    .then((data) => {
      // キャンセルで閉じた
      if( data.status === undefined ){
        return(false);
      }
      // 保存できなかった
      if( ! data.status ){
        alert(`ファイルが開けませんでした\n${data.message}`);
        return(false);
      }

      // 保存できた
      const message = document.querySelector('#message');
      message.textContent = 'ファイルに保存できました';
      message.style.display = 'inline';
      text.focus();
    })
    .catch((err) => {
      alert(err);
    });
});
</script>
</body>
</html>

関連ページ

ファイルダイアログで読み込みを行う場合はこちら。 blog.katsubemakito.net

参考ページ