Electronデスクトップアプリ開発入門(2)

Electronデスクトップアプリ開発入門(2)

Electron APIデモから学ぶ実装テクニック ― ウィンドウ管理とメニュー

2017年2月9日

Electron API Demosで紹介されている、Electronアプリの実装テクニックを紹介。今回はウィンドウ管理とメニューの実装方法を基礎から説明する。

尾崎 義尚
  • このエントリーをはてなブックマークに追加

はじめに

 前回はElectronのアーキテクチャと基本機能を紹介した。今回から次回と次々回は、Electronのデモアプリで紹介されている機能を使って、Electronアプリの実装のテクニックを紹介していく。今回はウィンドウ管理とメニューの実装テクニックを説明する。

APIデモアプリ

 GitHub上のElectron公式アカウントで公開されているElectron API Demosというデモアプリは、前回説明した各種APIを使って具体的にどんなことができるのかを知るのに便利だ。よく使うクラスと使い方が、デモの中で解説されているため、これを見ていけばElectronのAPIで何ができるのかが効率的に理解できる。

 そこで本稿では、このデモアプリの内容を順に解説していこう。なお、本稿ではデモのコードをそのまま転記するのではなく、そのままコードを実行できるように若干変更している。

 まずは、GitHubからElectron API Demosのソースを取得して、デモアプリを実行する(リスト1)。

Bash
$ git clone https://github.com/electron/electron-api-demos
$ cd electron-api-demos/
$ npm install
$ npm start
リスト1 Electron API DemosのソースをGitHubから取得して、デモアプリを起動

git cloneコマンドでAPI Demosをローカルにクローンする。
npm installコマンドで実行に必要なモジュールをインストールする。
npm startコマンドでアプリケーションを起動する

 リスト1のnpm startコマンドでデモアプリ(図1)が起動する。

図1 Electron API Demosの実行例

左に機能別のメニューがあり、右に説明が表示される。[View Demo]ボタンをクリックすると、実際にデモを見ることができる。

 デモアプリは、

  • 説明
  • デモがあるときは[View Demo]ボタン
  • サンプルコード

で構成されている。[View Demo]ボタンをクリックすると、コードが実行されて、そのUIを操作したりできる。

 【メニュー項目の一覧】は以下のようになっている。

 それでは、メニューの各項目を順に見ていこう。

ウィンドウ管理(WINDOWS)

 [WINDOWS]は、OSのWindowsのことではなく、アプリのウィンドウのことだ。つまりここでは、ウィンドウを作成・管理するためのサンプルが提供されている。

▲メニュー項目の一覧に戻る

ウィンドウの作成と管理(Create and Manage windows)
新しいウィンドウの作成(Create a new window)

 Electronのウィンドウは、BrowserWindowクラスで管理する。

 ●メインプロセスからのBrowserWindowクラスへのアクセス

 最も基本的な方法となるが、メインプロセス(=package.jsonでmainに指定されたJavaScriptソース)では、リスト2の記述でBrowserWindowクラスを直接呼び出せる。

JavaScript
const BrowserWindow = require('electron').BrowserWindow
リスト2 メインプロセスのBrowserWindowの宣言(index.js)

 ●レンダラープロセスからのBrowserWindowクラスへのアクセス

 一方、レンダラープロセスでは、BrowserWindowクラスに直接アクセスできない(つまり、レンダラープロセスからは新しいウィンドウを作成できない)。そのため、remoteモジュール日本語訳)を経由して、メインプロセスのBrowserWindowモジュールを取得する必要がある(リスト3)。なおremoteモジュールは、IPC(Inter-process Communication:プロセス間通信)呼び出しをラップして、レンダラープロセスからメインプロセスのオブジェクトを呼び出す簡単な方法を提供している。

JavaScript
const BrowserWindow = require('electron').remote.BrowserWindow
リスト3 レンダラープロセスのBrowserWindowの宣言(renderer-process/windows/create-window.js)

 このようにメインプロセスを経由することで、新しいレンダラーウィンドウを表示できる。デモアプリでも、メインプロセスからではなく、ウィンドウ(レンダラープロセス)から別のウィンドウを作成しているため、remoteモジュールを経由してメインプロセスのBrowserWindowモジュールにアクセスしている。

 ●メインプロセスからウィンドウを表示する基本コード

 デモのコードとは少し違うが、メインプロセスからウィンドウを表示する最も基本的なコードを紹介する(リスト4)。

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

