WebSurfer's Home

トップ > Blog 1   |   ログイン
APMLフィルター

WebBrowser の拡張

by WebSurfer 2012年7月1日 18:04

先の記事 SHDocVw.dll と AxSHDocVw.dll の作り方と使い方 では、ActiveX の WebBrowser コントロール (shdocvw.dll) をホストする、AxHost クラス から派生するラッパーコントロールを Visual Studio で生成し、それ使って NewWindow2 イベントを利用する例を書きました。

それと比較するために、.NET Framework の WebBrowser(これも shdocvw.dll のマネージラッパー)を拡張して同様なことを行うコードを書いてみました。

かなり面倒で、最初は COM の相互運用の知識がなかったのでお手上げ状態でした。あちこちググって調べて、動くようになるまで 3 日ぐらいかかりました。(笑)

詳しくは以下のコードとそのコメントを参照してください。参考にしたページの一覧も書いておきます。(手抜きですみません)

WebBrowser.CreateSink メソッド

Extended .NET 2.0 WebBrowser Control

COM相互運用機能の利用

COM相互運用機能の利用 - パート2

Microsoft .NET/COM の移行と相互運用性

COM ラッパー

ランタイム呼び出し可能ラッパー

COM 相互運用性 - 第 1 部 : C# クライアント チュートリアル

方法: COM ソースによって発生したイベントを処理する

.NET の WebBrowser を拡張したクラス

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using System.Runtime.InteropServices;
using System.Security;
using System.Security.Permissions;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Reflection;

namespace WebBrowserExtended
{
  public class MyWebBrowser : WebBrowser
  {
    // WebBrowser の AxtiveX への参照
    private IWebBrowser2 axIWebBrowser2;

    // WebBrowser の AxtiveX が作成されたとき呼び出される
    [PermissionSet(SecurityAction.LinkDemand, 
      Name = "FullTrust")]
    protected override void 
      AttachInterfaces(object nativeActiveXObject)
    {
      this.axIWebBrowser2 = 
        (IWebBrowser2)nativeActiveXObject;
      base.AttachInterfaces(nativeActiveXObject);
    }

    [PermissionSet(SecurityAction.LinkDemand, 
      Name = "FullTrust")]
    protected override void DetachInterfaces()
    {
      this.axIWebBrowser2 = null;
      base.DetachInterfaces();
    }

    public object Application
    {
      get
      {
        if ((this.axIWebBrowser2 == null))
        {
          throw new 
            AxHost.InvalidActiveXStateException(
              "Application",
              AxHost.ActiveXInvokeKind.PropertyGet);
        }

        // この Application プロパティは COM の
        // マネージラッパー
        return this.axIWebBrowser2.Application;
      }
    }

    public bool RegisterAsBrowser
    {
      get
      {
        if ((this.axIWebBrowser2 == null))
        {
          throw new 
            AxHost.InvalidActiveXStateException(
              "RegisterAsBrowser",
              AxHost.ActiveXInvokeKind.PropertyGet);
        }

        // この RegisterAsBrowser プロパティは
        // COM のマネージラッパー
        return this.axIWebBrowser2.RegisterAsBrowser;
      }
      set
      {
        if ((this.axIWebBrowser2 == null))
        {
          throw new 
            AxHost.InvalidActiveXStateException(
              "RegisterAsBrowser",
              AxHost.ActiveXInvokeKind.PropertySet);
        }

        // この RegisterAsBrowser プロパティは
        // COM のマネージラッパー
        this.axIWebBrowser2.RegisterAsBrowser = value;
      }
    }

    // シンクオブジェクトへの参照
    private MyWebBrowserEventSink sink;

    // HTTP 通信の cookie とは関係ないので注意
    private AxHost.ConnectionPointCookie cookie;

    // シンクをサブスクライバ・リストに追加
    [PermissionSetAttribute(SecurityAction.LinkDemand, 
      Name="FullTrust")]
    protected override void CreateSink() 
    {
      base.CreateSink();

      if ((this.axIWebBrowser2 == null)) 
      {
        throw new AxHost.InvalidActiveXStateException(
            "CreateSink",
            AxHost.ActiveXInvokeKind.MethodInvoke);
      }

      this.sink = new MyWebBrowserEventSink(this);
      this.cookie = new AxHost.ConnectionPointCookie(
                          this.axIWebBrowser2,
                          this.sink, 
                          typeof(DWebBrowserEvents2));
    }

