画面の描画内容を一時的にロックする方法

Firefox のウィンドウ内の描画を一旦停止して、処理を行った後で改めて再描画させる、という事をしたいと思った事はないでしょうか。Firefox 3(Gecko 1.9)以降であれば、これは以下のコードで実現できます。

var baseWindow = window.top
          .QueryInterface(Ci.nsIInterfaceRequestor)
          .getInterface(Ci.nsIWebNavigation)
          .QueryInterface(Ci.nsIDocShell)
          .QueryInterface(Ci.nsIBaseWindow);
baseWindow.setPosition(window.innerWidth, window.innerHeight); // これで画面の描画が止まる

gBrowser.addTab(); // これによって起こる変化は画面上に現れない
gBrowser.addTab(); // この変化も画面上に現れない
gBrowser.addTab(); // 同上

baseWindow.setPosition(0, 0); // ここでやっと描画が再開される

具体的な利用例を1つ挙げてみます。

CSS のプロパティを動的に変更して UI 要素の表示を変える事はよくあると思いますが、その際、複数の UI 要素の表示を調整するために順番に処理を行っていると、最初の状態から最終的な状態に至るまでの間の中間状態が描画されてしまって、画面がチラつく事があります。

例えば、普段はタブバーを非表示にしておき、ポインタが近づいた時だけタブバーを表示する、という機能は以下のように実装できます。

var TabbarAutoHide = {
  hide : function() {
    gBrowser.mStrip.collapsed = true;
    // show()でずらした表示位置を元に戻す。
    let style = gBrowser.mPanelContainer.style;
    style.setProperty('position', '', '');
    style.setProperty('margin-bottom', '', '');
    style.setProperty('height', '', '');
    // 一旦画面外まで大きくずらしているのは、その範囲を強制的に再描画させるため。
    let viewer = gBrowser.markupDocumentViewer;
    viewer.move(window.outerWidth, window.outerHeight);
    viewer.move(0, 0);
  },
  show : function() {
    gBrowser.mStrip.collapsed = false;
    let tabHeight = gBrowser.mStrip.boxObject.height
    let panelHeight = gBrowser.mPanelContainer.boxObject.height + tabHeight;
    // コンテンツ表示領域をずらす分だけ下に空白ができてしまうので、それをごまかす
    let style = gBrowser.mPanelContainer.style;
    style.setProperty('position', 'relative', 'important');
    style.setProperty('margin-bottom', -tabHeight+'px', 'important');
    style.setProperty('height', panelHeight+'px', 'important');
    // コンテンツ表示領域の表示位置をタブバーの分だけ上にずらす
    let viewer = gBrowser.markupDocumentViewer;
    viewer.move(window.outerWidth, window.outerHeight);
    viewer.move(0, -tabHeight);
  }
};

しかしこのままだと、タブバーの表示・非表示が切り替わる瞬間にコンテンツ表示領域がガタガタと揺れるように見えてしまってとても不格好です。

こういう時に、冒頭で紹介したテクニックが役に立ちます。

var TabbarAutoHide = {
  get baseWindow() {
    return window.top
             .QueryInterface(Ci.nsIInterfaceRequestor)
             .getInterface(Ci.nsIWebNavigation)
             .QueryInterface(Ci.nsIDocShell)
             .QueryInterface(Ci.nsIBaseWindow);
  },
  hide : function() {
    this.baseWindow.setPosition(window.innerWidth, window.innerHeight);
    gBrowser.mStrip.collapsed = true;
    ...
    viewer.move(0, 0);
    this.baseWindow.setPosition(0, 0);
  },
  show : function() {
    this.baseWindow.setPosition(window.innerWidth, window.innerHeight);
    gBrowser.mStrip.collapsed = false;
    ...
    viewer.move(0, -tabHeight);
    this.baseWindow.setPosition(0, 0);
  }
};

このように、複数回の再描画が行われそうな部分の前後で描画の一時停止と再開を行えば、ユーザにとっては煩わしいだけの画面上のチラつきを減らすことができます。

ただ、複数の関数(メソッド)でそれぞれ独自に描画の停止/再開を行っていると、停止→停止→再開→再開 のような順で処理が行われてしまうため、せっかく見えないようにしたはずの処理中の画面が見えてしまうことがあります。このようなトラブルを防ぐには、呼び出しの深さをカウントするなどの方法で、実際に描画を停止/再開させるケースを判別する必要があります。この点だけに絞って最小構成でライブラリ化した stopRendering.js という物を作りましたので、ぜひ使ってみて下さい。window['piro.sakura.ne.jp'].stopRendering.stop() で描画が停止され、window['piro.sakura.ne.jp'].stopRendering.start() で再開されます。

なお、この方法の発見に至った経緯も公開していますので、アドオン開発がどういう風に行われているのかの一端を覗いてみたい方は見てみるといいかもしれません。(これはかなり特殊なケースですが……)

※このエントリでは当初 nsIContentViewer の hide() メソッドと show() メソッドを使った方法を紹介していましたが、この方法には弊害があることが判明したため、nsIBaseWindow の setPosition() メソッドを使った方法に訂正しました。訂正前のエントリをご覧になった方はご注意下さい。訂正前の方法の弊害は自サイトの新しいエントリに詳しく記載しています。