app.on('ready', function() {
  mainWindow = new BrowserWindow({ width: 300, height: 200 });
  mainWindow.loadURL(`file://${__dirname}/index.html`);
  mainWindow.on('closed', () => {
    mainWindow = null;
  });
});
リスト4 メインプロセスからのウィンドウ表示(index.js)

先頭に必要なモジュールを宣言している。
appモジュールのreadyイベントで、ウィンドウ(BrowserWindow)を作成して、表示している。
今後、メインプロセスが「ウィンドウの表示のみ」の場合は、このコードを利用してほしい。

 appモジュールのreadyイベント*1は、Electronの初期化が完了したときに発生する。Electronの処理は、ready後に記述する必要がある。

 BrowserWindowクラスのコンストラクターでウィンドウサイズを指定して、loadURLメソッドで読み込むHTMLファイルを指定するとウィンドウが表示される。

 ●レンダラープロセスからウィンドウを表示する基本コード

 これについては、次に説明するリスト5~7のコードを参考にしてほしい。

ウィンドウ状態の管理とフォーカス(Manage window state / Window events: blur and focus)

 ウィンドウの状態管理とフォーカスは、どちらもウィンドウのイベントを処理しているデモなので、一緒に解説する。

 ここでは、デモを参考に以下のファイル群を作成することにした。

  • index.js: メインプロセスにおける処理。前掲のリスト4
  • index.html: 親ウィンドウのHTMLソース。レンダラープロセス。以下のリスト5に掲載
  • childwindow.html: 子ウィンドウのHTMLソース。レンダラープロセス。以下のリスト6に掲載
  • renderer-process.js: 親ウィンドウ(レンダラープロセス)における処理。以下のリスト7に掲載
HTML
<html>
<style>
  .smooth-appear {
    opacity: 1;
    transition: opacity .5s ease-in-out;
  }
  
  .disappear {
    opacity: 0;
  }
</style>
<body>
  <button id="manage-window">Open New Window</button>
  <div id="manage-window-reply"></div>
  <button id="focus-on-modal-window" class="disappear">Focus</button>
  <script>
    // renderer-process.jsファイル(リスト7)をモジュールとして読み込む
    require('./renderer-process')
  </script>
</body>
<html>
リスト5 親ウィンドウのHTMLソース(index.html)
HTML
<style>
body {
  padding: 10px;
  font-family: system, -apple-system, '.SFNSText-Regular', 'SF UI Text', 'Lucida Grande', 'Segoe UI', Ubuntu, Cantarell, sans-serif;
  color: #fff;
  background-color: #8aba87;
  text-align: center;
  font-size: 34px;
}
#close {
  color: white;
  opacity: 0.7;
  position: absolute;
  bottom: 20px;
  left: 50%;
  transform: translateX(-50%);
  font-size: 12px;
  text-decoration: none;
}
</style>
<p>Click on the parent window to see how the "focus on demo" button appears.</p>
<a id="close" href="javascript:window.close()">Close this Window</a>
リスト6 子ウィンドウのHTMLソース(childwindow.html)
JavaScript
const BrowserWindow = require('electron').remote.BrowserWindow
const path = require('path')

const manageWindowBtn = document.getElementById('manage-window')
let win