    // シンクのサブスクライブを解除
    [PermissionSetAttribute(SecurityAction.LinkDemand, 
      Name="FullTrust")]
    protected override void DetachSink() 
    {
      if (cookie != null) 
      {
        this.cookie.Disconnect();
        this.cookie = null;
      }
      base.DetachSink();
    }

    // NewWindow2 イベントの定義
    public event NewWindow2EventHandler NewWindow2;

    // .NET 側の NewWindow2 イベントを発動するメソッド
    protected virtual void 
      OnNewWindow2(NewWindow2EventArgs e) 
    {
      if ((this.NewWindow2 != null)) 
      {
        this.NewWindow2(this, e);
      }
    }

    // コネクションポイントからの呼び出しを受け取る
    // クライアント・シンクのクラス定義
    [ClassInterface(ClassInterfaceType.None)]
    public class MyWebBrowserEventSink :
      StandardOleMarshalObject, DWebBrowserEvents2
    {
      private MyWebBrowser browser;

      public MyWebBrowserEventSink(MyWebBrowser browser)
      {
        this.browser = browser;
      }

      // COM ソースから発生したイベントから呼び出される
      // メソッド
      public void 
        NewWindow2(ref object ppDisp, ref bool cancel)
      {
        NewWindow2EventArgs e = 
          new NewWindow2EventArgs(ppDisp, cancel);

        // .NET 側の NewWindow2 イベントを発動
        this.browser.OnNewWindow2(e);

        ppDisp = e.PpDisp;
        cancel = e.Cancel;
      }
    }
  }

  // NewWindow2 イベントのハンドラのデリゲート
  public delegate void NewWindow2EventHandler(object sender, 
    NewWindow2EventArgs e);

  // NewWindow2 イベントハンドラ引数のクラス定義
  public class NewWindow2EventArgs : EventArgs
  {        
    public object PpDisp { get; set; }
    public bool Cancel { get; set; }
        
    public NewWindow2EventArgs(object ppDisp, bool cancel)
    {
      this.PpDisp = ppDisp;
      this.Cancel = cancel;
    }
  }

  // DWebBrowserEvents2 インターフェイスの NewWindow2 メ
  // ソッド、IWebBrowser2 インターフェイスの Application
  // プロパティと RegisterAsBrowser プロパティをインポー
  // ト(つまり、マネージラッパーをコンパイル時に生成)。
  // ComImport, InterfaceType, Guid 指定は必須らしい。
  [ComImport, 
  InterfaceType(ComInterfaceType.InterfaceIsIDispatch), 
  Guid("34A715A0-6587-11D0-924A-0020AFC7AC4D")]
  public interface DWebBrowserEvents2
  {
    [DispId(0xfb)]
    void NewWindow2(
      [In, Out, MarshalAs(UnmanagedType.IDispatch)]
      ref object ppDisp, 
      [In, Out, MarshalAs(UnmanagedType.VariantBool)] 
      ref bool Cancel);
  }

  [ComImport, 
  Guid("D30C1661-CDAF-11D0-8A3E-00C04FC9E26E"),
  InterfaceType(ComInterfaceType.InterfaceIsIDispatch), ]
  public interface IWebBrowser2
  {
    [DispId(200)]
    object Application
    {
      [return: MarshalAs(UnmanagedType.IDispatch)]
      get;
    }

    [DispId(0x228)]
    bool RegisterAsBrowser
    {
      get;
      set;
    }
  }
}

Form1.cs

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;

namespace WebBrowserExtended
{
  public partial class Form1 : Form
  {
    private MyWebBrowser browser;
        
    public Form1()
    {
      browser = new MyWebBrowser();
            
      InitializeComponent();
                        
      browser.Dock = DockStyle.Fill;
      this.Controls.Add(browser);
      browser.NewWindow2 += 
        new NewWindow2EventHandler(browser_NewWindow2);
    }

    private void button1_Click(object sender, EventArgs e)
    {
      browser.Navigate(
        "http://msdntestnew/159-HyperLinkToPdf.aspx");
    }

    private void browser_NewWindow2(object sender, 
      NewWindow2EventArgs e)
    {
      Form1 frmWB = new Form1();

      // WebBrowser.AttachInterfaces メソッドは Visible
      // プロパティを true にすると呼び出される。なので、
      // ここで設定しないと RegisterAsBrowser プロパティ、
      // Application プロパティで例外がスローされてしまう。
      frmWB.Visible = true;

      frmWB.browser.RegisterAsBrowser = true;
      e.PpDisp = frmWB.browser.Application;
    }
  }
}

