QA@IT
この質問・回答は、@IT会議室からインポートされたものです。

複数の EXCEL.EXE が動いている時に、ひとつの Excel.Application を GetObject で特定して C# で操作するには?

Windows XP + Excel 2007 + Visual C# 2008 Express Edition を使っています。フォームを持った、普通の Windows Application を開発しています。

このアプリケーションの起動前から、すでに開かれた状態になっている Excel の Workbook を、このアプリケーションで操作したいと思っています。
ネットでいろいろ調べると、雰囲気的に、つぎのやりかたでできることは分かりました。

Microsoft.Office.Interop.Excel.Application app = (Microsoft.Office.Interop.Excel.Application)Microsoft.VisualBasic.Interaction.GetObject(null, "Excel.Application");

実際にこのやりかたで、目的とすることはほぼできました。
しかし、このやりかただと、題名に書いたような問題があります。複数の Excel のインスタンスが起動している場合です。すなわち、タスクマネージャーで EXCEL.EXE のプロセスが複数存在する場合です。
この場合、GetObject を使っても、どのインスタンスが取得できるかが運任せになるようです(若いプロセスが使われる?)。私としては複数のインスタンスが存在するならば GetObject も、全部のインスタンスを返してくれれば良いと思うのですが、どうもそういう仕様にはなっていないようです。

この問題を解決するには、アプリケーションのコードをどう直せば良いでしょうか?予想としては GetObject では無理であり、とたんに難しくなるのかな、と思っています。

なお、これは余談に近くなるのですが、そもそも GetObject というのは大昔の VB の頃からある関数ですが、仕様がイマイチ良く分からず、たとえ VB.NET や Microsoft.VisualBasic.Interaction の中に存在していたとしても、なんとなくあんまり使いたくないなあ、というのが本音です。

投稿者: unibon

回答

以下に述べることは質問ではなく、気付いたことの紹介です。

> pMonikers[0].BindToObject(pBindCtx, null, ref IID_IUnknown, out obj);

私はプログラムの動作の仕組みを分からないまま使わせてもらっているのですが、この BindToObject の実行時に例外が起こることがあります(もとのプログラムで Exception を catch しているのもそのためだと思います)。

ひとつは System.ArgumentException です。これは私の環境だと常に起こります。

もうひとつは System.IO.FileNotFoundException です。これは起こるときと起こらないときがあり、起こるときは画面に「COM コンポーネントのインストール中 xxx.xxx.xxx.xxx」というダイアログウィンドウが1秒間程度表示され、その後すぐに消えます。伏せ字の部分は IP アドレスのようです。
これがどういうときに起こるかを調べてみると、Windows Media Player 11 を動かしているときに起こるようです。IP アドレスは Microsoft 関連のところのようです。

最初、原因が分からなくて、画面に突然 IP アドレスとともに「インストール」などという文字が出て来たので、ちょっとびっくりしました。

投稿者: unibon

編集 履歴 (0)

スクリプトプログラマさん
> ROT Running Object Tableを見ればよいと思います。
> .NETにもその関係のメンバが用意されてます。

ありがとうございます。返事が遅くなりすみません。
ROT がぜんぜん分からなくて、つまづいていました。昔から Visual Studio などに付属する ROT Viewer などは見ていたのですが、いまだに仕組みが良く分かっていません。

Tdnr_Symさん
> スクリプトプログラマさんのおっしゃるように、ROT(Running Object Table)から、Excelのインスタンス(というよりワークブック)をすべて取得するには
> こんな感じになるでしょうか。

ありがとうございます。このまま動かすことができ、GetObject の代わりとして使うことができました。

GetObject だと、事前に指定したファイル名どおりのファイルを Excel で開いていないと、勝手に EXCEL.EXE が起動し、しかもデフォルトだと非表示で起動するらしく、そうなるたびにいちいちタスクマネージャーでプロセスを終了させていました。
ROT を使うやりかただと、Excel の Workbook が複数あっても、列挙された Workbook にアクセスしてどれを使うかをユーザーが選ぶこともできるので、とても楽になりました。