// ボタンクリック時に、レンダラープロセスから別ウィンドウを表示
manageWindowBtn.addEventListener('click', function(event) {
  const modalPath = path.join('file://', __dirname, '/childwindow.html')
  win = new BrowserWindow({
    width: 400,
    height: 275
  })
  win.on('resize', updateReply)
  win.on('move', updateReply)
  win.on('focus', hideFocusBtn)
  win.on('blur', showFocusBtn)
  win.on('close', function() {
    hideFocusBtn()
    win = null
  })
  win.loadURL(modalPath)
  win.show()

  function updateReply() {
    const manageWindowReply = document.getElementById('manage-window-reply')
    const message = `Size: ${win.getSize()} Position: ${win.getPosition()}`
    manageWindowReply.innerText = message
  }

  const focusModalBtn = document.getElementById('focus-on-modal-window')

  function showFocusBtn(btn) {
    if (!win) return
    focusModalBtn.classList.add('smooth-appear')
    focusModalBtn.classList.remove('disappear')
    focusModalBtn.addEventListener('click', function() {
      win.focus()
    })
  }

  function hideFocusBtn() {
    focusModalBtn.classList.add('disappear')
    focusModalBtn.classList.remove('smooth-appear')
  }
})
リスト7 親ウィンドウ(レンダラープロセス)における処理(renderer-process.js)

ウィンドウのサイズ変更(resize)、移動(move)、フォーカス(focus)、フォーカスが外れる(blur)が処理されている。

 特に難しいところはないと思うが、念のため、上記のコードを理解するためのポイントを確認しておこう。

JavaScript
win.on('resize', updateReply)
win.on('move', updateReply)

function updateReply() {
  const manageWindowReply = document.getElementById('manage-window-reply')
  const message = `Size: ${win.getSize()} Position: ${win.getPosition()}`
  manageWindowReply.innerText = message
}
リスト8 renderer-process.jsファイルから一部抜粋

 リスト8を見ると、ウィンドウのサイズ変更(resizeイベント*2)、または移動(moveイベント*2)が発生すると、updateReply関数が呼び出されるようになっている。updateReplay関数では、ウィンドウのサイズと位置を取得して、ページ上にテキスト表示している。

フレームなしウィンドウ(Create a frameless window)

 タイトルバーやコマンドボタンなどのフレームなしウィンドウを作成するには、BrowserWindowのコンストラクターでframeにfalseを指定する。

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

app.on('ready', function() {
  mainWindow = new BrowserWindow({ frame: false });
  mainWindow.loadURL(`file://${__dirname}/index.html`);
  mainWindow.on('closed', () => {
    mainWindow = null;
  });
});
リスト9 フレームなしのウィンドウを表示する(index.js)

BrowserWindowクラスのコンストラクターでframe: falseを指定することでフレームなしのウィンドウを表示する。

 HTMLソースには特に処理を記述していないため、省略する。このコードを実行すると、タイトルバーもコントロールボタンもないウィンドウが表示される(図2)。

図2 フレームなしウィンドウ

タイトルバーもコントロールボタンもないウィンドウが表示される。

 ウィンドウのオプションとしてフレームだけでなく、表示位置やサイズなどさまざまなオプションが指定できる。詳細はBrowserWindowAPIリファレンスを参照してほしい。

▲メニュー項目の一覧に戻る

ハングとクラッシュ(Handling window crashes and hangs)

 レンダラープロセスがハングしたり、クラッシュしたりしたときに、メインプロセスから検出できる。ハング時とクラッシュ時のイベントハンドラーを記述したコードを見ていこう。

JavaScript
const {app, BrowserWindow} = require('electron');
const dialog = require('electron').dialog

const path = require('path')

app.on('ready', function() {
  const hangWinPath = path.join('file://', __dirname, 'index.html')
  let win = new BrowserWindow({ width: 400, height: 320 })

  win.on('unresponsive', function () {
    const options = {
      type: 'info',
      title: 'レンダラープロセスがハングしています',
      message: 'このプロセスはハングしています。コマンド?',
      buttons: ['再読み込み', '閉じる']
    }
    dialog.showMessageBox(options, function (index) {
      if (index === 0) win.reload()
      else win.close()
    })
  })

  win.webContents.on('crashed', function () {
    const options = {
      type: 'info',
      title: 'レンダラープロセスがクラッシュしました',
      message: 'このプロセスはクラッシュしました。コマンド?',
      buttons: ['再読込', '閉じる']
    }
    dialog.showMessageBox(options, function (index) {
      if (index === 0) win.reload()
      else win.close()
    })
  })

  win.on('close', function () { win = null })
  win.loadURL(hangWinPath)
  win.show()
})
リスト10 メインプロセスのコード(index.js)

