この文書では、Mozilla の chrome コード (XUL アプリケーションや 拡張機能) の中で複数のウィンドウを取り扱う方法を解説します。また、新規ウィンドウを開く、すでにウィンドウが開いているか確認する、異なるウィンドウ間でデータを受け渡すといった場面での tips やサンプルコードを紹介します。
新規ウィンドウを開く
新規ウィンドウを開くためには、通常は window.open
もしくは window.openDialog
の DOM メソッドを次のように呼び出します。
var win = window.open("chrome://myextension/content/about.xul", "aboutMyExtension", "chrome,centerscreen");
window.open
の 1 つめのパラメータはウィンドウとその内容を記述した XUL ファイルの URI です。
2 つめのパラメータは、ウィンドウ名です。この名前はリンクやフォームにおいて target
属性として利用することができます。これは、ユーザが見るウィンドウタイトルとは異なります。そちらは、XULを使って指定します。
3 つめは任意のパラメータで、ウィンドウに設定する特別な機能のリストです。
window.openDialog
関数は同様に動作しますが、JavaScript コードから参照可能な追加の引数を設定することができます。また、ウィンドウの機能設定についても dialog
機能が常に設定されているように動作するなど、多少異なります。
XPCOM コンポーネントのコードからウィンドウを開く時などのように Window
オブジェクトを利用できない場合、 nsIWindowWatcher インターフェイスを利用できます。パラメータは window.open
に類似しており、実際 window.open
の実装では nsIWindowWatcher
のメソッドを呼び出しています。
var ww = Components.classes["@mozilla.org/embedcomp/window-watcher;1"] .getService(Components.interfaces.nsIWindowWatcher); var win = ww.openWindow(null, "chrome://myextension/content/about.xul", "aboutMyExtension", "chrome,centerscreen", null);
Window オブジェクト
上記セクションで window.open
の戻り値を代入されている win
変数に注目してください。これを使って開いたウィンドウにアクセスすることが出来ます。 window.open
(やそれに似たメソッド) の戻り値は、 window
変数と同じ型の Window
オブジェクト (普通は ChromeWindow
) です。
技術的には、この戻り値は nsIDOMJSWindow
や nsIDOMWindowInternal
を含む多くのインターフェイスを実装していますが、ユーザー定義のグローバル変数のプロパティやウィンドウの関数も持っています。なので、例えば、そのウィンドウに対応する DOM ドキュメントにアクセスするのに win.document
を使うことが出来ます。
しかし注意すべきなのは、 open()
の呼び出しが返るのは、そのウィンドウが完全にロードされる 前 なので、 win.document.getElementById()
のような幾つかの呼び出しは失敗するであろうという事です。この困難に打ち勝つには、開かれようとしているウィンドウの load
ハンドラに初期化コードを移動するか、下記にあるようにコールバック関数を渡すのがいいでしょう。
document.defaultView
を使ってドキュメントから Window
オブジェクトを取得できます。
コンテンツウィンドウ
XUL ウィンドウが、 <browser>
や <iframe>
のようなページを表示できるウィジェットを含んでいる時、そのウィジェットのドキュメントは、当然、chrome ウィンドウ自体のドキュメントからは分離されています。個々のサブドキュメントにもまた、一般常識的にはウィンドウが無いにもかかわらず、Window
オブジェクトがあります。
<tabbrowser>
のタブの中に開かれた chrome ウィンドウにも同じことが当てはまります。タブの中に開かれた chrome ドキュメント上の要素はあなたの chrome ドキュメントから分離されています。
次の二つのサブセクションでは、(異なるコンテキストにあるにもかかわらず) chrome とコンテンツの境界線を越えるどちらかの方法、すなわち、chrome ドキュメントの祖先である要素にアクセスする方法と、chrome ドキュメントの子孫である要素にアクセスする方法を記述します。
コンテンツのドキュメントにアクセスする
ドキュメントの中に <tabbrowser>
、 <browser>
、 もしくは <iframe>
要素に読み込まれたドキュメントがあるとします。そのドキュメントには browser.contentDocument
を、そのドキュメントの Window
オブジェクトには browser.contentWindow
を使ってアクセスできます。
信頼できないコンテンツを扱う時には XPCNativeWrapper についてよく知っておくべきです。XPCNativeWrapper が有効 (Firefox 1.5+ ではデフォルト) だと、拡張機能はコンテンツドキュメントの DOM に安全にアクセスできますが、コンテンツの JavaScript にはアクセスできません。コンテンツの JavaScript を直接扱うために XPCNativeWrapper をバイパスすることはセキュリティ上の問題に繋がります。
コンテンツページとやり取りする必要があるならば、 特権コードと非特権コードの協調 を参照して下さい。
content
ショートカット
<browser type="content-primary"/>
の場合、 content ショートカットプロパティを使ってコンテンツドキュメントの Window
オブジェクトにアクセスできます。例えば、
// content-primary ウィジェットに表示されているドキュメントのタイトルを警告ダイアログで表示する alert(content.document.title);
例えば、browser.xul
オーバーレイの中で content.document
を使うことで、Firefox のウィンドウで選択されているタブのウェブページにアクセスできます。
_content
を content
の代わりに使っています。 前者はここ最近非推奨とされているので、新しくコードを書く時には content
を使うべきです。
サイドバー内のドキュメントにアクセスする
Firefox にはサイドバーがあり、id="sidebar" の <browser>
要素として実装されています。サイドバー内部の要素や変数にアクセスするには、コンテンツのドキュメントにアクセスする時のように、 document.getElementById("sidebar").contentDocument
や .contentWindow
を使う必要があります。
Code snippets:Sidebar でサイドバーに関するより多くの Tips を参照して下さい。
子ウィンドウからトップレベルドキュメントの要素にアクセスする
逆のケースとして、<browser>
や <iframe>
に読み込まれた特権スクリプトから chrome ドキュメントにアクセスしたい場合があります。
これが役に立つ典型的な例は、Firefox のメインウィンドウのサイドバー内でコードを実行して、メインのブラウザウィンドウ内の要素にアクセスしたい時です。
DOM Inspector で見られるような DOM ツリーはこのようになっています。
#document window main-window ... browser #document window myExtensionWindow
子ウィンドウがある場所がコードが実行される場所です。
やるべき事は chrome ドキュメントの上の要素にアクセスすること、すなわち chrome ウィンドウから脱出して祖先にアクセスすることです。これは下記の文によって可能になります。
var mainWindow = window.QueryInterface(Components.interfaces.nsIInterfaceRequestor) .getInterface(Components.interfaces.nsIWebNavigation) .QueryInterface(Components.interfaces.nsIDocShellTreeItem) .rootTreeItem .QueryInterface(Components.interfaces.nsIInterfaceRequestor) .getInterface(Components.interfaces.nsIDOMWindow);
これによって chrome と コンテンツ の境界を越えることができ、メインウィンドウのオブジェクトが返ってきます。
すでに開いているウィンドウを見つける
ウィンドウメディエータ XPCOM コンポーネント (nsIWindowMediator インターフェイス) は、存在しているウィンドウに関する情報を提供します。現在開かれているウィンドウの情報を得るために、getMostRecentWindow
と getEnumerator
という二つのメソッドがよく使われます。nsIWindowMediator のページでより多くの情報と nsIWindowMediator
の使用例を参照して下さい。 === Example: Opening a window only if it's not opened already === XXX TBD
ウィンドウ間でのデータのやり取り
複数のウィンドウを扱っていると、一方のウィンドウからもう一方へ情報を渡さなければならないことがよくあります。ウィンドウが異なると DOM ドキュメントとグローバルオブジェクトも別個のものをスクリプトに対して持っているので、スクリプトの中で違うウィンドウから一つのグローバル JavaScript 変数を単純に使うことはできません。
データを共有するためのテクニックには効力や単純さの点で異なるものが幾つかあります。次の幾つかのセクションで最も単純なものから最も複雑なものまで説明しましょう。
例 1: openDialog
でウィンドウを開いた時にデータを渡す
window.openDialog
または nsIWindowWatcher.openWindow
でウィンドウを開く時に、そのウィンドウに任意の引数を渡すことができます。引数は単純な JavaScript オブジェクトで、開かれたウィンドウの中で window.arguments
プロパティからアクセスできます。
この例では、プログレスダイアログを開くのに window.openDialog
を使用しています。最大値と現在の進行値だけでなく、現在の状況を示すテキストも渡しています。nsIWindowWatcher.openWindow
の使い方は少しだけ変わっているので注意してください 。 TODO: link to How To Pass an XPCOM Object to a New Window when it has a more useful example
開く側のコード:
window.openDialog("chrome://test/content/progress.xul", "myProgress", "chrome,centerscreen", {status: "Reading remote data", maxProgress: 50, progress: 10} );
progress.xul
:
<?xml version="1.0"?> <?xml-stylesheet href="chrome://global/skin/" type="text/css"?> <window onload="onLoad();" xmlns="https://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> <script><![CDATA[ var gStatus, gProgressMeter; var maxProgress = 100; function onLoad() { gStatus = document.getElementById("status"); gProgressMeter = document.getElementById("progressmeter"); if("arguments" in window && window.arguments.length > 0) { maxProgress = window.arguments[0].maxProgress; setProgress(window.arguments[0].progress); setStatus(window.arguments[0].status); } } function setProgress(value) { gProgressMeter.value = 100 * value / maxProgress; } function setStatus(text) { gStatus.value = "Status: " + text + "..."; } ]]></script> <label id="status" value="(No status)"/> <hbox> <progressmeter id="progressmeter" mode="determined"/> <button label="Cancel" oncommand="close();"/> </hbox> </window>
例 2: 開いたウィンドウとの相互のやり取り
開かれたウィンドウがそれを開いたウィンドウとやり取りしなければならないことがあります。例えば、ユーザーがウィンドウに変化を加えたことを知らせるためにはその必要があるでしょう。開かれたウィンドウの window.opener プロパティを使うか、前のセクションで述べた方法でウィンドウに渡されたコールバック関数を介して、ウィンドウを開いたウィンドウを見つける事が出来ます。
先の例に、ユーザーがプログレスダイアログのキャンセルボタンを押した時に、それを開いたウィンドウにそれを知らせるためのコードを追加してみましょう。
window.opener
を使う。opener
プロパティはそのウィンドウを開いたウィンドウを返します。さらに言えば、そのウィンドウを開いた Window オブジェクトです。
もしプログレスダイアログを開いたウィンドウが cancelOperation
関数を宣言した事がわかっているなら、ユーザーがキャンセルボタンを押したのを知らせるのにこのように window.opener.cancelOperation()
を使うことが出来ます。
<button label="Cancel" oncommand="opener.cancelOperation(); close();"/>
- コールバック関数を使う。別のやり方として、開いた側のウィンドウは、前の例で状態を表す文字列を渡したのと同じ方法で、プログレスダイアログにコールバック関数を渡すことが出来ます:
function onCancel() { alert("Operation canceled!"); } ... window.openDialog("chrome://test/content/progress.xul", "myProgress", "chrome,centerscreen", {status: "Reading remote data", maxProgress: 50, progress: 10}, onCancel);
そうすると、プログレスダイアログはこのようにしてコールバック関数を実行できます:
<button label="Cancel" oncommand="window.arguments[1](); close();"/>
例 3: opener
で事足りなければ nsIWindowMediator
を使う
window.opener
プロパティはとても簡単に使うことが出来ますが、ウィンドウがいくつかのよく知られた場所から開かれたことが判っている時にしか使えません。より複雑なケースでは、上で紹介された nsIWindowMediator
を使う必要があります。
nsIWindowMediator
を使いたくなるケースの一つは、拡張機能の設定ウィンドウです。あなたは browser.xul へのオーバーレイと設定ウィンドウから成るブラウザ拡張機能を開発しているとします。オーバーレイには設定ウィンドウを開くボタンが含まれており、そのウィンドウはブラウザウィンドウから幾つかのデータを読み取る必要があるとします。あなたは覚えているでしょうが、Firefox の拡張マネージャもあなたの設定ダイアログを開くのに使われることがあります。
これはつまり、あなたの設定ダイアログにおける window.opener
の値は必ずしもブラウザウィンドウではなく、拡張マネージャウィンドウかもしれないということです。opener
の location
プロパティを調べて、それが拡張マネージャウィンドウの場合には opener.opener
を使うということも出来るかもしれませんが、より良い方法は nsIWindowMediator
を使う事です。
var wm = Components.classes["@mozilla.org/appshell/window-mediator;1"] .getService(Components.interfaces.nsIWindowMediator); var browserWindow = wm.getMostRecentWindow("navigator:browser"); // |browserWindow| から値を読む
設定ダイアログでユーザーが行なった変更を適用するのに同じテクニックを使いたい衝動に駆られるかもしれませんが、それをするのにより良い方法は preferences observers を使う事です。
高度なデータ共有方法
上のコードは一つのウィンドウから他のウィンドウやウィンドウ群にデータを渡す必要がある場合には便利ですが、ただ単に異なるウィンドウ間で共通の JavaScript 変数を共有したいだけの場合もあります。それぞれのウィンドウで、ローカル変数とそれに対応するセッタ関数を宣言して、ウィンドウをまたいで変数の「インスタンス」を同期させるという事も出来ますが、幸運にも、もっと良いやり方があります。
共有の変数を宣言するには、アプリケーションが起動している間ずっと存在していて、異なる chrome ウィンドウ内のコードから簡単にアクセスできるような場所を見つける必要があります。そういった場所は実際に幾つかあります。
JavaScript コードモジュールの使用
JavaScript コードモジュール は、グローバルな共有シングルトンオブジェクトを作成し、あらゆる JavaScript スコープにインポートするための簡単な方法です。インポートする側のスコープはコードモジュール内のオブジェクトとデータにアクセスできるようになります。コードモジュールはキャッシュされるため、すべてのスコープがコードモジュールの同じインスタンスを取得し、モジュール内のデータを共有できます。詳しくは Components.utils.import を参照してください。
- 長所:
- これは「まっとうなやり方」です。
- 作成やインポートはとても簡単です。
- XPCOM コンポーネントを構築する必要がありません。
- 短所:
- Firefox 3 以降でしか使えません。
- モジュールとインポートする側のスコープとの間でスコープが共有されるため、名前の衝突に気をつける必要があります。
XPCOM シングルトンコンポーネントを使う
データを共有する方法として最もクリーンで強力なのは、独自の XPCOM コンポーネント (Javascript で書けます) を定義する方法です。このコンポーネントには、 getService
を呼び出すことでどこからでもアクセスできます。
Components.classes["@domain.org/mycomponent;1"].getService();
- 長所:
- これは「まっとうなやり方」です。
- コンポーネントの中に任意の JavaScript オブジェクトを保管できます。
- コンポーネント間でスコープが共有されないので、名前の衝突を心配する必要はありません。
- 古いバージョンの Firefox でも使用できます。
- 短所:
window
オブジェクトや、alert
やopen
といったそのメンバ、その他ウィンドウ内部から取得できるたくさんのオブジェクトが利用できません。しかし、その機能性は失われていません。それらの便利なショートカットの代わりに、直接 XPCOM コンポーネントを使わなければならないだけのことです。もちろん、単にコンポーネント内にデータを保存するだけならこの事は問題ではありません。- XPCOM コンポーネントの作成を学ぶのに時間がかかります。
オンラインに XPCOM コンポーネントの作成についての記事や本が幾つかあります。
FUEL の Application オブジェクトを使う
JavaScript ライブラリ FUEL には、ウィンドウ間でデータを共有する簡単な方法があります。Application
オブジェクトはデータをグローバルに格納するのに使用できる storage
プロパティをサポートしています。この方法は XPCOM シングルトンを使用する方法を単純化したものです。
Application.storage.set(keyname, data); var data = Application.storage.get(keyname, default); 意味: keyname はデータを識別するのに使用する文字列 data はデータ default は keyname が存在しない場合に返されるデータ値
- 長所:
- これは「まっとうなやり方」です。
- コンポーネントの中に任意の JavaScript オブジェクトを保管できます。
- コンポーネント間でスコープが共有されないので、名前の衝突を心配する必要はありません。
- XPCOM コンポーネントを構築する必要がありません。
- 短所:
- Firefox 3 以降でしか使えません。
window
オブジェクトや、alert
やopen
といったそのメンバ、その他ウィンドウ内部から取得できるたくさんのオブジェクトが利用できません。しかし、その機能性は失われていません。それらの便利なショートカットの代わりに、直接 XPCOM コンポーネントを使わなければならないだけのことです。もちろん、単にコンポーネント内にデータを保存するだけならこの事は問題ではありません。
設定に共有データを保管する
単に文字列や数値を保管したいだけなら、XPCOM コンポーネントを丸々書くような難しいことをする必要は無いかもしれません。そういったケースでは設定サービスが使えます。
- 長所:
- きわめて簡単に単純なデータを保管できます。
- 短所:
- 複雑なデータを保管するのは簡単ではありません。
- 設定サービスを乱用してその後片付けを自分でしないと、
prefs.js
が肥大化してアプリケーションの起動が遅くなる場合があります。
プリファレンスシステムの詳しい解説とコードの例は Code snippets:Preferences を参照して下さい。
例:
var prefs = Components.classes["@mozilla.org/preferences-service;1"] .getService(Components.interfaces.nsIPrefService); var branch = prefs.getBranch("extensions.myext."); var var1 = branch.getBoolPref("var1"); // 設定値を得る
隠しウィンドウを使ったハック
拡張機能の作者の中には、データやコードを保管するのに特殊な 隠しウィンドウ を使っている人もいます。隠しウィンドウは普通のウィンドウに似ていますが、他のウィンドウと違って、アプリケーションが起動している間中ずっと利用でき、ユーザーからは見えません。このウィンドウに読み込まれるドキュメントは、メニューを実装するのにこれが使われている Mac では chrome://browser/content/hiddenWindow.xul
で、他のオペレーティングシステムでは resource://gre/res/hiddenWindow.html
です。隠しウィンドウは、ゆくゆくはそれが必要とされないオペレーティングシステムからは削除されるでしょう (バグ 71895) 。
隠しウィンドウへの参照は nsIAppShellService インターフェイスから取得できます。他の DOM オブジェクトと同じように独自のプロパティを設定できます:
var hiddenWindow = Components.classes["@mozilla.org/appshell/appShellService;1"] .getService(Components.interfaces.nsIAppShellService) .hiddenDOMWindow; hiddenWindow.myExtensionStatus = "ready";
しかし、隠しウィンドウに入れられたオブジェクトは依然としてそれを作ったウィンドウに所属しています。もしそういったオブジェクトのメソッドが XMLHttpRequest
のような window
オブジェクトのプロパティにアクセスしたら、元のウィンドウが閉じられていた場合にはエラーメッセージが出るでしょう。これを避けるには、スクリプトファイルを使ってオブジェクトを隠しウィンドウに読み込むのがいいでしょう:
var hiddenWindow = Components.classes["@mozilla.org/appshell/appShellService;1"] .getService(Components.interfaces.nsIAppShellService) .hiddenDOMWindow; hiddenWindow.Components.classes["@mozilla.org/moz/jssubscript-loader;1"] .getService(Components.interfaces.mozIJSSubScriptLoader) .loadSubScript("chrome://my-extension/content/globalObject.js"); hiddenWindow.myExtensionObject.doSomething();
globalObject.js
にはこのようなコードが含まれています:
var myExtensionObject = { doSomething: function() { return new XMLHttpRequest(); } }
- 長所:
- 隠しウィンドウでコードを実行する場合、コンポーネントを使うのとは違い、
window
オブジェクトやそのプロパティを利用できます。 - 隠しウィンドウに任意の JavaScript オブジェクトを保管できます。
- 隠しウィンドウでコードを実行する場合、コンポーネントを使うのとは違い、
- 短所:
- これはハックです。
- 同じウィンドウに異なる拡張機能がアクセスするので、衝突を避ける為に長い変数名を使わなければなりません。
- Windows や Linux のビルドでは、隠しウィンドウは削除されるかもしれません。
参考