Tags:

.NET Framework

SHDocVw.dll と AxSHDocVw.dll の作り方と使い方

by WebSurfer 2012年6月23日 16:11

ActiveX の WebBrowser コントロール (SHDocVw.dll) を .NET Windows Forms アプリで扱おうとしてかなりハマったので、再びそのようなことがないよう備忘録を残しておきます。

AxSHDocVw.dll を使用した Windows Forms アプリ

.NET Framework 2.0 以降では、SHDocVw.dll のマネージラッパーである WebBrowser コントロール が提供されていますので、.NET Windows Forms アプリで SHDocVw.dll を扱うことはあまりないかもしれませんが、公開されてないイベントを利用したい場合などは困ります。

マネージラッパーの WebBrowser を拡張する方法(Extended .NET 2.0 WebBrowser Control 参照)もあるようですが、今回の目的(target="_blank" のリンクをクリックすると、セッションを維持して、上の画像のように別の Form1 に表示)程度なら、直接 SHDocVw.dll を扱ったほうが簡単そうに思えたので、そうしてみました。

以下の記事を読む前に、shdocvw.dll コンポーネントに関する予備知識として、MSDN ライブラリの Internet Explorer のアーキテクチャ に目を通しておくことをお勧めします。

まず、Windows Forms アプリで ActiveX コントロールをホストするには、AxHost クラス から派生するラッパーコントロールを生成する必要があります。

それには、SDK に含まれている Aximp.exe (Windows フォーム ActiveX コントロールインポーター) を使って、ActiveX コントロール用の COM タイプライブラリに属する型定義を Windows フォームコントロールに変換します。

具体的には、以下の画像のようにコマンドプロンプトから aximp.exe を起動し、shdocvw.dll(ActiveX コントロール用の COM タイプライブラリ。即ち IE のコンポーネント)から、Microsoft Web Browser コントロール用の共通言語ランタイムプロキシ (SHDocVw.dll) および Windows フォームプロキシ (AxSHDocVw.dll) を作成します。

コマンドプロンプトから aximp.exe を起動し SHDocVw.dll と AxSHDocVw.dll を生成

紛らわしいのが、IE のコンポーネントの shdocvw.dll と、Aximp.exe が生成する共通言語ランタイムプロキシ SHDocVw.dll の名前が同じという点です。名前が同じなだけで、中身は別物ですので注意してください。実は、自分が最初にハマったのがここです。(笑)

共通言語ランタイムプロキシの SHDocVw.dll を生成するので、IE のコンポーネントの shdocvw.dll が存在するフォルダで作業すると、「AxImp Error: 出力ファイル 'SHDocVw.dll' への書き込みエラーです。」というエラーが出てうまくいきません。

オプションで /out: e:\AxSHDocVw.dll というように、プロキシを出力したいフォルダを指定し(この例では e:\)、ファイル名を AxSHDocVw.dll とすれば、以下の画像のように期待通りプロキシが生成されます。なお、この例では /source オプションを追加しているので、AxSHDocVw.dll のソースと PDB ファイルが追加されています。

生成された SHDocVw.dll と AxSHDocVw.dll

以上で Microsoft Web ブラウザコントロール用の共通言語ランタイムプロキシ (SHDocVw.dll) および Windows フォームプロキシ (AxSHDocVw.dll) が作成できました・・・が、実は、このように手動で作る必要はなかったのでした。(汗)

Visual Studio のウィザードで作ることが可能で、その方法は以下の通りです。

まず、Visual Studio のツールボックスに Microsoft Web Browser を追加します。

Visual Studio のツールボックスに Microsoft Web Browser を追加

ツールボックスの空白部分をクリックして出てくるダイアログで[アイテムの選択(I)...]をクリックして[ツールボックス アイテムの選択]ダイアログボックスを表示します。

上の画像のように、[COM コンポーネント]タブをクリックし、一覧から Microsoft Web Browser を探してチェックを入れます。

[OK]をクリックすると、ツールボックスに WebBrowser コントロールが追加されます("Microsoft Web Browser" というテキストで表示されます)。

ツールボックスから Microsoft Web Browser を Form にドラッグ&ドロップすると自動的に SHDocVw.dll と AxSHDocVw.dll 作って、参照設定に加えてくれます。(下の「2012/7/1 注記追加」を参照)