ウィンドウ(レンダラープロセス)がハングしたときのunresponsiveイベントと、クラッシュ時のcrashedイベント*3の、イベントハンドラーが記述されている。

  • *3 unresponsiveイベントはappオブジェクトに含まれているが、crashedイベントは、webContentsプロパティにより取得できるwebContentsオブジェクト(=Webページを描画・制御するためのもの)に含まれている。webContentsオブジェクトのイベントについては「Electron Documentation / API / webContents / Instance Events」(英語)を参照してほしい。

 デモアプリでは、レンダラープロセスからさらにウィンドウを表示しているため、remoteモジュールを経由してウィンドウを作成しているが、リスト10ではメインプロセスから呼び出すため、直接、ウィンドウを表示している。

 ウィンドウがハング状態になるとunresponsiveイベントが発生するため、win.on('unresponsive')をハンドルして、クラッシュ時の処理を記述している(リスト11)。

JavaScript
  win.on('unresponsive', function () {
    const options = {
      type: 'info',
      title: 'レンダラープロセスがハングしています',
      message: 'このプロセスはハングしています。コマンド?',
      buttons: ['再読み込み', '閉じる']
    }
    dialog.showMessageBox(options, function (index) {
      if (index === 0) win.reload()
      else win.close()
    })
  })
リスト11 クラッシュ時の処理部分を抜粋

optionsに、「メッセージダイアログのタイトル」と「メッセージ」、「表示するボタンの文字列」を設定して、dialog.showMessageBoxメソッドでダイアログを表示している。コールバック関数に、クリックされたボタンのindexが返されるため(インデックス番号は0スタートで、[再読み込み]ボタン=0、[閉じる]ボタン=1になる)、[再読み込み]ボタンがクリックされたら(つまりindex === 0のとき)、win.reload()でウィンドウを再読み込みしている。

 このように、ハング状態をunresponsiveイベントとして検出して、ウィンドウの再読み込みやアプリの終了などの対応が可能だ。

 また同様に、クラッシュ時はcrashedイベントで検出できるため、同じ方法で処理を記述している。

 レンダラープロセス(index.html)側では、リンクがクリックされたら、プロセスをクラッシュあるいはハングさせている。

HTML
<!DOCTYPE html>
<html>
<head>
<style>
  body {
    padding: 14px;
    font-family: system, -apple-system, '.SFNSText-Regular', 'SF UI Text', 'Lucida Grande', 'Segoe UI', Ubuntu, Cantarell, sans-serif;
    color: #fff;
    background-color: #8aba87;
    text-align: center;
    font-size: 40px;
  }

  #crash, #hang {
    color: white;
    left: 50%;
    text-decoration: none;
  }
</style>
  <title>Hello Electron</title>
  <meta charset="UTF-8">
</head>
<body>
  <p><a id="crash" href="javascript:process.crash()">クラッシュ</a></p>
  <p><a id="hang" href="javascript:process.hang()">ハング</a></p>
</body>
</html>
リスト12 process.crash()でプロセスをクラッシュさせており、process.hang()でプロセスをハングさせている(index.html)

 プログラムを起動して[ハング]リンクをクリックした後、しばらく(30秒ほど)待つとダイアログが表示される(図3)。[再読み込み]ボタンをクリックすると、ウィンドウが再読み込みされる。

図3 [ハング]リンクをクリックすると、プロセスがハングして、ダイアログが表示される

 同じように[クラッシュ]リンクをクリックすると、プロセスがクラッシュして、ダイアログが表示される(図4)。

図4 [クラッシュ]リンクをクリックすると、プロセスがクラッシュして、ダイアログが表示される

 このようにレンダラープロセスのハングやクラッシュを検出することで、アプリが落ちてしまうような致命的な問題に対して適切な処理を記述できる。

 メニューのデモとしては、アプリケーションメニュー(=メニューバー)とコンテキストメニューの2つが紹介されている。

▲メニュー項目の一覧に戻る

アプリケーションメニュー(Create an application menu)

 このデモでは、メニューバーにカスタム項目を追加するコード(リスト13)が紹介されている。

