IE の アドオン 開発

1 件のコメント

今回は、Internet Explorer 上で動作する アドオン の開発を C# で実装してみます。

アドオンはページ内で ActiveX オブジェクトの生成を行う、または objectタグ を HTML に書き込むことで利用できるものです。 メニューの[ツール]-[アドオンの管理]から、現在利用できるアドオンを確認できます。

※ 本記事の更新履歴を末尾に掲載しています。

サンプルコード の ダウンロード

本記事 の メニュー

  1. 概要
  2. 空のソリューションの作成
  3. アドオンプロジェクトの作成
  4. アドオンの実装
  5. テストプロジェクトの作成
  6. テストページの作成
  7. アドオンのテスト実行
  8. まとめ

関連記事

概要

C# & .NET Framework を用いて、 BHO(Browser Helper Object) なるもの ── いわゆるアドオン ── をマネージコードで作る方法を記載します。 ちまたの記事では C++ が多いですが、個人的にもう触りたくない(…実際は忘れたwww)ので、C#を使うことにしました。

目標は、よくある「Hello World」の表示を行うもので、ついでにテキストボックスを編集するとコールバックされるものとします。 具体的には、HTML に input ボタン を埋め込んで、そのボタンを押すと、「Hello World」と書いた Form ダイアログを表示する。 表示されたテキストを変更するとHTMLへ通知されるというプログラムです。

以下では、Visual Studio 2010、Internet Explorer 9.0 の環境で解説します。

完成イメージ

クラス図

各クラス(ファイル)と役割は以下の通りです。

  • Form1.cs … WindowsForm。MyForm.cs から 呼び出される。
  • IMyFormEvent.cs … JavaScript へ 公開する イベントインターフェース。
  • IMyFormMethod.cs … JavaScript へ 公開する 関数インターフェース
  • IObjectSafety.cs … IE の 警告回避 を行うインターフェース。
  • IObjectSafetyTLB.cs … IE の 警告回避 を行う実装。
  • MyForm.cs … プラグイン本体。IMyFormMethod, IMyFormEvent, IObjectSafety を実装する。

以下で、IObjectSafety.cs, IObjectSafetyTLB.cs を除く、各ファイルの内容について記載します。 IObjectSafety.cs, IObjectSafetyTLB.csに関する詳細は こちら を参照してください。

空のソリューションの作成

  1. [ファイル]-[新規作成]-[プロジェクト]を選択
    →「新しいプロジェクト」ウィンドウが開く
  2. 空のソリューション」を選択
    (「インストールされたテンプレート」から[その他のプロジェクトの種類]-[Visual Studio ソリューション]を選択すると「空のソリューション」が出てきます。)
  3. 「名前」と「場所」を入力 or 確認して、「OK」を選択
    今回、名前は "SampleActiveXPlugIn"、場所は適当に…とします。

プロジェクトをいきなり作ると、ソリューション名とプロジェクト名が一致してしまいます。 個人的に、ソリューション名とプロジェクト名は分けたいので、このような方法をとりました。