投稿者: unibon

編集 履歴 (0)

こんばんは。

すでに解決されているようなので、蛇足ですが

私としては複数のインスタンスが存在するならば GetObject も、全部のインスタンスを返してくれれば良いと思うのですが、どうもそういう仕様にはなっていないようです。

スクリプトプログラマさんのおっしゃるように、ROT(Running Object Table)から、Excelのインスタンス(というよりワークブック)をすべて取得するには
こんな感じになるでしょうか。

using System;
using System.Runtime.InteropServices;
using System.Runtime.InteropServices.ComTypes;
using Microsoft.Office.Interop.Excel;
using System.Collections.Generic;
using System.Diagnostics;

namespace ConsoleApplication1
{
    class Program
    {

        static Guid IID_IUnknown = new Guid("00000000-0000-0000-C000-000000000046");
        [DllImport("ole32.dll")]
        static extern int CreateBindCtx(uint reserved, out IBindCtx ppbc);

        static void Main(string[] args)
        {
            Workbook[] books = GetOpenedExcelBooks();

            foreach (Workbook book in books)
            {
                // ブック名を出力
                Console.WriteLine(book.Name);

                Marshal.ReleaseComObject(book);
            }
        }

        static Workbook[] GetOpenedExcelBooks()
        {
            List<Workbook> arr = new List<Workbook>();

            // IBindCtx
            IBindCtx pBindCtx = null;
            CreateBindCtx(0, out pBindCtx);

            // IRunningObjectTable
            IRunningObjectTable pROT = null;
            pBindCtx.GetRunningObjectTable(out pROT);

            // IEnumMoniker
            IEnumMoniker pEnumMoniker = null;
            pROT.EnumRunning(out pEnumMoniker);

            pEnumMoniker.Reset();

            for (; ; )
            {
                // IMoniker
                IMoniker[] pMonikers = { null };

                IntPtr fetched = IntPtr.Zero;
                if (pEnumMoniker.Next(1, pMonikers, fetched) != 0)
                {
                    break;
                }

                // For Debug
                string strDisplayName;
                pMonikers[0].GetDisplayName(pBindCtx, null, out strDisplayName);
                Debug.WriteLine(strDisplayName);


                object obj = null;
                Workbook pBook = null;
                try
                {
                    pMonikers[0].BindToObject(pBindCtx, null, ref IID_IUnknown, out obj);

                    pBook = obj as Workbook;
                    if (pBook != null)
                    {
                        arr.Add(pBook);
                    }
                }
                catch (Exception)
                {
                }
                finally
                {

                    Marshal.ReleaseComObject(pMonikers[0]);
                }

            }

            Marshal.ReleaseComObject(pEnumMoniker);
            Marshal.ReleaseComObject(pROT);
            Marshal.ReleaseComObject(pBindCtx);

            return arr.ToArray();
        }

    }
}

投稿者: Tdnr_Sym

編集 履歴 (0)

mitchinさん
> このような要件でしたらブックはアプリからのみ起動(アプリも起動しっ放し)するようにし、アプリを終了する時はブックを閉じるようにした方がいいのではと思います。
> つまりアプリとブックを 1セットとして考えます。

多層(Multi-tier な)アプリケーションということですよね。ごもっともだと思います。
ただ、私が今回、Excel を使う背景が、やむを得ない事情で使っている(外部からの理由で強制的に使わざるを得ない)ということもあり、これを多層化するとさらに複雑になってしまうこともあり、今回は見送っています。

デューンさん、こんにちは。
私の憶測でしかありませんがひょっとしたら Excel が特殊なのかもしれません。時間ができたらもっとシンプルな OLE サーバーで試してみたいと思います。

投稿者: unibon

編集 履歴 (0)