JavaScript
const electron = require('electron')
const path = require('path')
const BrowserWindow = electron.BrowserWindow
const Menu = electron.Menu
const app = electron.app


let template = [{
  label: '編集',
  submenu: [{
    label: 'やり直し',
    accelerator: 'Shift+CmdOrCtrl+Z',
    role: 'redo'
  }, {
    type: 'separator'
  }, {
    label: 'コピー',
    accelerator: 'CmdOrCtrl+C',
    role: 'copy'
  }]
}, {
  label: '表示',
  submenu: [{
    label: '全画面表示切り替える',
    accelerator: (function() {
      if (process.platform === 'darwin') {
        return 'Ctrl+Command+F'
      } else {
        return 'F11'
      }
    })(),
    click: function(item, focusedWindow) {
      if (focusedWindow) {
        focusedWindow.setFullScreen(!focusedWindow.isFullScreen())
      }
    }
  }]
}]

if (process.platform === 'darwin') {
  const name = electron.app.getName()
  template.unshift({
    label: name,
    submenu: [{
      label: `About ${name}`,
      role: 'about'
    }, {
      label: 'Quit',
      accelerator: 'Command+Q',
      click: function() {
        app.quit()
      }
    }]
  })
}

app.on('ready', function() {
  const menu = Menu.buildFromTemplate(template)
  Menu.setApplicationMenu(menu)

  const winPath = path.join('file://', __dirname, 'index.html')
  let win = new BrowserWindow({ width: 400, height: 320 })
  win.on('close', function() { win = null })
  win.loadURL(winPath)
  win.show()
})
リスト13 アプリケーションメニューを表示するコード(index.js)

コードの内容は後述する。

 デモアプリはメニュー項目の数が多いので、リスト13ではある程度省略した。このコードを実行すると、ウィンドウに対するメニューが表示される(図5)。

図5 Windowsでのメニュー表示
図5 macOSでのメニュー表示
図5 各プラットフォームでのメニュー表示

上: Windowsでは、メニューバーが表示される。
中: macOSでは、アプリケーションメニューが追加される。
下: Linux(Ubuntu)では、メニューバーが表示される。

 では、リスト13のコードをブロックごとに見ていこう。

 まずtemplateという変数(=テンプレート)にメニューの定義を記述している(リスト14)。

JavaScript
let template = [{
  label: '編集',
  submenu: [{
    label: 'やり直し',
    accelerator: 'Shift+CmdOrCtrl+Z',
    role: 'redo'
  }, {
    type: 'separator'
  }, {
    label: 'コピー',
    accelerator: 'CmdOrCtrl+C',
    role: 'copy'
  }]
}]
リスト14 メニューの定義

 そのテンプレートからメニュー(=Menuオブジェクト)を構築するわけだが、そのメニューを構成する各メニュー項目を表すMenuItemオブジェクトには、以下のプロパティを設定できる。全ての項目がOptional(省略可能)になっており、何も設定しなくても(もちろん何もしない項目になるが)項目を表示できる。

プロパティ 説明
label メニューに表示するラベルを指定する。指定がない場合、ロール(後述)のデフォルト文字列を表示するか、何も表示しない
sublabel WindowsとLinux環境でメニュー項目の下に説明文を表示する
type メニューに表示する項目の種類を指定する。
  • normal: (デフォルト)通常のメニュー項目として表示する
  • separator: 境界線
  • submenu: 下階層のメニュー
  • checkbox: オン/オフを固定化できるメニュー項目
  • radio: グループ内で単一の値を選択できるメニュー項目
role ロール(=項目のアクション)を設定する。設定されているときにはclickプロパティは無視される。設定可能な値は後述する
accelerator メニューのショートカットキーを設定する(例:CommandOrControl+C、または省略形のCmdOrCtrl+C)。コマンドキーとしては以下が指定可能だが、macOS/Windows/Linuxで利用できるように、基本的にCmdOrCtrlの使用をお勧めする。
  • CommandまたはCmd(macOSのみ)
  • ControlまたはCtrl(WindowsやLinux)
  • CmmandOrControlまたはCmdOrCtrl
