none
遅延バインディングでExcelのイベントを取得したい RRS feed

  • 質問

  • こんにちは。
    Excelの遅延バインディングでイベントを取得する方法について質問させてください。

    具体的には、下記と同じことを遅延バインディングで行うのはどうしたらよいでしょうか?

    Excel.Application excel = new Excel.Application();
    excel.Visible = true;
    excel.WorkbookBeforeClose += new Excel.AppEvents_WorkbookBeforeCloseEventHandler(delegate
    {
            MessageBox.Show("WorkbookBeforeClose");
    });


    なお、イベントの取得以外は遅延バインディングでExcelを操作できています。
    よろしくお願いします。

    2009年3月13日 1:47

回答

  • イベントインターフェースからのコールバックを受けるためのIIDやDispIDを定義してやらないとイベントは拾えないかも。

    1   [ComImport, ComVisible(true), Guid("00024413-0000-0000-C000-000000000046"),] 
    2    public interface IApplicationEvents 
    3    { 
    4        [PreserveSig, MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime), DispId(0x622)] 
    5        void WorkbookBeforeClose([In, MarshalAs(UnmanagedType.Interface)] object Wb, [In, Out] ref bool Cancel); 
    6    } 
    7    [ComVisible(true),ClassInterface(ClassInterfaceType.None)] 
    8    public class ApplicationEvents_SinkHelper : IApplicationEvents  
    9    { 
    10        public void SetEvent(object excelapp) 
    11        { 
    12            System.Runtime.InteropServices.ComTypes.IConnectionPointContainer icpc; 
    13            System.Runtime.InteropServices.ComTypes.IConnectionPoint icp; 
    14 
    15            icpc = (System.Runtime.InteropServices.ComTypes.IConnectionPointContainer)excelapp; 
    16 
    17            Type typeiae = typeof(IApplicationEvents); 
    18            Attribute[] atributes =(Attribute[]) typeiae.GetCustomAttributes(typeof(GuidAttribute), false); 
    19            string iid = ((GuidAttribute)atributes[0]).Value; 
    20            Guid guid = new Guid(iid); 
    21 
    22            //指定したインターフェースIDのインターフェースがあるか探す 
    23            icpc.FindConnectionPoint(ref guid, out icp); 
    24            if (icp != null
    25            { 
    26                icp.Advise(thisout cookie); 
    27            } 
    28        } 
    29        private int cookie; 
    30
    31        #region IApplicationEvents メンバ 
    32 
    33        void IApplicationEvents.WorkbookBeforeClose(object Wb, ref bool Cancel) 
    34        { 
    35            WorkBookBefoeCloseEventArgs e = new WorkBookBefoeCloseEventArgs(Wb,Cancel); 
    36            OnWorkbookBeforClose(e); 
    37            Cancel = e.Cancel; 
    38        } 
    39 
    40        protected virtual void OnWorkbookBeforClose(WorkBookBefoeCloseEventArgs e) 
    41        { 
    42            if (WorkbookBeforeClose != null
    43            { 
    44                WorkbookBeforeClose(this, e); 
    45            } 
    46        } 
    47 
    48        public event EventHandler<WorkBookBefoeCloseEventArgs> WorkbookBeforeClose; 
    49 
    50        public class WorkBookBefoeCloseEventArgs : CancelEventArgs 
    51        { 
    52            public WorkBookBefoeCloseEventArgs(object wb, bool cancel) 
    53                : base(cancel) 
    54            { 
    55                WorkBook = wb; 
    56            } 
    57 
    58            public object WorkBook { getprivate set; } 
    59        } 
    60
    61        #endregion 
    62    } 


    COMオブジェクトが確実にReleaseComObjectされるようにしたくて遅延バインディングのラッパクラスを作ったことがありますが、結局はイベント関係のインタフェース定義を大量に書かなければなりませんでした。
    #このようなコードを動的に生成させることが出来れば真の遅延バインディングが出来るのですけどね
    • 回答としてマーク umeo 2009年3月14日 3:10
    2009年3月13日 13:10

すべての返信

  • イベントインターフェースからのコールバックを受けるためのIIDやDispIDを定義してやらないとイベントは拾えないかも。

    1   [ComImport, ComVisible(true), Guid("00024413-0000-0000-C000-000000000046"),] 
    2    public interface IApplicationEvents 
    3    { 
    4        [PreserveSig, MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime), DispId(0x622)] 
    5        void WorkbookBeforeClose([In, MarshalAs(UnmanagedType.Interface)] object Wb, [In, Out] ref bool Cancel); 
    6    } 
    7    [ComVisible(true),ClassInterface(ClassInterfaceType.None)] 
    8    public class ApplicationEvents_SinkHelper : IApplicationEvents  
    9    { 
    10        public void SetEvent(object excelapp) 
    11        { 
    12            System.Runtime.InteropServices.ComTypes.IConnectionPointContainer icpc; 
    13            System.Runtime.InteropServices.ComTypes.IConnectionPoint icp; 
    14 
    15            icpc = (System.Runtime.InteropServices.ComTypes.IConnectionPointContainer)excelapp; 
    16 
    17            Type typeiae = typeof(IApplicationEvents); 
    18            Attribute[] atributes =(Attribute[]) typeiae.GetCustomAttributes(typeof(GuidAttribute), false); 
    19            string iid = ((GuidAttribute)atributes[0]).Value; 
    20            Guid guid = new Guid(iid); 
    21 
    22            //指定したインターフェースIDのインターフェースがあるか探す 
    23            icpc.FindConnectionPoint(ref guid, out icp); 
    24            if (icp != null
    25            { 
    26                icp.Advise(thisout cookie); 
    27            } 
    28        } 
    29        private int cookie; 
    30
    31        #region IApplicationEvents メンバ 
    32 
    33        void IApplicationEvents.WorkbookBeforeClose(object Wb, ref bool Cancel) 
    34        { 
    35            WorkBookBefoeCloseEventArgs e = new WorkBookBefoeCloseEventArgs(Wb,Cancel); 
    36            OnWorkbookBeforClose(e); 
    37            Cancel = e.Cancel; 
    38        } 
    39 
    40        protected virtual void OnWorkbookBeforClose(WorkBookBefoeCloseEventArgs e) 
    41        { 
    42            if (WorkbookBeforeClose != null
    43            { 
    44                WorkbookBeforeClose(this, e); 
    45            } 
    46        } 
    47 
    48        public event EventHandler<WorkBookBefoeCloseEventArgs> WorkbookBeforeClose; 
    49 
    50        public class WorkBookBefoeCloseEventArgs : CancelEventArgs 
    51        { 
    52            public WorkBookBefoeCloseEventArgs(object wb, bool cancel) 
    53                : base(cancel) 
    54            { 
    55                WorkBook = wb; 
    56            } 
    57 
    58            public object WorkBook { getprivate set; } 
    59        } 
    60
    61        #endregion 
    62    } 


    COMオブジェクトが確実にReleaseComObjectされるようにしたくて遅延バインディングのラッパクラスを作ったことがありますが、結局はイベント関係のインタフェース定義を大量に書かなければなりませんでした。
    #このようなコードを動的に生成させることが出来れば真の遅延バインディングが出来るのですけどね
    • 回答としてマーク umeo 2009年3月14日 3:10
    2009年3月13日 13:10
  • ある意味質問と無関係なのですが、「遅延バインディング」とは何を指すのでしょうか?

    もともとCOMは遅延バインディングで、Excelがインストールされていない環境でumeoさんのコードを実行するとプログラムそのものは実行でき、
    new Excel.Application()の部分で例外(COMExceptionでしたっけ?)が発生したと思います。
    「遅延バインディング」によってどのようなことを望んでいるのかわかりませんでした。
    もしかして、C# 4.0のdynamicキーワードが該当しますでしょうか?

    gekkaさんの「COMオブジェクトが確実にReleaseComObjectされるようにしたくて」も…やはりできないものなのでしょうか?
    ここまでCOMを扱ったことがないのでよくわかりませんでした。

    それはそうと、tlbimpの出力をDLLだけでなくC#などでもほしいですよね。生成されたDLLのメタデータを参照しながらコード生成しなさいってことなのかな。

    2009年3月14日 2:45
  • gekkaさん、正に望んでいた通りのご回答、
    どうもありがとうございました。

    これで先に進めそうです。
    本当にありがとうございました。
    2009年3月14日 3:11
  •  佐祐理さん、こんにちは。

    佐祐理さん の発言:

    「遅延バインディング」によってどのようなことを望んでいるのかわかりませんでした。

    対象のExcelのバージョンが2000~2007と複数あるためです。

    2009年3月14日 3:36
  • 佐祐理 さん の発言:

    もともとCOMは遅延バインディングで

    アーリーバインディング(事前バインディング)とレイトバインディング(遅延バインディング)があるはずです。

    C++でも、コンパイル時点で#importしておくと、そのメソッドの有無をコンパイラが知ることができ、未知のメソッドを呼び出そうとコードを書いてもエラーになります。コンパイル時点で型の概要が分かっていることをアーリーバインディングとみなして差し支えはないかと。
    # C#では参照設定を追加しておくことが相当する。

    対して、レイトバインディングはIDispatchを実装したクラスに対して、DISPIDで呼び出しをかけます。コンパイラはIDispatchの呼び出しのみ解釈し、その結果の呼び出しの正当性についてはチェックできないため、無茶な呼び出しであってもコンパイルエラーにならず、実行時にそのDISPIDがないというエラーになるという仕組みです。
    # C#ではリフレクションを使って、InvokeMember等をすることに相当する。また、C# 4.0のdynamicキーワードでも良い。

    佐祐理 さん の発言:

    「遅延バインディング」によってどのようなことを望んでいるのかわかりませんでした。

    既に回答がついていますが、アーリーバインディングではバージョンの互換性に問題が生じるとされています。
    このあたりはKBにも記載があります。

    http://support.microsoft.com/kb/302902/ja

    佐祐理 さん の発言:

    gekkaさんの「COMオブジェクトが確実にReleaseComObjectされるようにしたくて」も…やはりできないものなのでしょうか?
    ここまでCOMを扱ったことがないのでよくわかりませんでした。

    参照設定の追加やtlbimpの結果のラッパーDLLが提供するクラスには、IDisposableやファイナライザが実装されていません。
    "確実に" 実行するためには、try-finallyでReleaseComObjectをすればよいのですが、1つ1つのオブジェクトに対してこれを書くのは手間がかかるという状態です。

    これについてはHongliangさんのblogが参考になるかと思います。
    http://hongliang.seesaa.net/article/14551716.html
    解決した場合は、参考になった返信に「回答としてマーク」のボタンを利用して、回答に設定しましょう(複数に設定できます)。
    2009年3月15日 1:27
    モデレータ
  • Azulean の発言:

    アーリーバインディング(事前バインディング)とレイトバインディング(遅延バインディング)があるはずです。

    C++でも、コンパイル時点で#importしておくと、そのメソッドの有無をコンパイラが知ることができ、未知のメソッドを呼び出そうとコードを書いてもエラーになります。コンパイル時点で型の概要が分かっていることをアーリーバインディングとみなして差し支えはないかと。

    対して、レイトバインディングはIDispatchを実装したクラスに対して、DISPIDで呼び出しをかけます。コンパイラはIDispatchの呼び出しとして解釈しないため、無茶な呼び出しであってもエラーにならず、実行時にそのDISPIDがないというエラーになるという仕組みです。

    なるほどっ。ところで今回、Excel 2000~2007を制御するためのレイトバインディングとのことですが、Excelにはもともとバージョン間に互換があるはずです。
    理想論としては、完全に互換があればExcel 2000用に書いたコードが2007でも動作するはずです。
    互換のない部分はどうするのか、そのバージョンだけ違うコードを書くことになるでしょう。ということはどういうことでしょう?
    あらかじめ互換のある部分とない部分を把握し各バージョン向けにコードを書く、それはすなわちアーリーバインディングなのではないでしょうか?

    で、実践しようとするとeventだけどうにもならないということでこの質問になったんですね。f(^^;
    一応、Office 2007 PIAでExcel 2000が起動できることまでは確認しました。


    参照設定の追加やtlbimpの結果のラッパーDLLが提供するクラスには、IDisposableやファイナライザが実装されていません。
    "確実に" 実行するためには、try-finallyでReleaseComObjectをすればよいのですが、1つ1つのオブジェクトに対してこれを書くのは手間がかかるという状態です。

    こちらもイベント周りが難しいとこいうことでしょうか? Excel本体に関して言うと、Dispose()の手抜き実装ですが
    1 class Application: Excel.ApplicationClass, IDisposable { 
    2   public void Dispose() { 
    3     Marshal.ReleaseComObject( this ); 
    4   } 
    5
    で閉じることはできました。他のオブジェクトのCOM、.NET双方の参照カウントがぐちゃぐちゃになりそうですね…。
    2009年3月15日 3:24
  • 佐祐理 さん の発言:

    理想論としては、完全に互換があればExcel 2000用に書いたコードが2007でも動作するはずです。
    互換のない部分はどうするのか、そのバージョンだけ違うコードを書くことになるでしょう。ということはどういうことでしょう?
    あらかじめ互換のある部分とない部分を把握し各バージョン向けにコードを書く、それはすなわちアーリーバインディングなのではないでしょうか?

    アーリーバインディング・レイトバンディングと、互換性を把握してコードを書け分けることに直接の関係はありません。
    その型の詳細をコンパイラが知っているかどうかの違いですので、コードを書く人が知っているかどうかではありません。

    佐祐理 さん の発言:

    一応、Office 2007 PIAでExcel 2000が起動できることまでは確認しました。
    起動自体はできるとは思います。
    しかし、細部の型やメンバを使い始めると、2000にもあるかどうか、2000とDISPID等が変わっていないかどうかを気にする必要があるかもしれません。

    正直なところ、具体的にどこがどう引っかかるのかと説明できるほど、Officeを操作するコードを書いていないので、具体例を挙げられません。
    KBに上がっているという事実をもって、避けた方が無難だという裏付けの薄い主張になっています。

    佐祐理 さん の発言:

    こちらもイベント周りが難しいとこいうことでしょうか? Excel本体に関して言うと、Dispose()の手抜き実装ですが
    1 class Application: Excel.ApplicationClass, IDisposable { 
    2   public void Dispose() { 
    3     Marshal.ReleaseComObject( this ); 
    4   } 
    5
    で閉じることはできました。

    私が書きました内容と、イベントとは直接関係ありません。
    COMクラスの解放コードが面倒というのが主張で、ご提示頂いた手法でもラップクラスをたくさん書くことになると思うとぞっとします。
    似たような名前のクラスがあると、間違えて実体を使ってしまうといったバグが出てくるかもしれないという意味でもちょっと怖いですね。


    解決した場合は、参考になった返信に「回答としてマーク」のボタンを利用して、回答に設定しましょう(複数に設定できます)。
    2009年3月15日 7:21
    モデレータ