なぜか両方指定すると例外が起こってしまいます。クラス名に "Excel.Application" や "Excel.Workbook" を指定してみましたが、ダメでした。GetObject の仕様も難解です。

以下を参考に、二つ指定しても動くとは思いますと書いたのですが、
ひょっとして、以下のサンプルもすでに起動されてない場合だけなのかもしれませんね・・・
http://msdn.microsoft.com/ja-jp/library/e9waz863(VS.80).aspx

投稿者: デューン

編集 履歴 (0)

> Excel のマクロが動いていて、セルの値が時々刻々と自動更新されるブックがあり、そのセルの値を参照したいのです。そのブックをローカルコンピューター内の「サーバー」として使いたい感じです。
> なお、前回 new してもいいと私は書きましたが、やっぱり Excel のブックは「サーバー」として動かしっぱなしにしたいので、要求仕様としてはアプリケーション(これが「クライアント」になる)の起動・終了とは無関係にブックは開きっぱなしにしたいので、new ではなく GetObject のように、開いているブックに接続するほうが良かったです。

このような要件でしたらブックはアプリからのみ起動(アプリも起動しっ放し)するようにし、アプリを終了する時はブックを閉じるようにした方がいいのではと思います。
つまりアプリとブックを 1セットとして考えます。

投稿者: mitchin

編集 履歴 (0)

ROT Running Object Tableを見ればよいと思います。
.NETにもその関係のメンバが用意されてます。

投稿者: スクリプトプログラマ

編集 履歴 (0)

mitchinさん
> マクロのセキュリティレベルを「中」にしておけば、プログラムから開いてもダイアログが表示されるので、手動で開くのと同じです。

Excel 2007 を使っているのですが、セキュリティーの設定が私には難しく、ちょっとやってみたのですが、なぜかうまく動かせませんでした。前回書きませんでしたが、実は、後述のような「サーバー」としても動かしたいという思いもあり、new する方法はちょっと後回しにしてしまいましたが、またいつか必要になることなので、近いうちに調べてみたいと思います。

mitchinさん
> そもそも外部から与えられたほとんど手を入れることのできないブックをアプリ側からどんな操作をしたいのでしょうか?

Excel のマクロが動いていて、セルの値が時々刻々と自動更新されるブックがあり、そのセルの値を参照したいのです。そのブックをローカルコンピューター内の「サーバー」として使いたい感じです。
なお、前回 new してもいいと私は書きましたが、やっぱり Excel のブックは「サーバー」として動かしっぱなしにしたいので、要求仕様としてはアプリケーション(これが「クライアント」になる)の起動・終了とは無関係にブックは開きっぱなしにしたいので、new ではなく GetObject のように、開いているブックに接続するほうが良かったです。(前回は、このあたりが自分でもはっきりしていませんでした。)

デューンさん
> GetObjectのクラス名はパスを指定しないときは必須だったと思います。
> クラス名とパスを両方指定しても動くとは思います。
> (指定しないとExcel以外のCOMコンポーネントで開いている場合もとれちゃうかもしれませんね。)

なぜか両方指定すると例外が起こってしまいます。クラス名に "Excel.Application" や "Excel.Workbook" を指定してみましたが、ダメでした。GetObject の仕様も難解です。

デューンさん
> いじわるではないのですが、Excelを複数起動して、同じファイルを開くことも可能です。(2個目以降は読み取り専用になりますが)

個人的にはこのようにファイル依存なのがちょっと違和感があります。ファイルがなくても「サーバー」として動いてくれても良さそうな気がします。もっとも、OLE の経緯を考えるとファイルありきなのかなとも思います。

投稿者: unibon

編集 履歴 (0)

GetObjectのクラス名はパスを指定しないときは必須だったと思います。
クラス名とパスを両方指定しても動くとは思います。
(指定しないとExcel以外のCOMコンポーネントで開いている場合もとれちゃうかもしれませんね。)