また、以下のような修飾キーも使用可能だ。
  • Alt
  • Option(macOSにしかないため、Altキーを使うことが推奨される)
  • Shift
  • Super(WindowsやLinuxではWindowsキー、macOSではCmdキー)
それ以外のキーについては公式リファレンス(英語)を参照されたい
icon メニュー横に表示するアイコン画像を指定する。サイズが指定できないので、大きくない画像を用意する必要がある
enabled メニュー項目の有効(true)または無効(false)を指定する
visible メニュー項目の表示(true)または非表示(false)を指定する
checked typeプロパティにcheckboxまたはradioを指定したときに、チェックされている状態(true)か、チェックされていない状態(false)かを指定する
submenu 下階層のメニューを定義する
id メニューを識別するIDを文字列で指定する
position メニューの表示位置を指定する。idプロパティで指定された項目の挿入位置を指定する(例:'before=id-string')。詳細後述。
  • before: 指定したID項目の前に挿入する
  • after: 指定したID項目の後に挿入する
  • endof: 論理グループの最後に挿入する
click メニューがクリックされたときに実行されるfunctionを記述する
表1 設定可能な、MenuItemクラスのプロパティ
ロール(role)

 roleとして指定可能な値は以下である。

説明
undo やり直し
redo 繰り返し
cut 切り取り
copy コピー
paste 貼り付け
pasteandmatchstyle 元のスタイルを保持して貼り付け
selectall 全て選択
delete 削除
minimize 最小化
close ウィンドウを閉じる
quit アプリを終了
reload 再読み込み
toggledevtools 開発者ツール(DevTools)の表示/非表示
togglefullscreen 全画面表示の切り替え
resetzoom ズームのリセット
zoomin 拡大
zoomout 縮小
表2 ロール(roleプロパティ)に設定可能な値

 以下の値はmacOSのみで利用可能なroleである。

説明
about アプリについて
hide 非表示
hideothers 他を非表示
unhide 再表示
startspeaking 読み上げ開始
stopspeaking 読み上げ停止
front 全面に切り替え
zoom ウィンドウを全画面表示
window サブメニューのウィンドウ
help サブメニューのヘルプ
services サブメニューのサービス
表3 macOSのみで、ロール(roleプロパティ)に設定可能な値

 メニュー項目では、前掲のリスト14のようにロールでOS標準の機能を利用することもできるし、下記のリスト15のようにclickイベントにコードを記述するなどしたカスタム処理の実装も可能だ。

JavaScript
let template = [{
  label: '全画面表示切り替える',
  accelerator: (function() {
    if (process.platform === 'darwin') {
      return 'Ctrl+Command+F'
    } else {
      return 'F11'
    }
  })(),
  click: function(item, focusedWindow) {
    if (focusedWindow) {
      focusedWindow.setFullScreen(!focusedWindow.isFullScreen())
    }
  }]
}]
リスト15 「プラットフォームごとにショートカットキーの切り替え」「clickイベントにコードを記述」といったカスタム処理が実装されている

 メニューの項目定義も実装もJavaScriptだけでできることがお分かりいただけただろう。

ポジション(position)

 メニューの表示位置としてpositionプロパティを指定できることを表1に示したが、少し分かりにくいので、コードと実行例を示しておこう。positionプロパティにendofを記述すると、値ごとに論理グループが形成されて、記載した順に並べられて表示される。例えばリスト16では、editwindowhelpの3つの論理グループを記述している。

JavaScript
let template = [{
  label: '編集',
  submenu: [
    { label: 'コピー', position: 'endof=edit' },
    { label: '並べる', position: 'endof=window' },
    { label: 'アプリについて', position: 'endof=help' },
    { label: '更新', position: 'endof=help' },
    { label: '貼り付け', position: 'endof=edit' },
    { label: '選択', position: 'endof=edit' }
  ]
}]
リスト16 endofを指定したメニューオブジェクトの定義

positionプロパティにendofで論理グループを指定している。

 このコードの実行結果を見てみよう。