参照設定に追加された AxSHDocVw と SHDocVw

あとは、以下のような NewWindow2 イベントのハンドラのコードを記述すれば、target="_blank" のリンクをクリックすると新たに Form1 生成されて表示され、その中の axWebBrowser に pdf が表示されます。また、セッションも引き継がれます(IE7 以前のブラウザはダメかも・・・未検証です)。

private void axWebBrowser1_NewWindow2(object sender, 
    AxSHDocVw.DWebBrowserEvents2_NewWindow2Event e)
{
    // target="_blank" のリンクをクリックしたとき、
    // 以下のコードがないと、新たに IE が開きそこ
    // に pdf が表示される。セッションは切れる。

    // 以下のコードがあると、新たに Form1 が表示さ
    // れ、その中の axWebBrowser に pdf が表示され
    // る。セッションは引き継がれる。

    Form1 frmWB = new Form1();
    frmWB.axWebBrowser1.RegisterAsBrowser = true;
    e.ppDisp = frmWB.axWebBrowser1.Application;
    frmWB.Visible = true;
}

----- 2012/7/1 注記追加 -----

Visual Studio 2010 のツールボックスから Microsoft Web Browser を Form にドラッグ&ドロップすると自動的に生成されるプロキシ(ラッパー)の名前は、Interop.SHDocVw.dll と AxInterop.SHDocVw.dll になります。(aximp.exe を使って作ったものとは中身も少々違うようです)

SHDocVw.dll と AxSHDocVw.dll は何が違うかというと、AxSHDocVw.dll のソースを見ての想像ですが、前者が ActiveX コントロールのプロパティ、メソッドの COM ラッパー、後者がそれらの COM ラッパーと .NET アプリを仲介するためのプロパティ、メソッド、イベントを提供するクラスのようです。

例えば、AxSHDocVw.dll に Application というプロパティが以下のように定義されています。SHDocVw.dll には return this.ocx.Application; で使われている Application という COM ラッパーが定義されてるということのようです。

namespace AxSHDocVw {
  [System.Windows.Forms.AxHost.ClsidAttribute(
    "{8856f961-340a-11d0-a96b-00c04fd705a2}")]
  [System.ComponentModel.DesignTimeVisibleAttribute(true)]
  [System.ComponentModel.DefaultProperty("Name")]
  public class AxWebBrowser : System.Windows.Forms.AxHost {
        
    private SHDocVw.IWebBrowser2 ocx;
        
    private AxWebBrowserEventMulticaster eventMulticaster;
        
    private System.Windows.Forms.
              AxHost.ConnectionPointCookie cookie;
        
    public AxWebBrowser() : 
            base("8856f961-340a-11d0-a96b-00c04fd705a2") {
    }
    
    [System.ComponentModel.DesignerSerializationVisibility(
      System.ComponentModel.
      DesignerSerializationVisibility.Hidden)]
    [System.Runtime.InteropServices.DispIdAttribute(200)]
    public virtual object Application {
      get {
        if ((this.ocx == null)) {
          throw 
            new System.Windows.Forms.AxHost.
                InvalidActiveXStateException(
                  "Application", 
                  System.Windows.Forms.AxHost.
                    ActiveXInvokeKind.PropertyGet);
        }
        return this.ocx.Application;
      }
    }
    // ・・・中略・・・
  }
  // ・・・中略・・・
}

上記はプロパティの場合ですが、イベントの場合はもっと複雑で、AxSHDocVw.dll には、イベントの宣言、デリゲートの定義、イベントの引数クラスの定義、クライアント・シンクのクラス定義等々が含まれます。詳しくは別の記事 WebBrowser の拡張 に書きましたのでそちらを見てください。

Tags:

.NET Framework

長い日本語の添付ファイル名が文字化け

by WebSurfer 2011年10月30日 22:00

.NET Framework 4 の SmtpClient を使用すると、添付ファイルに長い日本語のファイル名を使うと文字化けするという問題があります。(.NET 3.5 以前では問題なし)

原因はファイル名が二重エンコーディングされてしまうからで、そのメカニズムは このブログ に詳しく書いてあります。

この問題は、米国の MSDN フォーラムでも報告されており、原因が .NET Framework 4 のバグであることは Microsoft も認識しているようです。

その時の Microsoft の人の回答(2010/7/23 付けの記事参照)では、次のリリースを待つか、短い名前にするしかないということでした。