いじわるではないのですが、Excelを複数起動して、同じファイルを開くことも可能です。(2個目以降は読み取り専用になりますが)

・・・まぁそのケースはエラーでいいと思いますけど。

投稿者: デューン

編集 履歴 (0)

マクロのセキュリティレベルを「中」にしておけば、プログラムから開いてもダイアログが表示されるので、手動で開くのと同じです。
また、読み取り専用で開くこともできますし、起動中はアプリ側の操作をできなくすることもできます。

そもそも外部から与えられたほとんど手を入れることのできないブックをアプリ側からどんな操作をしたいのでしょうか?

投稿者: mitchin

編集 履歴 (0)

私は GetObject の使い方をあまり良く知らなかったのですが、つぎのようにすると、私の目的は達成できるようです。

Microsoft.Office.Interop.Excel.Workbook workbook = (Microsoft.Office.Interop.Excel.Workbook)Microsoft.VisualBasic.Interaction.GetObject(@"C:\foo\bar\hoge.xlsb", null);

(ちなみにこの場合はキャストは Excel.Application ではなく Excel.Workbook になります。)

これは大昔の VB の GetObject の仕様と変わっていないと思いますが、要はファイル名を指定して、逆にクラス名は指定しないやりかたです。
ファイル名を指定、というのがいかにも昔の OLE という感じがしますが、これでファイル名を頼りにして、開かれているブックを特定できるようです。(そのファイル名のブックが開かれていないと新たにオープンする?)

すみません、私はなんだか難しく考えすぎていたみたいです。とりあえず、いったんここでまとめのつもりであり、その旨報告させていただいた次第です。

投稿者: unibon

編集 履歴 (0)

mitchinさん、こんにちは。コメントありがとうございます。

> 既に開かれているブックがアクティブならプロセスを特定することはできると思いますが、そこから Excel の操作ができるかどうかは判りません。

特定したプロセスから Excel の Application のインスタンスを辿るのがとても難しそうに感じます。
たとえば、
http://homepage1.nifty.com/MADIA/vb/vb_bbs/200607/200607_06070021.html
などを見てみたのですが、やはり難しいように思います。

そもそもの素朴な疑問なのですが、なぜ動いている COM のインスタンスを列挙したいだけなのに、こんなに難しい話になってしまうのか、ということがあります。

> 別途 Excel を起動して操作したいブックを開くのであれば Processクラスを使って管理できます。
> Excel のインスタンスは New Microsoft.Office.Interop.Excel.Application() で作ります。

今のところ、すでに開いているブックを対象にしたいと思っています。ただ、どうしても無理そうならば、new することも検討したいと思います。しかし、new する場合、マクロやアドインを有効にする方法が良く分かっていなくて、「じゃあ、手動で開いてマクロやアドインが有効になっているブックに、あとから C# で GetObject でアタッチするほうが簡単だな。」と思って、現在の方法を模索しています。マクロやアドインの他にも、なにか問題が出そうな気もして、できるなら new ではなくアタッチしたいと思っています。

(なお、対象となる Excel のブックは、外部から与えられるだけのものであり、ほとんど手を入れることができないものです。Excel 以外の選択肢を探す、という自由度もまったく与えられていません。)

投稿者: unibon

編集 履歴 (0)

Processクラスを使うといいかもしれません。

Process.GetProcessesByName("EXCEL")で Excel のプロセスの配列を取得できます。
Id プロパティや MainWindowTitle プロパティなどを調べてみてください。
既に開かれているブックがアクティブならプロセスを特定することはできると思いますが、そこから Excel の操作ができるかどうかは判りません。

別途 Excel を起動して操作したいブックを開くのであれば Processクラスを使って管理できます。
Excel のインスタンスは New Microsoft.Office.Interop.Excel.Application() で作ります。

投稿者: mitchin

編集 履歴 (0)
ウォッチ

この質問への回答やコメントをメールでお知らせします。