Electronでアプリを作っていると、意外なところでネイティブ的な挙動をさせるために工夫しなければいけないポイントがあります。この記事ではマルチウインドウなアプリで自然なメニューを実現するための設計方法について、代表的なElectronアプリであるAtomの実装を例に説明したいと思います。
実際の設計を見て行く前に、考慮すべきElectron特有の仕様や制約について見ていきます。
Electronに関する前提知識
Menuクラスの基本的な使い方
Electronではプラットフォームをまたいでメニュー機能を実現するために、Menuというクラスが提供されています。
ElectronはOS X, Windows, Linuxをサポートしていますが(参考: Supported Platforms)、 OS XだけはWindows, Linuxとメニューの考え方が異なります。Windows, Linuxは1つのアプリケーションが複数のウインドウを持ち, その各ウィンドウが独立したメニューを持っています。
一方、OS Xでも複数のウインドウは存在するのですが、各ウィンドウはメニューを持たずアプリケーション全体で1つのメニューを持っています。
Electronはこのそれぞれの考え方に対して別のメニュー設定APIを持っています。
- BrowserWindow#setMenu: 各ウィンドウにメニューを設定する。OS Xでは動作しない。
- Menu#setApplicationMenu: アプリケーション全体のメニューを設定する。Windows, Linuxでは全ウィンドウのメニューがすべて設定される。
BrowserプロセスとRendererプロセス
Electronではアプリ全体で1つのBrowserプロセス(Mainプロセスとも呼ばれます)と, 各ウィンドウに対応するRendererプロセスが存在します。
各プロセス間ではメモリが共有されないため、Electronが提供するipc(inter-process communication)の仕組みを使って通信を行います。Browserプロセス用にはipcMainモジュール、Rendererプロセス用にはipcRendererモジュールが用意されています。
注意すべき点として、MenuクラスはBrowserプロセス内でしか利用できません。そのため、RendererプロセスからMenuを利用するには、ipc経由でBrowserプロセスへメッセージを送るか、もしくはremoteモジュールを利用する必要があります。
Atomのメニュー設計
ここからはAtomのソースコードを実際に見ていきます(上記Electronの例とは異なり、AtomのソースコードはCoffeeScriptで記述されています)。
Atomは複数のウィンドウを持ち、その各ウィンドウがマルチタブなテキストエディタになります。各ウィンドウは独立したメニューを持っています。
前述した通り、マルチウィンドウなアプリケーションのメニュー管理はプラットフォームにより挙動が異なるため、Atomではこれらを統一的に扱うためのクラス設計を行っています。
ウィンドウ管理
Atomでは、アプリケーション全体で1つのBrowserプロセス、各ウィンドウごとに1つのRendererプロセスが起動されます。Atomのクラス構造も、Browserプロセス内で使用されるものとRendererプロセス内で使用されるものが分かれています。
以下に、メニュー管理に関係するクラスの構造をまとめた図を示します。
まずBrowserプロセス内では、AtomApplicationクラスがAtomアプリケーション全体を司る根本のクラスになります。そして個々のウィンドウごとに起動されるRendererプロセスの一つ一つを抽象化したものがAtomWindowクラスです。BrowserプロセスはAtomWindowクラスから上述したipcモジュールを通じて各Rendererプロセスとやり取りを行います。
実際にアプリケーションのメニューを管理するのはApplicationMenuクラスです。このクラスは各AtomWindowに設定されたメニュー内容を保持し、現在フォーカス中のウィンドウに合わせてメニュー内容を更新したり、ユーザによってメニュー要素が選択された際にイベントを各ウィンドウに通知したりします。ApplicationMenuクラスは内部的にElectronの提供するMenuクラスを使用しています。
Rendererプロセス内ではAtomEnvironmentが各Rendererプロセスの全体を管理する根本のクラスになります。RendererプロセスはAtomEnvironmentクラスからipcモジュールを通じてメニューの更新を要求したり、逆にメニュー選択時のイベントを受け取ったりします。
メニュー設定
Atomでは各ウィンドウのメニュー内容は各Rendererプロセスが管理します。以下にメニュー更新時の処理の流れをまとめた図を示します。
Rendererプロセスは、設定したいメニュー内容をMenuManagerクラス(上図左下)に渡します。
MenuManagerクラスはIPC経由でそのメニュー内容をBrowserプロセスに伝達し、BrowserプロセスはApplicationMenu経由でメニューを更新します(上図ではクラスの管理される構造を示すためにAtomEnvironment, AtomWindowなどを経由してメニュー内容が伝達されるように記載していますが、実際にはいくつか間を飛ばしてデータのやり取りが行われます。詳しくは実際のソースコードを参照してください)。
実際にメニュー内容としてMenuManagerに渡されるデータは以下の様なオブジェクトになります。
{"menu": [
{
label: "File",
submenu: [{
label: "Open",
command: "applicaiton:open"
}, ...
]
}, ...
]}
labelが各メニュー要素に表示される文字列、commandがそのメニュー要素が選択された際に実行されるアクションを表しています。これはElectronのAPIで提供されているMenuItemクラスとほぼ同じ構造になります。実際、ApplicationMenuまでこのオブジェクトが渡された際に、これらのオブジェクトはMenuItemインスタンスの配列に変換されてElectronの提供するMenuクラスに渡されます。
MenuItemインスタンスはclickというプロパティにそのメニュー要素が選択された際に実行されるコールバック処理を保持します。ApplicationMenuクラスはcommandに記載されたアクションが実行されるようにコールバック処理を生成します。
一見回りくどいこの処理は、アプリケーションがBrowserプロセスとRendererプロセスに分かれていることに起因しています。AtomではRendererプロセスが各ウィンドウで表示したいメニュー内容を管理・生成します。しかし上述したようにElectronではMenuクラスへアクセスできるのはBrowserプロセスのみになっており、RendererプロセスはIPC経由でBrowserプロセスへメニュー表示内容を渡し、Browserプロセスがメニューを更新する必要があります。この際、IPC経由で送られるデータは内部的にJSON形式にエンコードされるため、コールバック関数を送ることはできません。この制約のもとでメニュー選択時の処理内容をRendererプロセス側から指示するために、Atomでは各処理をcommandという文字列で表し、それをメニュー内容に含めています。実際にcommand文字列がどう処理されるかは次節で説明します。
Atomでは実際に表示されるメニュー内容をCSON形式のファイルにして外部化しています。こちらに各プラットフォーム用のCSONファイルがありますので、参考に見てみるとよりイメージが湧くと思います。
プラットフォームごとに別のファイルが用意されているのは、OS XのデザインガイドラインやWindowsのデザインガイドラインに示されるように、各プラットフォームでメニューに要求される要素が微妙に異なるためです。
メニュー選択時のイベント処理
前節で述べたように、メニュー選択時のアクションはcommand文字列で指定されます。
command文字列が実際に各ウィンドウの処理として実行されるまでの流れをまとめた図を以下に示します。
ユーザがメニュー要素を選択すると、command文字列がAtomApplicationへ通知されます。
AtomApplicationはそのcommand文字列が「新しいウィンドウを開く」などの自身が処理できる内容であれば実行し、それ以外であればフォーカス中のAtomWindowインスタンスへ転送します。AtomWindowインスタンスは「ウィンドウを閉じる」などの自身が処理できる内容であれば実行し、それ以外であればIPC経由でRendererプロセスに転送します。Rendererプロセス内ではCommandRegistryクラスやWindowEventHandlerクラスがcommand文字列に応じて実行する処理を保持しており、必要に応じて実行されます。
このように、実際にメニュー要素が選択された際は、BrowserプロセスからRendererプロセスに向かっていくつかのクラスの間でcommand文字列が転送され、必要な処理が実行されます。
まとめ
自分でElectronアプリを作成した際に、デフォルトのAPIがプラットフォームの違いをいい感じに吸収してくれてるんだろう、とか思っていたら以外と設計に苦戦した経緯があり、その際に調べた内容を備忘録的に書いてみました。
まとめると、Electronアプリでメニュー表示をしっかり実装するためには以下の課題がありました。
- OS XとWindows, Linuxでのメニュー設計の違い
- Rendererプロセス主導でメニュー管理を行うためのIPC利用を考慮に入れた設計
AtomではBrowserプロセス側で各Rendererプロセスを抽象化するクラスを用意し、メニュー選択イベントの処理内容をcommandという形で文字列として表現することで上記を解決していました。