とは言え、現実に問題ない PC があるのは確かですので(自分の開発マシンがそうです)、何らかの更新プログラムがリリースされていて、問題が発生しない PC には適用されているはずと思って調べたら、Connect のページ に、Hotfix KB2183292 によってファイル名の二重にエンコーディングの問題も解決されるとの話がありました。

試しに、二重エンコーディングの問題の出ていた PC に、Hotfix KB2183292 をダウンロードしてインストールすると問題が解決しました。

Windows Update でインストールされる .NET Framework 4 用の更新プログラム で Hotfix KB2183292 が置き換えられると書いてありましたので、この更新プログラムによって二重エンコーディングの問題も解決されているのかと思いましたが、どうもそうではないようです。

実際、.NET Framework 4 用の更新プログラム がインストールされている PC でも二重エンコーディングの問題が発生しました。

というわけで、Windows Update で解決されているのではなさそうなので、次期リリース(.NET 4.5 ?)でこの問題が解決されるまでは、長い日本語のファイル名を添付ファイルに使う場合は、何らかの対策が必要なようです。

問題は、Attachment コンストラクタ (String, ContentType) で長い日本語のファイル名を使用すると二重エンコーディングされるということなので、代わりに Attachment コンストラクタ (Stream, ContentType) を使用し、Content-Disposition の filename の方にファイル名を設定することで問題を回避できるはずです。

以下のような感じです。

private string EncodeHeader(string str, Encoding enc)
{
  string ret = 
    System.Convert.ToBase64String(enc.GetBytes(str));
  ret = string.Format("=?{0}?B?{1}?=", enc.BodyName, ret);
  return ret;
}

private void button2_Click(object sender, EventArgs e)
{
  System.Text.Encoding enc = 
    System.Text.Encoding.GetEncoding("utf-8");

  string mailAddress = this.userName + "@" + this.host;

  MailAddress to = 
    new MailAddress(mailAddress, EncodeHeader("日本花子", enc));
  MailAddress from = 
    new MailAddress(mailAddress, EncodeHeader("日本太郎", enc));

  MailMessage mailMessage = new MailMessage(from, to);

  mailMessage.Subject = textBox1.Text;
  mailMessage.Body = textBox2.Text;

  // 添付ファイル
  file = "テスト用の長めのファイル名です.pdf";
  FileStream fs = 
    new FileStream(file, FileMode.Open, FileAccess.Read);

  Attachment data = 
    new Attachment(fs, MediaTypeNames.Application.Octet);
  ContentDisposition disposition = data.ContentDisposition;
  disposition.FileName = 
    EncodeHeader(System.IO.Path.GetFileName(file), enc);

  mailMessage.Attachments.Add(data);

  SmtpClient smtpClient = new SmtpClient();
  smtpClient.Host = host;
  smtpClient.Port = portSmtp;

  try
  {
    smtpClient.Send(mailMessage);
    MessageBox.Show("Your mail was sent.");
  }
  catch (SmtpException ex)
  {
    SmtpStatusCode status = ex.StatusCode;
    MessageBox.Show("SmtpException: " + ex.Message + 
      "\nSmtpStatusCode: " + status.ToString());
  }
  catch (Exception ex)
  {
    MessageBox.Show("Exception: " + ex.Message);
  }
  finally
  {
    mailMessage.Dispose();
  }
}

こうすると、以下のように Content-Type の name は application/octet-stream となり、Content-Disposition の filename にファイル名が設定され、正しいファイル名を取得できます(メーラーによってはダメかもしれませんが)。

Content-Type: application/octet-stream;
 name="application/octet-stream"
Content-Transfer-Encoding: base64

Content-Disposition: attachment;
 filename="=?utf-8?B?44OG44...BkZg==?="

なお、一旦 Cotent-Type の name が二重エンコーディングされたものになってしまうと、Content-Disposition の filename にファイル名を設定しても、何故かそちらは無視されてしまいます。メーラーにもよると思いますが Windows メールはダメでした。

という訳で、上記のコード方法が次期バージョンがリリースされるまでの Workaround としては適当なようです(ただし、filename のところが、一行で 76 文字の制限を越えてしまいますので、メーラーによっては対応が必要かもしれません)。

Tags: ,

.NET Framework

About this blog

2010年5月にこのブログを立ち上げました。主に ASP.NET Web アプリ関係の記事です。

Calendar

<<  2024年3月  >>
252627282912
3456789
10111213141516
17181920212223
24252627282930
31123456

View posts in large calendar