アドオンプロジェクトの作成

  1. [ファイル]-[新規作成]-[プロジェクト]を選択
    →「新しいプロジェクト」ウィンドウが開く
  2. クラスライブラリ」を選択
    (「インストールされたテンプレート」から[Visual C#]-[Windows]を選択すると「クラスライブラリ」が一覧に出てきます。)
  3. 「名前」、「場所」、「ソリューション」を入力 or 確認して、「OK」を選択
    名前は "MyForm"、場所は自動のまま、ソリューションは "ソリューションに追加" とします。
  4. プロジェクトのプロパティを変更
    • アプリケーション
      • (任意)「既定の名前空間」を "Sample.ActiveXPlugIn" に変更
      • (必須)「センブリ情報」-「アセンブリを COM 参照可能にする」にチェック
    • ビルド
      • (推奨)「COM 相互運用機能の登録」にチェック

ソリューションエクスプローラーにあるソリューションアイコンを右クリックして[追加]-[新しいプロジェクト]でも同じことができます。

「アセンブリを COM 参照可能にする」は、有効にしていないと、JavaScriptからアクセスできないので、必須になります。 その他は個人的な趣味の世界なので任意かとは思います。 「既定の名前空間」は完全に気分で変えています。 「COM 相互運用機能の登録」は便利なので有効にしています。 「COM 相互運用機能の登録」を無効のままにする場合、自前でレジストリ登録を行う必要がある点にご注意ください。

アドオンの実装

「アドオンプロジェクト」に以下のファイルを作成していきます。

IObjectSafety.cs, IObjectSafetyTLB.csに関する詳細は こちら を参照してください。

Form1.cs

"新しい項目"として "Windows フォーム" を追加。 テキストボックス(textBox1)、ボタン(button1)を配置します。 テキストボックスには TextChanged、ボタンには Click イベントを実装します。

namespace Sample.ActiveXPlugIn
{
    using System;
    using System.Collections.Generic;
    using System.ComponentModel;
    using System.Data;
    using System.Drawing;
    using System.Linq;
    using System.Text;
    using System.Windows.Forms;
    
    public partial class Form1 : Form
    {
        // ---------------------------------------------------
        // コンストラクタ
        // ---------------------------------------------------
        public Form1()
        {
            this.InitializeComponent();
        }

        // ---------------------------------------------------
        // イベント
        // ---------------------------------------------------
        public event EventHandler TextChanged;

        // ---------------------------------------------------
        // プロパティ
        // ---------------------------------------------------
        public string Message
        {
            get
            {
                return this.textBox1.Text;
            }

            set
            {
                this.textBox1.Text = value;
            }
        }

        // ---------------------------------------------------
        // メンバ関数
        // ---------------------------------------------------
        private void button1_Click(object sender, EventArgs e)
        {
            this.Close();
        }

        private void textBox1_TextChanged(object sender, EventArgs e)
        {
            if (this.TextChanged != null)
            {
                this.TextChanged(sender, e);
            }
        }
    }
}

自動生成コードは省いています。 このクラスは MyForm クラス(HTMLからアクセスされるクラス)の中で利用します。 MyForm クラスが必要そうなプロパティ、メンバ関数を公開するようにしておきます。 (今回だと、TextChangedイベント と Messageプロパティ)

IMyFormMethod.cs
namespace Sample.ActiveXPlugIn
{
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Runtime.InteropServices;
    using System.Text;

    [Guid("8948A099-FCAE-48D6-B5F1-D909CAEAB280")]
    public interface IMyFormMethod
    {
        [DispId(1)]
        void Open(string message);

        [DispId(2)]
        void Close();
    }
}

GUIDは任意で指定してください。 このGUIDはどこかで使われることはありません。 DispIdはこのクラス内でかぶらないように数値を指定してください。

COM 公開される関数の一覧になります。 ここで定義された関数は、ActiveXオブジェクトを生成したのち JavaScript でアクセスできます。

IMyFormEvent.cs
namespace Sample.ActiveXPlugIn
{
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Runtime.InteropServices;
    using System.Text;

    [Guid("045A50A0-C9EA-44FD-A1B8-01A5601EA605")]
    [InterfaceType(ComInterfaceType.InterfaceIsIDispatch)]
    public interface IMyFormEvent
    {
        [DispId(1)]
        void Opened(IMyFormMethod mine);

        [DispId(2)]
        void Closed(IMyFormMethod mine);

        [DispId(3)]
        void TextChanged(string text);
    }
}

GUIDは任意で指定してください。 このGUIDはどこかで使われることはありません。 InterfaceType を InterfaceIsIDispatch とすることで、コールバック関数であることを定義します。 DispIdはこのクラス内でかぶらないように数値を指定してください。

ここで定義した関数は、イベント発生時に JavaScript へコールバックすることができる関数です。

MyForm.cs
namespace Sample.ActiveXPlugIn
{
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Runtime.InteropServices;
    using System.Text;
    using System.Windows.Forms;

    [Guid("273737CB-CA7D-45C5-84F6-EF4D5A3D8C79")]
    [ComSourceInterfaces(typeof(IMyFormEvent))]
    [ClassInterface(ClassInterfaceType.None)]
    [ProgId("Sample.ActiveXPlugIn.MyForm")]
    public class MyForm : IObjectSafetyTLB, IMyFormMethod
    {
        // ---------------------------------------------------
        // メンバ変数
        // ---------------------------------------------------
        private Form1 form;

        // ---------------------------------------------------
        // コンストラクタ
        // ---------------------------------------------------
        public MyForm()
        {
        }

        // ---------------------------------------------------
        // デリゲート
        // ---------------------------------------------------
        public delegate void DefaultEventHandler(IMyFormMethod form);

        public delegate void TextChangedEventHandler(string text);

        // ---------------------------------------------------
        // イベント
        // ---------------------------------------------------
        public event DefaultEventHandler Opened;

        public event DefaultEventHandler Closed;

        public event TextChangedEventHandler TextChanged;

        // ---------------------------------------------------
        // メンバ関数
        // ---------------------------------------------------        
        public void Open(string message)
        {
            this.CreateSubWindow();

            if (!string.IsNullOrEmpty(message))
            {
                this.form.Message = message;
            }
            
            this.form.Show();
        }

        public void Close()
        {
            this.form.Close();
            this.form.Dispose();
        }

        protected void OnOpened()
        {
            if (this.Opened != null)
            {
                this.Opened(this);
            }
        }

        protected void OnClosed()
        {
            if (this.Closed != null)
            {
                this.Closed(this);
            }
        }

        protected void OnTextChanged(string text)
        {
            if (this.TextChanged != null)
            {
                this.TextChanged(text);
            }
        }

        private void CreateSubWindow()
        {
            Form1 form;
            
            form= new Form1();
            form.Shown += new EventHandler((sender, e) => this.OnOpened());
            form.FormClosed += new FormClosedEventHandler((sender, e) => this.OnClosed());
            form.TextChanged += new EventHandler((sender, e) => this.OnTextChanged(((TextBox)sender).Text));
            
            this.form = form;
        }
    }
}

GUIDは任意で指定してください。 このGUIDは HTML の オブジェクトタグ で利用します。 ComSourceInterfaces で公開するイベントを関連づけます。 COM インターフェース を自前で準備しているので ClassInterface は None とします。 ProgId は任意の文字列で…かぶらない感じに指定します。 IObjectSafety を継承、実装(IObjectSafetyTLB)させておかないと、インスタンス生成ができません。 IObjectSafety に関する詳細は こちら を参照してください。

デリゲートを明示的に準備しておかないと動きませんでした。 汎用的なデリゲート Action<T> を使うとイベントが動きません。 面倒ですが、明示的にデリゲートを準備してください。 なお、イベントは ComSourceInterfaces で指定したイベントと名称を合わせるようにしてください。

テストプロジェクトの作成

  1. [ファイル]-[新規作成]-[プロジェクト]を選択
    →「新しいプロジェクト」ウィンドウが開きます。
  2. ASP.NET 空の Web アプリケーション」を選択
    (「インストールされたテンプレート」から[Visual C#]-[Web]を選択すると「ASP.NET 空の Web アプリケーション」が一覧に出てきます。)
  3. 「名前」、「場所」、「ソリューション」を入力 or 確認して、「OK」を選択
    名前は "MyFormTest"、場所は自動のまま、ソリューションは "ソリューションに追加" とします。

テストページの作成

「テストプロジェクト」に以下のファイルを作成していきます。 ActiveX の呼び出し方は2通りあるので、その2種類(objectタグを利用する方法とActiveXObjectで動的に生成する方法)とも試してみます。

パターン1(objectタグの利用)
  • Default.htm

パターン1では、objectタグを利用した使用方法を示します。 CLSID を指定して ActiveX を利用します。 HTML ページの生死と ActiveX の生死を同じくする方法です。

Default.htm
<!DOCTYPE html>
<html>
<head>
    <title>Sample ActiveX Plug-in Test Page</title>
    <style type="text/css">
        textarea
        {
            width: 400px;
            height: 100px;
            font-size: 9pt;
        }
    </style>
</head>
<body>

<input id="btn" type="button" value="開く" /><br />
<textarea id="txt"></textarea>

<object id="MyForm" classid="CLSID:273737CB-CA7D-45C5-84F6-EF4D5A3D8C79"></object>

<div style="width:0; height:0; display:none; visibility:hidden;">
    <script type="text/javascript">
        window.onload = function () {
            document.getElementById('btn').addEventListener('click', function (event) {
                document.MyForm.Open('Hello World !!');
            }, false);
        };   
    </script>
    <script language="javascript" type="text/javascript" for="MyForm" event="Opened(form)">
    var txt = document.getElementById('txt');
    txt.value = 'ActiveX opened.\r\n' + txt.value;
    </script>
    <script language="javascript" type="text/javascript" for="MyForm" event="Closed(form)">
    var txt = document.getElementById('txt');
    txt.value = 'ActiveX closed.\r\n' + txt.value;
    </script>
    <script language="javascript" type="text/javascript" for="MyForm" event="TextChanged(text)">
    var txt = document.getElementById('txt');
    txt.value = 'ActiveX text changed - "' + text + '"\r\n' + txt.value;
    </script>
</div>
</body>
</html>

objectタグを利用する場合、scriptタグの for 属性に objectタグの id を、event属性に イベント名を指定します。

パターン2(new ActiveXObject の利用)
  • Default2.htm
  • Default2.js

パターン2では、ActiveXオブジェクトを動的に生成、使用する方法を示します。 イベントの取り付け方が気持ち悪いですが、スクリプトを完全に外部ファイル化できるのが良いところと思います。

Default2.htm
<!DOCTYPE html>
<html>
<head>
    <title>Sample ActiveX Plug-in Test Page</title>
    <style type="text/css">
        .textarea
        {
            width: 400px;
            height: 100px;
            overflow: auto;
            font-size: 9pt;
            border: 1px solid silver;
            white-space: nowrap;
        }
    </style>
    <script type="text/javascript" src="Default2.js"></script>
</head>
<body>

<input id="btn" type="button" value="開く" /><br />
<div id="txt" class="textarea"></div>

</body>
</html>
Default2.js
var obj, btn, txt;

obj = new ActiveXObject('Sample.ActiveXPlugIn.MyForm');

window.onload = function () {
    btn = document.getElementById('btn');
    txt = document.getElementById('txt');

    btn.addEventListener('click', function (event) {
        obj.Open('Hello World !!');
    }, false);

    function addMessage(message){
        var div, log;

        log = '';
        log += '[';
        log += (new Date()).toLocaleString();
        log += '] ';
        log += message;

        div = document.createElement('div');
        div.appendChild(document.createTextNode(log));

        txt.insertBefore(div, txt.firstChild);
    }

    function obj::Opened(form) {
        addMessage('Opened.');
    }

    function obj::Closed(form) {
        addMessage('Closed.');
    }

    function obj::TextChanged(text) {
        addMessage('TextChanged - "' + text + '"');
    }
};   

ActiveXObject で動的に生成する場合、イベントの接続は "function イベントオブジェクト::イベント名" で行います。 オブジェクトの生成とイベントの付与の順序関係上、 onload より前に ActiveX を生成しておき、 onload 中( ActiveX が絶対存在するとき)でイベントを接続します。 同じ関数内で ActiveX オブジェクトの生成とイベントの接続を行うとエラーになります。

アドオンのテスト実行

  1. テストプロジェクトの参照設定にアドオンプロジェクトを追加
  2. テストプロジェクトをスタートアッププロジェクトに指定
  3. デバッグ実行

参照設定はビルド順を指定するためです。 プロジェクト依存関係をしている場合は不要と思います。

ActiveXオブジェクトの生成で失敗する場合…

  • 正しくレジストリ登録されているか?
    レジストリを確認してみてください。 MyForm.cs の GUID および ProgId が正しく登録されているかどうか?
  • IObjectSafety が正しく実装されているか?
    MyForm.cx が正しく IObjectSafety を実装しているか確認してみてください。

まとめ

IE 向け アドオン の開発から、実際に利用するところまでを記載しました。

開発編
開発だと、インターフェースの作り方、属性の付け方などがキーになるかと思います。 デリゲートは自分がはまってしまったので…注意してください。

  • 「アセンブリを COM 参照可能にする」を設定
  • 接続用のインターフェースを作成(2種類)
  • IObjectSafety を実装

利用編
利用だと、(メモリーリークが気になりますが…)イベントを受信するための記載方法2通りがキーになるかと思います。 ActiveXObject 関数を利用する場合、同じ関数内(スコープ内)だとイベントの接続ができないので、遅延させて接続するようにします。

  • object タグを利用して ActiveX を使う場合
    インスタンス生成
    <object id="[インスタンスへアクセスするためのID]" classid="CLSID:[実態に指定したGUID]" />
    
    イベントの接続
    <script language="javascript" type="text/javascript" for="[objectタグのID]" event="[イベント名]([引数...])">
        // イベントの実装
    </ script>
    
  • ActiveXObject 関数を利用して ActiveX を使う場合
    インスタンス生成
    var obj = new ActiveXObject("[ProgIdで指定したクラス名]");
    
    イベントの接続
    function obj::[イベント名]([引数...]) {
        // イベントの実装
    }
    

今回、参考にしたサイトは以下の通りです。(漏れてるページがありそうですが…)

更新履歴

  1. こんにちは。これは、BHO(Browser Helper Object) ではなくて、通常の(安全とマークされた)ActiveX ですね。「なるもの」の表現の意味が、似たものの意味でしたら失礼...

    返信削除