図6 論理グループごとに分かれてメニューが表示される(Windows)
図6 論理グループごとに分かれてメニューが表示される(macOS)
図6 論理グループごとに分かれてメニューが表示される(上:Windows、下:macOS)

▲メニュー項目の一覧に戻る

コンテキストメニュー(Create a context menu)

 アプリのウィンドウを右クリックして表示されるコンテキストメニューも、アプリケーションメニューと同じくMenuクラスで作成できる(リスト17)。

JavaScript
const electron = require('electron')
const BrowserWindow = electron.BrowserWindow
const Menu = electron.Menu
const MenuItem = electron.MenuItem
const app = electron.app

const menu = new Menu()
menu.append(new MenuItem({ label: 'Hello' }))
menu.append(new MenuItem({ type: 'separator' }))
menu.append(new MenuItem({ label: 'Electron', type: 'checkbox', checked: true }))

app.on('browser-window-created', function(event, win) {
  win.webContents.on('context-menu', function(e, params) {
    menu.popup(win, params.x, params.y)
  })
})

app.on('ready', function() {
  mainWindow = new BrowserWindow({ width: 400, height: 300 });
  mainWindow.loadURL(`file://${__dirname}/index.html`);
  mainWindow.on('closed', () => {
    mainWindow = null;
  });
});
リスト17 コンテキストメニューを表示するコード(index.js)

MenuクラスのインスタンスにappendメソッドでMenuItemオブジェクトを追加する。
appオブジェクトのbrowser-window-createdイベントが発行されたタイミングで(つまりウィンドウが作成されたとき)、コンテキストメニュー(=webContentsオブジェクトのcontext-menuイベント)を登録している。
開いたウィンドウのcontext-menuイベントが発生したタイミングで、menu.popupメソッドを呼び出して、コンテキストメニューをポップアップ表示している。

 レンダラープロセス(index.html)側の処理は、リスト18のようにしよう。

HTML
<html>
<body>
  Context Menu
</body>
</html>
リスト18 HTML側には特に何も記述しない(index.html)

 これを実行して、ウィンドウで右クリックするとコンテキストメニューが表示される(図7)。

図7 表示されたウィンドウで右クリックするとコンテキストメニューが表示される

 コンテキストメニューなので、状態(=コンテキスト)に応じて表示内容を切り替えたい場合は、IPCで送信されたレンダラープロセス内からのメッセージを、メインプロセスのipcMainモジュールでチャネルごとに受信して、各チャネルに応じたメニューをポップアップさせるとよい(IPCについては次回詳しく説明する)。

 メインプロセスでは具体的に、show-context-menu1show-context-menu1という2つのチャネル(=イベント名)を定義して、それぞれのイベントハンドラー内で異なるコンテキストメニューを表示する(リスト19)。

JavaScript
const electron = require('electron')
const BrowserWindow = electron.BrowserWindow
const Menu = electron.Menu
const MenuItem = electron.MenuItem
const app = electron.app
const ipc = electron.ipcMain

ipc.on('show-context-menu1', function(event) {
  const menu1 = new Menu()
  menu1.append(new MenuItem({ label: 'Context Menu 1' }))
  const win = BrowserWindow.fromWebContents(event.sender)
  menu1.popup(win)
})

ipc.on('show-context-menu2', function(event) {
  const menu2 = new Menu()
  menu2.append(new MenuItem({ label: 'Context Menu 2' }))
  const win = BrowserWindow.fromWebContents(event.sender)
  menu2.popup(win)
})

app.on('ready', function() {
  mainWindow = new BrowserWindow({ width: 400, height: 300 });
  mainWindow.loadURL(`file://${__dirname}/index.html`);
  mainWindow.on('closed', () => {
    mainWindow = null;
  });
});
リスト19 メインプロセス側では、ipcMainモジュールでコンテキストに応じたチャネルを用意しておき、対応したメニューを表示するようにしておく(index.js)

 レンダラープロセスでは、状態に応じたチャネル(=先ほど定義したshow-context-menu1show-context-menu1)にIPC(=ipcRendererモジュール)でメッセージ送信するように記述する(リスト20)。

HTML
<html>
<body>
  <button id="context-button1">Button1</button>
  <button id="context-button2">Button2</button>
