AIRが持つHTMLレンダリングエンジン

さて、今回はAIRが持つHTMLレンダリング機能について解説したい。

当連載の初回でも簡単に説明したとおり、AIRはWebブラウザの機能を内蔵している。おかげで、AIRはFlexによる開発だけではなく、標準的なHTML/JavaScriptを用いてアプリケーションを開発することが可能となっている。そして、Flexをベースとして開発されたアプリケーションであっても、このHTMLレンダリングエンジンを利用してWebページを表示したり、操作したりすることもできる。ちなみに、AIRランタイムに含まれているのはApple SafariブラウザのベースとなっているWebKitというHTMLレンダリングエンジンだ。

このHTMLレンダリング機能を用いると、インターネット上のWebページといえども、アプリケーションの構成部品として自在に取り扱うことが可能となる。この技術を応用したサンプルアプリケーションには、以下のようなものが挙げられる。

「Scout」は、HTMLソースやDOMツリーなど、Webページのありとあらゆる情報を表示し、編集することが可能なブラウザアプリケーションだ。Adobe Labsのサンプル集から入手できる

「DryerFox」は、ドラム式洗濯機の中でWebページがぐるぐる回るというもの。Doug Schmidt氏のブログから入手可能だ。ただし、Apollo αで動作するようになっているので、AIR βで動作させるにはアプリケーションディスクリプタを書き変える必要がある。

今回の記事でも、AIRのHTMLレンダリング機能を理解するのに手頃なサンプルを用意したので、後に紹介する。まずは、基本的なAPIを解説しよう。

HTML関連のAPI

HTML関連のAPIは、それほど数も多くなく、理解は容易だ。とりあえず以下の二つを押さえておけば問題はない。

  • mx.controls.HTML
  • flash.html.HTMLControl

mx.controls.HTML

こちらは、HTMLをロードして表示するためのMXMLコンポーネントだ。location属性にURLを指定するだけで、指定したWebページを表示することができ、表示領域からはみ出る場合は自動的にスクロールバーが表示される。

例えば、HTMLコンポーネントを用いてマイコミジャーナルを表示するのであれば、

<mx:HTML location="http://journal.mycom.co.jp/index.html" width="100%" height="100%" />

たったこれだけだ。上で紹介したサンプルのどちらも、このHTMLコンポーネントを使用して作成されている。

flash.html.HTMLControl

単にWebページを表示するだけではなく、ページ内のDOMやJavaScriptコードを操作したり、Webページの読み込みにおける様々なイベントを捕捉したい場合は、flash.html.HTMLControl APIに関する知識が必要になる。先に説明したHTMLコンポーネントも、ページのレンダリング自体はHTMLControlを用いて行っており、HTMLクラスのプロパティ「htmlControl」でアクセスできる。

このクラスを直接用いた場合、以下のようなコードになるのが典型的だ。

// (1) HTMLControlを生成してプロパティに値をセット
var html:HTMLControl = new HTMLControl();
html.width = 200;
html.height = 150;

// (2) Webページを読み込み
html.load(new URLRequest("http://journal.mycom.co.jp/index.html"));

// (3) Spriteに貼り付け
var sprite:Sprite = new Sprite();
sprite.addChild(html);

(1) HTMLControlは、普通にnewしてインスタンスを生成し、プロパティをセットできる。

(2) loadメソッドを使用して、Webページの読み込みを行うことができる。引数には、flash.net.URLRequestを指定する。HTML形式の文字列を引数に取るloadString(htmlContent:String)メソッドも存在する。

(3) HTMLControlクラスは、直接Flexコンポーネントに子要素として追加することはできない。この例のように、flash.display.Spritemx.core.UIComponentに子要素として追加することで、他のFlexコンポーネント上に表示させることができるようになる。

HTMLControlのイベント処理

HTMLControlクラスは、addEventListenerメソッドを用いて、HTMLレンダリングの際の様々なイベントを処理するリスナを登録することができる。数多くのイベントが存在するが、良く使うのは以下のようなイベントだ。

  • Event.COMPLETE - ページの読み込みが完全に完了した
  • Event.DOM_INITIALIZE - DOMツリーを生成する直前
  • Event.HTML_BOUNDS_CHANGE - HTMLコンテンツの幅や高さが変化した場合
  • HTMLUncaughtJavaScriptExceptionEvent.UNCAUGHT_JAVA_SCRIPT_EXCEPTION - 捕捉されていないJavaScript例外が発生した場合

HTMLControlをWebブラウザのように使う

HTMLControlクラスは、loadメソッドを用いて読み込んだページの履歴を保持しており、一般的なWebブラウザのように「進む」「戻る」などのナビゲーションを行うことが可能だ。

そうした、一般的なWebブラウザが持つナビゲーションと似たようなことを行うためのメソッド・プロパティは以下の通りだ。

  • historyLengthプロパティ - 保持している履歴の総数を表す
  • historyPositionプロパティ - 履歴内の、どの位置にいるのかを返す。このプロパティに値をセットすることも可能。
  • historyBack()メソッド - 履歴を一つ戻る
  • historyForward()メソッド - 履歴を一つ進む
  • historyGo(steps:int)メソッド - ゼロを履歴内の現在位置とし、指定した履歴のページにジャンプする
  • reload()メソッド - 現在のページを読みなおす
  • cancelLoad()メソッド - ロードを中止する

