[Electron] アプリのメニューを作成する

Electronでアプリの「メニュー」を作成します。

基本的にはElectronのドキュメントの内容に沿って、Electronに予め用意されている機能を利用しWinodwsとmacOSの両方に対応するところまでを取り上げます。

Electronの機能でサクッとメニューを実装する

メインプロセスで設定する場合は非常にシンプルで、具体的なメニューをMenu.buildFromTemplate()に渡し、その際の戻り値をMenu.setApplicationMenu()にセットするだけです。

Electronでは予めよく利用するであろうメニューは「role」という機能で簡単に実装できます。

ソースコード - Windows

実際のコードを見た方が早いですね。メニューの大項目毎にJavaScriptのオブジェクトとして指定しているのがわかります。

roleは予め決められた文字列を、labelには実際にメニューに表示する文字列を自由に指定します。labelが未指定の場合はデフォルトで設定されている英語の文字列が出力されます。submenuは文字通り下位のメニューを作成することができます。

const { app, Menu, BrowserWindow } = require('electron');

//------------------------------------
// メニュー
//------------------------------------
// メニューを準備する
const template = Menu.buildFromTemplate([
    {
      label: "ファイル",
      submenu: [
        { role:'close', label:'ウィンドウを閉じる' }
      ]
    },
    {
      label: "編集",
      submenu: [
        { role:'undo',  label:'元に戻す' },
        { role:'redo',  label:'やり直す' },
        { type:'separator' },
        { role:'cut',   label:'切り取り' },
        { role:'copy',  label:'コピー' },
        { role:'paste', label:'貼り付け' },
      ]
    }
]);

// メニューを適用する
Menu.setApplicationMenu(template);

//------------------------------------
// ウィンドウ
//------------------------------------
// 初期化が終了したらウィンドウを新規に作成する
app.whenReady().then(()=>{
   const win = new BrowserWindow({
    width: 1024,
    height: 800,
    webPreferences: {
      nodeIntegration: true,
    }
  })
  win.loadFile('public/index.html')
})

// 以下略

実行結果

これをWindowsで実行すると以下のようなメニューが表示されます。テキストボックスなどを表示しメニューからコピーやカット、貼り付けなどを実際に行うことができます。期待通りの結果になりますね。

macOSに対応するには?

ところが同じコードをmacOSで実行すると以下のように「ファイル」が表示されず、想定しない結果となってしまいました。

普段macOSを使わない方には馴染みが無いと思いますが、macOSではどのアプリを利用してもメニューの構成が同じような作りになっています。つまり異なるアプリを使っても基本的な操作方法がほぼ同じなため利用者が迷うことなく使うことが出来るというわけです。これは自然とそうなったわけではなく、Appleが定めたガイドラインに開発者が従っているためです。詳細についてはAppleのドキュメントを参照ください。 developer.apple.com

そのためElectronのアプリをWindowsmacOSの両方に対応させたい場合には、基本的にmacOSのルールに従って作成する方が手間が少なくてすみます。

ソースコード - Windows/macOS対応版

というわけでWindowsmacOSの両方に対応したコードが以下になります。 Electronのドキュメントにlabelを付けて分かりやすくした物です。そのままコピペなどでご利用いただけますが、独自メニューを追加したり多言語対応する場合などはもう一工夫必要です。

const { app, Menu, BrowserWindow } = require('electron');

// 実行環境がmacOSならtrue
const isMac = (process.platform === 'darwin');  // 'darwin' === macOS

//------------------------------------
// メニュー
//------------------------------------
// メニューを準備する
const template = Menu.buildFromTemplate([
  ...(isMac ? [{
      label: app.name,
      submenu: [
        {role:'about',      label:`${app.name}について` },
        {type:'separator'},
        {role:'services',   label:'サービス'},
        {type:'separator'},
        {role:'hide',       label:`${app.name}を隠す`},
        {role:'hideothers', label:'ほかを隠す'},
        {role:'unhide',     label:'すべて表示'},
        {type:'separator'},
        {role:'quit',       label:`${app.name}を終了`}
      ]
    }] : []),
  {
    label: 'ファイル',
    submenu: [
      isMac ? {role:'close', label:'ウィンドウを閉じる'} : {role:'quit', label:'終了'}
    ]
  },
  {
    label: '編集',
    submenu: [
      {role:'undo',  label:'元に戻す'},
      {role:'redo',  label:'やり直す'},
      {type:'separator'},
      {role:'cut',   label:'切り取り'},
      {role:'copy',  label:'コピー'},
      {role:'paste', label:'貼り付け'},
      ...(isMac ? [
          {role:'pasteAndMatchStyle', label:'ペーストしてスタイルを合わせる'},
          {role:'delete',    label:'削除'},
          {role:'selectAll', label:'すべてを選択'},
          {type:'separator' },
          {
            label: 'スピーチ',
            submenu: [
              {role:'startSpeaking', label:'読み上げを開始'},
              {role:'stopSpeaking',  label:'読み上げを停止'}
            ]
          }
        ] : [
          {role:'delete',    label:'削除'},
          {type:'separator'},
          {role:'selectAll', label:'すべてを選択'}
        ])
     ]
  },
  {
    label: '表示',
    submenu: [
      {role:'reload',         label:'再読み込み'},
      {role:'forceReload',    label:'強制的に再読み込み'},
      {role:'toggleDevTools', label:'開発者ツールを表示'},
      {type:'separator'},
      {role:'resetZoom',      label:'実際のサイズ'},
      {role:'zoomIn',         label:'拡大'},
      {role:'zoomOut',        label:'縮小'},
      {type:'separator'},
      {role:'togglefullscreen', label:'フルスクリーン'}
    ]
  },
  {
    label: 'ウィンドウ',
    submenu: [
      {role:'minimize', label:'最小化'},
      {role:'zoom',     label:'ズーム'},
      ...(isMac ? [
           {type:'separator'} ,
           {role:'front',  label:'ウィンドウを手前に表示'},
           {type:'separator'},
           {role:'window', label:'ウィンドウ'}
         ] : [
           {role:'close',  label:'閉じる'}
         ])
    ]
  },
  {
    label:'ヘルプ',
    submenu: [
      {label:`${app.name} ヘルプ`},    // ToDo
      ...(isMac ? [ ] : [
        {type:'separator'} ,
        {role:'about',  label:`${app.name}について` }
      ])
    ]
  }
]);

// メニューを適用する
Menu.setApplicationMenu(template);

//------------------------------------
// ウィンドウ
//------------------------------------
// 初期化が終了したらウィンドウを新規に作成する
app.whenReady().then(()=>{
   const win = new BrowserWindow({
    width: 1024,
    height: 800,
    webPreferences: {
      nodeIntegration: true,
    }
  })
  win.loadFile('public/index.html')
})

// 以下略

Windowsの実行結果

このコードをWindowsで実行すると以下のようになります。

macOSの実行結果

同じコードでmacOS用のメニューも以下のように作成できました。

参考ページ