</body>
<script>
  const ipc = require('electron').ipcRenderer
  const contextMenuBtn1 = document.getElementById('context-button1')
  contextMenuBtn1.addEventListener('click', function() {
    ipc.send('show-context-menu1')
  })
  const contextMenuBtn2 = document.getElementById('context-button2')
  contextMenuBtn2.addEventListener('click', function() {
    ipc.send('show-context-menu2')
  })
</script>
</html>
リスト20 レンダラープロセスからメインプロセスにIPCでメッセージを送信(index.html)

ここではボタンのクリックイベントで送信するチャネルを切り替えている。

 このように状態によってIPCチャネルを切り替えることで、コンテキストメニューを切り替えている。

▲メニュー項目の一覧に戻る

グローバルショートカット(Register keyboard shortcuts)

 グローバルショートカットは、アプリがアクティブでない状態でも利用できるショートカットキーである。さっそくコードを見てみよう。

JavaScript
const electron = require('electron')
const app = electron.app
const dialog = electron.dialog
const globalShortcut = electron.globalShortcut

app.on('ready', function() {
  globalShortcut.register('CommandOrControl+Alt+K', function() {
    dialog.showMessageBox({
      type: 'info',
      message: 'Success!',
      detail: 'You pressed the registered global shortcut keybinding.',
      buttons: ['OK']
    })
  })
})

app.on('will-quit', function() {
  globalShortcut.unregisterAll()
})
リスト21 グローバルショートカットキーの登録

globalShortcut.registerメソッドで、ショートカットキーを登録している。

 このサンプルは、UIを持たないアプリのため、HTMLソースはない。アプリ起動時(=readyイベント)にglobalShortcut.registerメソッドを使って、CommandContrlAltKキーを同時に押すとメッセージボックスを表示するショートカットキーを登録している。また、アプリの終了時(=will-quitイベント)で globalShortcut.unregisterAllメソッドを呼び出して、登録したショートカットキーを解除している。

 アプリを起動してもウィンドウが表示されないため、アプリがアクティブになることはない。何がアクティブになっていてもよいので、登録したショートカットキーを押してみると、メッセージボックスが表示される(図8)。

図8 登録したショートカットキーを押すとメッセージボックスが表示される
図8 登録したショートカットキーを押すとメッセージボックスが表示される

 システム全体で使うようなアプリに使うとよいだろう。

▲メニュー項目の一覧に戻る

まとめ

 今回は、Electron API Demosのウィンドウ管理機能とメニューについて解説した。次回は、OSのネイティブ機能の呼び出しや、IPCを使ったアプリ内のプロセス間通信を解説する。

1. Electronとは? アーキテクチャ/API/インストール方法/初期設定

Windows/macOS/Linuxで実行できるデスクトップアプリをWeb技術で作ろう! Electronの概要から開発を始めて動かすところまでを解説する。

2. 【現在、表示中】≫ Electron APIデモから学ぶ実装テクニック ― ウィンドウ管理とメニュー

Electron API Demosで紹介されている、Electronアプリの実装テクニックを紹介。今回はウィンドウ管理とメニューの実装方法を基礎から説明する。

3. Electron APIデモから学ぶ実装テクニック ― ネイティブUIと通信

Electron API Demosで紹介されている、Electronアプリの実装テクニックを紹介。今回はネイティブUIと通信の実装方法を基礎から説明する。

4. Electron APIデモから学ぶ実装テクニック ― システムとメディア

Electron API Demosで紹介されている、Electronアプリの実装テクニックを紹介。今回はシステムとメディアの実装方法を基礎から説明する。

5. Electronアプリのデバッグと、パッケージ化

本格的にElectronアプリ開発を進める方に向けて、そのデバッグ方法と、製品リリースのためのパッケージ作成の方法について説明する。

イベント情報(メディアスポンサーです)

Twitterでつぶやこう!


Build Insider賛同企業・団体

Build Insiderは、以下の企業・団体の支援を受けて活動しています(募集概要)。

ゴールドレベル

  • グレープシティ株式会社
  • 日本マイクロソフト株式会社