読み込んだページのJavaScriptにアクセスする

HTMLControlでWebページを読み込んだ後、windowプロパティにアクセスすれば、そのページにおけるJavaScriptのwindowオブジェクトにアクセスすることができる。

html.load(new URLRequest("http://journal.mycom.co.jp/index.html"));

// ドキュメントのタイトルを表示
trace(htmlControl.window.document.title);

サンプルアプリケーションの解説

では、以上の説明を踏まえてサンプルアプリケーションを紹介したい。

今回のサンプルは、上部のテキストフィールドにURLを入力して「Go」ボタンを押すと、Webページが表示されるというものだ。ただこれだけだと面白くないので、表示されたWebページ内のリンクは全て、マウスオーバーするだけでリンク先ページの「サムネイル」が表示されるようにした。

このサンプルで表示したページ内の全てのリンクは、リンク先のサムネイルが表示される

以下に示すのがサンプルの全ソースコードだ。エラー処理を考慮していなかったり、まだ粗削りな部分はあるがご容赦願いたい。今回は、コード中のコメントをもって解説と代えさせていただく。

HTMLControlDemo.mxml

<?xml version="1.0" encoding="utf-8"?>
<mx:WindowedApplication xmlns:mx="http://www.adobe.com/2006/mxml" layout="absolute" creationComplete="init();">
  <mx:Script>
    <![CDATA[
      import flash.html.HTMLControl;
      import flash.html.JavaScriptObject;

      private const DEFAULT_HOME:String = "http://www.google.com";

      // サムネイルに表示するページの幅と高さ
      private const THUMBNAIL_WIDTH:uint = 1024;
      private const THUMBNAIL_HEIGHT:uint = 800;

      // アプリケーションの初期化     
      private function init():void {
        // HTMLコントロールにリスナを登録
        htmlPage.htmlControl.addEventListener(Event.LOCATION_CHANGE, onLocationChange);
        htmlPage.htmlControl.addEventListener(Event.COMPLETE, onHTMLComplete);
        // テキストフィールドにホームページのアドレスをセット
        locationInput.text = DEFAULT_HOME;
        changeLocation();
      }
      // HTMLControlのロケーションが変化した際呼び出される
      private function onLocationChange(event:Event):void {
        var htmlControl:HTMLControl = HTMLControl(event.target);
        // テキストフィールドに表示されたURLを変更
        locationInput.text = htmlControl.location;
      }
      // ページの移動を行うメソッド
      private function changeLocation():void {
        var url:String = locationInput.text;
        htmlPage.location = url;
      }
      // ページ内で、一度表示したサムネイルのキャッシュ
      private var thumbnailCache:Object;

      // WEBページの読み込みが完了した際呼び出される
      private function onHTMLComplete(event:Event):void {
        thumbnailCache = {};

        var htmlControl:HTMLControl = HTMLControl(event.target);
        // JavaScriptオブジェクトにアクセスして、ページ内のリンクを全て取得
        var links:JavaScriptObject = htmlControl.window.document.links;
        for (var i:uint = 0; i < links.length; i++) {
          // リンクにイベントハンドラを登録
          links[i].addEventListener("mouseover", createThumbnailLoadFunc(links[i]));
          links[i].addEventListener("mouseout", createThumbnailHideFunc(links[i]));
          links[i].addEventListener("click", createThumbnailHideFunc(links[i]));
        }
      }

      // サムネイルを非表示にする関数のオブジェクトを返す
      private function createThumbnailHideFunc(link:JavaScriptObject):Function {
        return function(mouseEvent:JavaScriptObject):void {
          trace("hide thumbnail.");
          var thumbnail:Sprite = thumbnailCache[link.href];
          if (!thumbnail) {
            return;
          }
          htmlPage.removeChild(thumbnail);
        }
      }
      // サムネイルをロードし、表示する関数のオブジェクトを返す
      private function createThumbnailLoadFunc(link:JavaScriptObject):Function {
        return function(mouseEvent:JavaScriptObject):void {
          // URLをキーとしてキャッシュされているサムネイルを取得
          var thumbnail:Sprite = thumbnailCache[link.href];
          // キャッシュに存在しない場合は、新たにロード
          if (!thumbnail) {
            thumbnail = new Sprite();

            var html:HTMLControl = new HTMLControl();
            html.load(new URLRequest(link.href));

            // Webページを縮小して表示
            html.scaleX = .5;
            html.scaleY = .5;

            html.width = THUMBNAIL_WIDTH;
            html.height = THUMBNAIL_HEIGHT;
            thumbnail.addChild(html);
            thumbnailCache[link.href] = thumbnail;
          }
          // マウスポインタの位置に表示
          thumbnail.x = mouseEvent.x;
          thumbnail.y = mouseEvent.y;
          htmlPage.addChild(thumbnail);
        };
      }
    ]]>
  </mx:Script>

  <mx:VBox x="10" y="10" height="100%" width="100%">
    <mx:HBox height="20" width="100%">
      <mx:TextInput id="locationInput" width="100%"/>
      <mx:Button label="Go"/> 
    </mx:HBox>
    <mx:HTML id="htmlPage" width="100%" height="100%" />
  </mx:VBox>
</mx:WindowedApplication>