Office 製品をスクリプトから使うこと by kanegon Microsoft Office を外部からスクリプトで制御する場合の注意点、および、その基盤 技術であるオートメーション機能について検証してみる。 なお、この文書では機能の検証を目的とするため、操作の説明などはほとんどない。 読むにあたってはスクリプトの起動方法など、基本的なことは知ってたほうが良いかも。 ----------------------------------------------------------- 目次 ----------------------------------------------------------- 1. COM って何(その1) 2. Office と COM 3. COM って何(その2) 4. COM の操作の例 5. 参照カウンタとガベージ・コレクション 6. 基本操作 7. インプロセス サーバとアウトプロセス サーバ 8. シングルユース アプリケーションとマルチユース アプリケーション 9. 例外処理 10. リモート操作 11. VBA(Visual Basic for Application) 12. 外部のスクリプトから VBA を使う 13. Windows API の呼び出し 14. Office 製品で共通のこと 15. Office Web コンポーネント 16. JavaScript と VBScript の違いとか 17. 注意書き 18. 参考資料 ----------------------------------------------------------- 1. COM って何(その1) COM(Component Object Model)は、コンポーネントの再利用を推進するためのバイナ リ標準である。 コンポーネントとして他のアプリケーションに機能を提供するものを COM サーバ(ま たは COM コンポーネント)、他のコンポーネントを利用するものを COM クライアン トと呼ぶ。 COM の機能を利用することで外部のアプリケーションとの連携が容易になる。 COM のインタフェースは実装または利用する言語に依存しない。 # 以降の文章中、ただ COM といった場合、COM サーバを指すものとする。 すべての COM は IUnknown と呼ばれるインタフェースをサポートする。これが COM である最低条件である。 多くの COM は同時に IDispatch と呼ばれるインタェフースも同時にサポートする。 IDispatch を実装することにより、オートメーション機能をサポートする。 オートメーション機能には名前を動的に解決する機能が含まれており、これにより、 スクリプトからの操作が可能になる。オートメーション機能を実装する COM を、オー トメーション サーバと呼び、オートメーション機能を利用するアプリケーションを オートメーションクライアントと呼ぶ。 また、DllRegisterServer() と DllUnregisterServer() のメソッドを実装し、自己 登録型の COM を ActiveX コントロールと呼ぶ。 COM の多くはオートメーションサーバであり、同時に ActiveX コントロールでもあ る。 # 以降オートメーションを表す場合も、ActiveX コントロールを表す場合も特に強調 # が必要な局面でなければすべて COM で統一して表現するものとする。 すべての COM コンポーネントには一意のクラス識別子(CLSID)があり、すべてのイン ターフェイスには一意のインターフェイス識別子(IID)がある。CLSID と IID は、す べてのコンポーネントに渡り一意の識別子(GUID)である。GUID は特殊なアルゴリズ ムで生成され、全世界のすべての GUID の一意性を保証する。COM は内部的にはこれ らの識別子によって管理される。クラス識別子はたとえば以下のようなものである。 00024500-0000-0000-C000-000000000046 しかし、スクリプトで COM を操作するとき、この識別子でアクセスするのはあまり 現実的ではない。COM には憶えやすい名前である ProgID があり、それがレジスト リの中で GUID にマップされる。ProgID は以下のようなものである。 Excel.Application スクリプトではこの名前を使って COM にアクセスする。 2. Office と COM Office の各製品はオートメーションサーバでもある COM であり、オートメーション クライアントの機能も保持している。そのため、外部の COM を利用したり、外部の アプリケーションが Office の機能を使うことがいずれも可能となっている。 3. COM って何(その2) ここまでの説明が分かりにくい場合、次のように言い換えても良い。 COM とは VB でフォーム上に貼り付けたり、あるいは、スクリプトを含む任意のアプ リケーションから CreateObject() などの関数を使って簡単に利用することができる (そのように設計された)コンポーネントであり、Windows では一般的なものである。 COM の多くは最初から部品として提供されるものだが、中には独立したアプリケーシ ョン自体が COM インタフェースをサポートすることで、単独で動作するアプリケー ションがまるごと他のアプリケーションから制御可能になっている場合もある。 Office はまさにこのケースにあたり、他のアプリケーションと容易に連携が可能で ある。 4. COM の操作の例 実際に Office を操作する前に簡単な COM の使い方を見てみる。 以下は Windows 標準の COM コンポーネントである FileSystemObject を使用してパ ス名の操作を行うだけの非常に簡単な例である。 ※ 以下のコードを実際に試してみる場合には先頭の行番号を削除すること。 [JavaScript: sample4a.js] ----------------------------------------------------------------------- 1: var fso = new ActiveXObject("Scripting.FileSystemObject"); 2: var newpath = fso.BuildPath("C:\\Windows", "新しいフォルダ"); 3: fso = null; 4: WScript.Echo(newpath); ----------------------------------------------------------------------- 1行目で FileSystemObject の COM のインスタンスを作成する。 "Scripting.FileSystemObject" は FileSystemObject を識別するための ProgID で ある。この名前を元に new ActiveXObject() としてインスタンスを生成す る。 FileSystemObject はパス名を生成するだけの目的なので、パス名を生成した後はす みやかにインスタンスを破棄する。3行目の null 代入が破棄にあたる。 その後、生成したパス名を表示する。 このコードはあくまで一般にお作法とされるものを例で示しただけのものである。後 述するが、JavaScript の場合、実は null 代入だけではオブジェクトは完全に破棄 されない。 [VBScript: sample4a.vbs] ----------------------------------------------------------------------- 1: Dim fso, newpath 2: Set fso = CreateObject("Scripting.FileSystemObject") 3: newpath = fso.BuildPath("C:\Windows", "新しいフォルダ") 4: Set fso = Nothing 5: WScript.Echo newpath ----------------------------------------------------------------------- 基本的な構造は JavaScript と同じである。 VBScript では最初に変数の宣言を行う。 2行目で FileSystemObject の COM のインスタンスを作成する。 ここでの構文は CreateObject() になる。 VBScript では代入に2通りの書式がある。 先頭に Set が必要な場合と、必要ではない場合である。 オブジェクトへの参照を変数またはプロパティに代入する場合には Set が必要であ り、COM すなわちオブジェクトであるため、COM の操作では必然的に Set が必要に なるようだ。 4行目でインスタンスを破棄する。VBScript での破棄は Nothing の代入である。 5. 参照カウンタとガベージ・コレクション COM はオブジェクトを直接破棄するコマンドを持たず、オブジェクトの参照がなくな った時点で自動的に破棄される。 ここでは参照を管理する参照カウンタの概念について、また、スクリプト言語によっ て異なるオブジェクトの寿命管理について述べる。 参照カウンタとは変数やプロパティに保持するオブジェクトへの参照の総計である。 次の例を考えてみる。 [VBScript: sample5a.vbs] ----------------------------------------------------------------------- 1: Dim fso, fso2, newpath 2: Set fso = CreateObject("Scripting.FileSystemObject") ' 1 3: Set fso2 = fso ' 2 4: newpath = fso.BuildPath("C:\\Windows", "新しいフォルダ") 5: Set fso = Nothing ' 1 6: newpath2 = fso2.BuildPath("C:\\Windows", "新しいフォルダ2") 7: Set fso2 = Nothing ' 0 (オブジェクト破棄) 8: WScript.Echo newpath 9: WScript.Echo newpath2 ----------------------------------------------------------------------- この例では同じオブジェクトを fso, fso2 の2つの異なる変数から参照している。 5行目で fso を使い終わり Nothing を代入をしているが、ここでオブジェクトを破 棄してしまうと、同じオブジェクトを参照する6行目が処理できなくなってしまう。 そのために COM では参照カウンタの概念を用いて寿命管理を行っている。 行の右側にコメントで記述した数値が現在の行を実行した直後のオブジェクトの参照 数である。 この参照数は変数やプロパティに参照を代入するたびにカウントアップし、逆に変数 の内容を書き換えて参照を保持しなくなったり、変数そのものが破棄されるとき、カ ウントダウンしていく。カウントダウンした結果が 0 になれば、すなわち誰も利用 していないことになり、その瞬間オブジェクトが破棄される。 5行目では参照数のカウントダウンは行うが、参照数が 0 にはならないためにオブジェ クトが破棄されることはない。 7行目で fso2 対する Nothing の代入が行われて初めて参照数が 0 になり、オブジェ クトが削除される。参照数のカウントはオブジェクト自身が行う。 参照カウンタがカウントダウンされるのはその参照が無効になるタイミングである。 参照が無効になるのは null 代入以外にも複数の要因がある。以下に示す。 [参照が無効になる要因] - null(Nothing) 代入 - 該当の変数を別のオブジェクトで上書き - 変数の寿命切れ (変数が関数内のローカル変数で関数を抜けた場合や、プログラムの終了時など) 変数はできるだけローカル変数を使う、などの考慮が正しくなされている場合や、非 常に小さいプログラムで null 代入直後にプログラムが終了するような場合には null 代入に神経質になる必要はない。 JavaScript や VBScript ではプログラムが終了するとき、使用しているすべての変 数の寿命切れと見なされ、適切な後処理が行われるためである(たぶん)。 逆に長時間にわたって動作させるプログラムで、かつ、グローバルな変数でオブジェ クトを管理する場合、適切なタイミングで参照を初期化する必要がある。 ここで、参照について考えてみる。 スクリプト言語から COM オブジェクトの参照は直接ではなく、(たぶん)言語毎のラ ッパオブジェクトを経由していると考えられる。だとすれば COM の直接の参照数は 常に 1 (ラッパオブジェクトからの参照のみ)であり、上で述べている参照カウンタ の参照数とは実際には言語毎のラッパオブジェクトへの参照数と予想される。 しかし、たとえそうだとしても VBScript ではこのことは問題とはされない。なぜな ら、COM が参照カウンタをベースにオブジェクトの寿命管理を行うように、VBScript 自身も参照カウンタをベースに COM を保持するラッパオブジェクトの寿命管理を行 うためである。利用者から見た場合、両者の区別はなく、結果的に予想通りの動作に なる。 しかし、JavaScript では事情が異なる。JavaScript のラッパオブジェクトは JavaScript のメモリ管理に従う。JavaScript ではガベージ・コレクションでオブジ ェクトの寿命管理を行うため、参照カウンタを持たず、null 代入をしても直接オブ ジェクトの破棄にはつながらない。ガベージ・コレクションの管理下のオブジェクト はガベージ・コレクションのタイミングでまとめて参照の有無がチェックされ、必要 ならオブジェクトが破棄される。このオブジェクト破棄によってはじめて COM への 参照も破棄される。このガベージ・コレクションのタイミングは不定期である。(ガ ベージ・コレクションはその実装毎に特定のロジックにしたがって処理されるが、利 用者から見た場合は不定期と見なすのが一般的である。JavaScript のガベージ・コ レクションは実際に仕様がよく分からないが。) よって、オブジェクトが破棄されるタイミングも不定期であり、オブジェクトは非常 に長時間生存する可能性もある。 以下に JavaScript のサンプルを示す。 [JavaScript: sample5a.js] ----------------------------------------------------------------------- 1: var fso = new ActiveXObject("Scripting.FileSystemObject"); // 1 2: var fso2 = fso; // 2 3: var newpath = fso.BuildPath("C:\\Windows", "新しいフォルダ"); 4: fso = null; // 1 5: var newpath2 = fso2.BuildPath("C:\\Windows", "新しいフォルダ2"); 6: fso2 = null; // 0 (※1) 7: WScript.Echo(newpath); 8: WScript.Echo(newpath2); ----------------------------------------------------------------------- ※1 オブジェクトはすぐには破棄されない 基本的な流れは VBScript と同じで、実際のコードでもほとんど処理は変わらない。 しかし、JavaScript では、行の右側にコメントで記述してある現在の参照カウント 数が 0 になっても、オブジェクトがすぐに破棄されるとは限らない。 オブジェクトが破棄されるのは論理的な参照カウントが 0 になった後、ガベージ・ コレクションが動作するタイミングであり、非常に時間がかかる可能性がある。 これは JavaScript の仕様である。 この問題を回避し、オブジェクトを強制的に破棄するためには CollectGarbage() と いう undocumented なメソッドを使用する必要があるらしい。 当然ながら undocumented なメソッドは Microsoft の保障外となるが、Microsoft Q&A ページで紹介されてたりするものなので、現状では他に手段はないらしい。 参照カウンタとガベージ・コレクションの動作の違いについて、もう少し分かりやす い例を示す。 [VBScript: sample5b.vbs] ----------------------------------------------------------------------- 1: Sub Foo 2: Dim excel 3: WScript.Echo "Foo start" 4: Set excel = CreateObject("Excel.Application") 5: excel.visible = True 6: WScript.Sleep 5000 7: WScript.Echo "Foo end" 8: End Sub 9: Sub Bar 10: Dim excel 11: WScript.Echo "Bar start" 12: Set excel = CreateObject("Excel.Application") 13: excel.visible = True 14: Set excel = Nothing 15: WScript.Sleep 5000 16: WScript.Echo "Bar end" 17: End Sub 18: Sub Baz 19: Dim excel 20: WScript.Echo "Baz start" 21: CreateObject("Excel.Application").visible = True 22: WScript.Sleep 5000 23: WScript.Echo "Baz end" 24: End Sub 25: Foo 26: Bar 27: Baz 28: WScript.Sleep 5000 ----------------------------------------------------------------------- [JavaScript: sample5b.js] ----------------------------------------------------------------------- 1: function foo() 2: { 3: WScript.Echo("foo start"); 4: var excel = new ActiveXObject("Excel.Application"); 5: excel.visible = true; 6: WScript.Sleep(5000); 7: excel = null; 8: WScript.Echo("foo end"); 9: } 10: function bar() 11: { 12: WScript.Echo("bar start"); 13: var excel = new ActiveXObject("Excel.Application"); 14: excel.visible = true; 15: WScript.Sleep(5000); 16: excel = null; 17: CollectGarbage(); 18: WScript.Echo("bar end"); 19: } 20: function baz() 21: { 22: WScript.Echo("baz start"); 23: new ActiveXObject("Excel.Application").visible = true; 24: WScript.Sleep(5000); 25: WScript.Echo("baz end"); 26: } 27: foo(); 28: CollectGarbage(); 29: bar(); 30: baz(); 31: WScript.Sleep(5000); ----------------------------------------------------------------------- これは合計3つの Excel のインスタンスを起動し、終了するタイミングを見るサンプ ルである。 まず、VBScript 側は予想通りの動きをする。 Foo では Nothing の代入を行っていないが、関数の終了時には Excel が破棄されて いるし、Bar では Nothing 代入直後に破棄される。Baz では Excel インスタンスの 参照を変数に保持していないが、Bar と同様、その直後に破棄される。 JavaScript 側ではタイミングが異なる。 まず、CollectGarbage() 呼び出しがなければ、すべてのインスタンスがプログラム 終了まで残る。この程度のコードではガベージ・コレクションが動作するタイミング がないためである。 このサンプルではガベージ・コレクションの呼び出しを入れているが、それでもすべ て予想通りとは限らない。 foo() 直後の CollectGarbage() では Excel は破棄される。これは問題ない。 しかし、bar() では 16行目に null 代入を行い、その後、CollectGarbage() してる にも関わらず破棄されず、baz() のタイミングでは2つの Excel インスタンスが存在 している。 baz() では CollectGarbage() を呼び出してないため、この2つのインスタンスはプ ログラム終了時まで残っている。 まだまだ謎は多いが、とりあえず関数内でインスタンス化した Excel は参照が残ら ない限り、関数外で CollectGarbage() をすることで対処になっている感じである。 ※ ここのサンプルの確認は Windows XP SP1 + Office 2000 SP3 で行った。 ※ この節ではメモリ管理手法の違いが COM に与える影響について説明し、ガベージ・ コレクションなどメモリ管理手法そのものの仕組みについては説明しない。 興味があれば別途調べてみることをお勧めする。 ※ 参照カウンタとラッパオブジェクトの関係についての話はただの推測であり、実 際の内部構造と一致している保障はどこにもない。 6. 基本操作 Excel を例に Office の基本的な操作について説明する。 Excel の主要なオブジェクトには以下のようなものがある。 Excel Application (アプリケーション) Workbook (ワークブック、.xls ファイルに相当) WorkSheet (ワークシート、タブに相当) Range (操作対象とする文書の特定の領域) このうち、Excel Application オブジェクトが Excel オブジェクト モデルの最上位 の階層のオブジェクトになる。Application オブジェクトを使ってアプリケーション レベルのプロパティの判定や指定、または、アプリケーション レベルのメソッドの 実行を行う。また、Application オブジェクトは Excel オブジェクトの残りのすべ てのオブジェクトのエントリ ポイントになる。 Excel の操作では、まず、Application オブジェクトを作成し、このオブジェクトを 起点として他のオブジェクトを作成したり、あるいは Excel インスタンスそのもの の操作を行う。 外部から直接作成できるのは原則的には最上位のオブジェクトである Application オブジェクトのみである。 ただし、Excel や Word では Application オブジェクトの特定の子オブジェクトに 対して直接参照を作成することもできる。これは例外的な扱いである。 var doc = new ActiveXObject('Word.Document'); var book = new ActiveXObject('Excel.Sheet'); var book = new ActiveXObject('Excel.Chart'); Excel.Sheet ではワークシートが 1 つ含まれる Workbook オブジェクトへの参照が 作成される。Excel.Chart ではグラフを含むワークシートが含まれる Workbook オブ ジェクトへの参照が作成される。いずれも WorkSheet ではなく、Workbook オブジェ クトであることに注意すること。 これらの場合、実際には Application オブジェクトへの暗黙の参照が作成されてい て、Document または Workbook の Application アクセサ プロパティを使用して参 照できる。暗黙の参照は既存の Excel や Word インスタンスがあるとその参照にな るため、別の作業域に Workbook が作成されることになってしまう可能性があり、あ まり推奨できるものではない。使わないほうが安全といえる。 Office 製品で参照可能なすべての最上位オブジェクト ----------------------------------------------------- Access アプリケーション Access.Application Excel アプリケーション Excel.Application Excel ブック Excel.Sheet Excel.Chart FrontPage アプリケーション FrontPage.Application Outlook アプリケーション Outlook.Application PowerPoint アプリケーション PowerPoint.Application Word アプリケーション Word.Application Word 文書 Word.Document ----------------------------------------------------- 以下に Excel の簡単な操作例を示す。 [JavaScript: sample6a.js] ----------------------------------------------------------------------- 1: var excel = new ActiveXObject("Excel.Application"); 2: excel.visible = true; 3: var workbook = excel.Workbooks.Add(); 4: var worksheet = workbook.Worksheets(1); 5: worksheet.Range("b2") = "hello"; 6: workbook.SaveAs("C:\\office_samples\\output6a_ja.xls"); 7: workbook.Close(false); 8: excel.Quit(); 9: excel = null --------------------------------------------------------------- [VBScript: sample6a.vbs] ----------------------------------------------------------------------- 1: Dim excel, workbook, worksheet 2: Set excel = CreateObject("Excel.Application") 3: excel.visible = True 4: Set workbook = excel.Workbooks.Add 5: Set worksheet = workbook.Worksheets(1) 6: worksheet.Range("b2") = "hello" 7: workbook.SaveAs "C:\office_samples\output6a_vbs.xls" 8: workbook.Close False 9: excel.Quit 10: Set excel = Nothing --------------------------------------------------------------- JavaScript 版を例にとって各行の説明を行う。 - 起動 1: var excel = new ActiveXObject('Excel.Application') Excel の新しいインスタンスを作成し、そのオブジェクトの参照を変数 excel に 代入する。 - 可視化 2: excel.visible = true Excel(に限らず Office 全般)のインスタンスを作成しても初期状態では不可視と なっている。 表示させる場合には visible プロパティに true を設定して可視化を行う必要が ある。 バックグラウンドで自動化処理を行うようなツールでは不可視のままの方がよいが その場合でもデバッグ中は表示させるのが普通である。 不具合の状態を目で確認することができない上、うっかり Excel のインスタンス を残してツールが終了してしまった場合など、不可視の状態では終了させることも 困難なためである。 - Workbook の取得 3: var workbook = excel.Workbooks.Add(); 文書を操作するには、まず .xls ファイルを読み込むか、新規に作成する必要があ る。 ここではファイルに相当する Workbook オブジェクトを新規に作成している。 - Worksheet の取得 4: var worksheet = workbook.Worksheets(1); 作成した Workbook の1ページ目の sheet を取得している。 - セルの操作 5: worksheet.Range("b2") = "hello"; Range オブジェクトを通じてセルにテキストを書き込む。 - ファイルに保存 6: workbook.SaveAs("d:\\a.xls"); 名前をつけてファイルに保存する。 - Workbook を閉じる 7: workbook.Close(false); Workbook を閉じる。パラメタの false は workbook の編集の有無に関わらず強制 的に閉じることを意味する。 スクリプトを利用して Excel 操作の自動化を行う場合、保存が必要なら、事前に Save() または SaveAs() で保存していることが自然なので、Close() でも問い合 わせのダイアログが表示されないようにしておく。 - Excel の終了を宣言 8: excel.Quit(); Excel オブジェクトの使用終了を宣言する。 前節までは null 代入しか説明してなかったが、null 代入はあくまで、COM オブ ジェクトの参照に関わる低レベルなものであり、Excel インスタンスの明示的な終 了のためにこの Quit() メソッドを呼ぶのが正しい。 というか呼ばないと実際に不具合が発生するので終了時は必ず呼ぶこと。 Quit() の呼び出しと null 代入による参照の破棄はいずれも必要である。 - 参照を破棄 9: excel = null Excel オブジェクトへの参照を破棄する。 ただし、JavaScript の場合、表面上は破棄したつもりであっても内部処理とのタ イミングにずれがあって簡単には破棄されないのは前節で示したとおり。 7. インプロセス サーバとアウトプロセス サーバ COM には .dll または .ocx の拡張子を持つインプロセス サーバと、.exe ファイル であるアウトプロセス サーバの2種類がある。 インプロセス サーバは COM を起動したアプリケーションのプロセス内でのみ動作す る。一方、アウトプロセス サーバは自立型アプリケーションとして動作する。 Office 製品(の少なくとも最上位の Application オブジェクト)はアウトプロセス サーバである。 アウトプロセス サーバの例としては Office 製品以外では Internet Explorer など がある。 アウトプロセス サーバの特徴(注意点)の一つはプロセスを超えたリークが発生する ことである。 インプロセス サーバの場合、COM を起動したプロセス内でのみ動作するため、該当 のプロセスが終了すれば COM も自動的に終了し、リークの影響もプロセス内にとど まる。 アウトプロセス サーバの場合、起動後、明示的に終了が指示されなければ、呼び出 した COM クライアントが終了してもそのまま起動し続けることが可能である。プロ セスが終了したのに COM が残ることを意図してないのであれば、それはリークであ る。 例を挙げれば、集計の自動化のため Excel を非表示にしつつ処理をさせるツールで、 処理の途中で例外が発生したために正しい終了処理が行えなかったようなパターンで 発生する。 このツール自体を 100 回起動し、すべてエラーで終了したとすると 100 個の Excel のインスタンスが同時に起動していることになり、OS 自体を不安定にしてしまうよ うな事態が起こりうる。 Excel の場合、Quit() メソッドが終了の宣言にあたる。 Excel を起動して、何も操作せず Workbook も作成しない場合に限っては COM の参 照がなくなることで自動的にインスタンスも破棄された。しかし、それ以外の操作を 行うと参照の破棄だけでは Excel インスタンスは終了せず、そのまま存在し続ける。 Quit() は明示的な解放の宣言であり、Excel インスタンスを使い終わったとき、参 照を破棄する前に、必ず呼ぶ必要がある。 しかし、処理の途中で例外が発生すると、残りのコードの実行を省略してプログラム が終了してしまうことがある。省略されたコードの中に Quit() 呼び出しがあると、 それがリークの原因となる。 アウトプロセス サーバを使用するとき、必ず例外を補足してしかるべき対処を行う 必要がある。これは特にアウトプロセス サーバに限った話ではないが、アウトプロ セスサーバを使う場合、特に注意が必要である。 また、アウトプロセス サーバではリークの注意点とは別にユニークな性質も持つ。 プロセスに依存しないため、自分以外のプロセスが起動した Excel のインスタンス に接続して操作したり、Excel の1つのインスタンスを複数のアプリケーションで同 時に制御することも可能である。 以下にサンプルを示す。 これは、既に起動している Excel のインスタンスに後から接続し、その Excel を強 制的に終了させるものである。ここでは終了させているだけだが、もちろん終了だけ でなく他の操作も可能である。 [JavaScript: sample7a.js] ----------------------------------------------------------------------- 1: var excel = GetObject("", "Excel.Application"); 2: excel.Quit(); 3: excel = null; ----------------------------------------------------------------------- [VBScript: sample7a.vbs] ----------------------------------------------------------------------- 1: Dim excel 2: Set excel = GetObject(, "Excel.Application") 3: excel.Quit 4: Set excel = Nothing ----------------------------------------------------------------------- このサンプルを実行して動作確認を行うには事前に Excel を起動しておくこと。 8. シングルユース アプリケーションとマルチユース アプリケーション Office の各製品はアプリケーションの種類としてシングルユースとマルチユースの いずれかに分類される。 マルチユース アプリケーションでは複数の COM クライアントが同じアプリケーショ ンのインスタンスを共有するのが規定の動作となり、シングルユースアプリケーショ ンでは複数の COM クライアントはそれぞれ別のインスタンスを取得するのが規定の 動作となる。 Access シングルユース Excel シングルユース FrontPage シングルユース Outlook マルチユース PowerPoint マルチユース Word シングルユース Outlook の場合、メールの送受信データなど単一のリソースを扱う必要があるため、 複数のインスタンスを作成させないのだと推測できる。しかし、PowerPoint がマル チユースである理由はよく分からない。全画面表示するからなのか? シングルユースの場合、新しいインスタンスを作成するか、既存のインスタンスに接 続するかは選択可能である。 [JavaScript] ----------------------------------------------------------------------- 1: var excel = new ActiveXObject("Excel.Application"); // 新規インスタンス作成 2: var excel2 = GetObject("", "Excel.Application"); // 既存インスタンスに接続 ----------------------------------------------------------------------- [VBScript] ----------------------------------------------------------------------- 1: Dim excel, excel2 2: Set excel = CreateObject("Excel.Application") ' 新規インスタンス作成 3: Set excel2 = GetObject(, "Excel.Application") ' 既存インスタンスに接続 ----------------------------------------------------------------------- マルチユースの場合、インスタンスが存在しない場合に限って新しいインスタンスを 作成できるが、複数のインスタンスを作成することはできない。 CreateObject() などのオブジェクト作成の関数を使った場合でも既存インスタンス が存在すればそのインスタンスを受け取ることになる。 [JavaScript] ----------------------------------------------------------------------- 1: var excel = new ActiveXObject("Excel.Application"); // 新規インスタンス作成または接続 2: var excel2 = GetObject("", "Excel.Application"); // 既存インスタンスに接続 ----------------------------------------------------------------------- [VBScript] ----------------------------------------------------------------------- 1: Dim excel, excel2 2: Set excel = CreateObject("Excel.Application") ' 新規インスタンス作成または接続 3: Set excel2 = GetObject(, "Excel.Application") ' 既存インスタンスに接続 ----------------------------------------------------------------------- 特に PowerPoint の処理の自動化を行うとき、終了時に無条件で Quit() などを行う と、予期せぬ影響を引き起こす可能性があるので注意すること。 9. 例外処理 前の節でも説明したとおり、例外を拾って正しく終了処理をしないと Office インス タンスがリークして Office のゾンビが蓄積されてしまう可能性がある。正しく例外 処理を行っていないとそれ以外にもさまざまな不具合が発生する可能性があり、 Office の操作では適切な例外処理は必須といえる。 まず、リーク対策としては大きく2つの方針がある。 1つは Office の読み書きをなるべく狭い範囲に閉じ込めること。できれば関数単位 とし、その中でインスタンスの作成、解放と読み書きを行うような方法である。 Excel であれば、最初はファイルを開いてすべてデータを読み込んでクローズ、計算 は Excel と関係なくスクリプト内で行い、最後に再度ファイルを開いて書く、など。 もうひとつの方法はインスタンスをグローバルな変数に保持し、終了時に確実にクロー ズする方法である。 比較的規模の大きいものは前者、規模の小さいものは後者が向いているかもしれない。 リークに関係するのは主に終了処理のみだが、正常な結果を保証するためには処理毎 にエラーチェックと適切な対処も必要である。 このためには office 操作のコマンド毎にエラーを検出できる仕組みが必要になる。 以下、それらをふまえたエラー処理の方法について説明する。 [JavaScript: sample9a.js] ----------------------------------------------------------------------- 1: var file = "C:\\office_samples\\input9x.xls" 2: try { 3: var excel = new ActiveXObject('Excel.Application') 4: try { 5: excel.visible = true 6: var workbook = excel.workbooks.Open(file, 0, true); 7: try { 8: WScript.Echo(workbook.worksheets(1).Range("B2").value); // 実際の読み書き 9: } 10: catch (e) { 11: WScript.Echo("ERROR: "+e.description); 12: } 13: try { 14: workbook.Close(false); 15: } 16: catch (e) { 17: } 18: } 19: catch (e) { 20: WScript.Echo("ERROR: "+e.description); 21: } 22: try { 23: excel.Quit(); 24: } 25: catch (e) { 26: } 27: } 28: catch (e) { 29: WScript.Echo("ERROR: "+e.description); 30: } ----------------------------------------------------------------------- JavaScript では例外は比較的素直に記述できる。 文法的にも Java と非常に似ているし、try〜catch の範囲内の例外は最初に発生し た例外でキャッチされ、エラーコードや詳細メッセージなどを得ることができる。 上のコードはかなり面倒そうにも思えるが、実際にはグループ内で使用する小物ツー ルであれば Close() や Quit() のエラーなどは拾わないことも多い。 一時的なツールのの場合、プログラムではエラーを一切拾わずにシステムに任 せてもよい。デフォルトの状態ではシステムはエラーを検出したら詳細なエラーメッ セージを表示してプログラムを中断するため、デバッグには十分である。むしろ行番 号などの情報が付加されたりするため、自分でエラーメッセージを表示するよりも分 かりやすいことも多い。 [VBScript: sample9a.vbs] ----------------------------------------------------------------------- 1: On Error Resume Next 2: Dim excel, workbook 3: Dim file 4: file = "C:\office_samples\input9x.xls" 5: Set excel = CreateObject("Excel.Application") 6: If Err.Number = 0 Then 7: excel.visible = True 8: Set workbook = excel.Workbooks.Open(file, 0, True) 9: If Err.Number = 0 Then 10: WScript.Echo workbook.Worksheets(1).Range("B2").value 11: If Err.Number <> 0 Then 12: WScript.Echo "ERROR: " & Err.Description 13: End If 14: Else 15: WScript.Echo "ERROR: " & Err.Description 16: End If 17: workbook.Close False 18: excel.Quit 19: Set excel = Nothing 20: Else 21: WScript.Echo "ERROR: " & Err.Description 22: End If ----------------------------------------------------------------------- 問題は VBScript である。 VBScript は例外を処理するにはあまり適しておらず、正しい例外を拾うためにはい くぶん面倒な記述になる。 ここでは VBScript のエラー処理についていくつかの方法を説明する。 上に示したのは最初例である。よい例とはいえないかもしれないが、かなり読みにく いコードである。 面倒な理由は VBScript の例外機構が2つのモードしか用意していないことによる。 1つは例外が発生したらすぐにプログラムを打ち切ってプログラムを終了、または、 関数から復帰するモードである。これはデフォルトである。明示的な宣言は以下。 On Error Goto 0 もう1つは例外が発生したら、結果だけを Err オブジェクトに格納し、処理はそのま ま続行するモードである。エラーを処理するにはこちらを使う。宣言は以下。 On Error Resume Next これらの指定を関数毎に行う。 注意が必要なのはエラーが連続して発生すると後で起こったエラーで Err オブジェ クトの内容が上書きされてしまうことである。エラーが連続して発生した場合、直接 の原因であり情報を必要とするのはほとんどの場合、最初のエラーである。 そのため、エラーが発生する可能性のある行が連続する場合、エラー情報が上書きさ れて失われないように各行毎にチェックを行う必要がある。 結果、処理1行毎に複数行のエラーチェック処理が必要になり、読みづらいコードに なってしまう。上記サンプルはこの例を示している。 Web などに転がっているサンプルでは先頭で "On Error Resume Next" の宣言だけを 記述してエラーを一切拾わない記述も多い。この場合、エラーをすべて無視して突っ 走るため、確かに最後の Close や Quit の呼び出しが漏れることはなく、リークは 起こらないかも知れないが、正常に動作したかどうかがわからないのではプログラム として問題であり、選択すべきではない。 2つめとして、幾分ましな方法を紹介する。処理をいくつかの関数とそれを呼び出す メイン部分に分離する方法がある。 このとき、メイン処理ではエラーを拾う(On Error Resume Next)設定にして、関数側 ではエラーを拾わない(On Error Goto 0)設定とする。 このようにすることで関数内でエラーが発生した場合、最初のエラーで処理を中断し てそのエラー情報を呼び出し元であるメイン処理に返す。メイン処理ではかならず エラーチェックをして正常かエラーかが判断できる。 このパターンの例を以下に示す。 [VBScript: sample9b.vbs] ----------------------------------------------------------------------- 1: On Error Resume Next 2: 3: Sub ExcelRead(file) 4: On Error Goto 0 5: excel.visible = True 6: Set workbook = excel.Workbooks.Open(file, 0, True) 7: WScript.Echo workbook.Worksheets(1).Range("B2").value 8: workbook.Close False 9: excel.Quit 10: Set excel = Nothing 11: End Sub 12: 13: Dim excel, workbook 14: Dim file 15: file = "C:\office_samples\input9x.xls" 16: Set excel = CreateObject("Excel.Application") 17: If Err.Number = 0 Then 18: Call ExcelRead(file) 19: If Err.Number <> 0 Then 20: WScript.Echo "ERROR: " & Err.Description 21: End If 22: Else 23: WScript.Echo "ERROR: " & Err.Description 24: End If ----------------------------------------------------------------------- このようにすると関数内の記述はすっきりし、かつ、エラーも検出できる。 エラーのモードは関数毎にリセットされ、"On Error Goto 0" がデフォルトになるた め、先頭の "On Error Goto 0" は省略できる(たぶん)。 問題は、ExcelRead の中からさらに別の関数(例外を発生させる)を呼び出してその結 果を ExcelRead 内で処理したい、といった場合である。エラー処理する(On Error Resume Next)とエラー処理しない(On Error Goto 0)を交互に切り替えなければなら ず、対象のプログラムの構造にもよるがかなり面倒なものになる可能性がある。 3つめとして、外的要因に依存する項目だけ明示的にエラーチェックを行い、それ以 外はエラーが起こらないように祈る、という方法もある。 具体的にはファイルオープンの直後だけエラー判定を入れてあとは気にしない、とい うもの。もちろん事前にテストをしっかり行って処理が動作するのを確認するのが前 提となる。実際、現実的なエラーのうち、ほとんどはこれでカバーできると思われる が、あまりに心もとない。たとえば複数ファイルを扱うプログラムで1ファイルだけ 古いバーションが紛れ込むような場合、このようなチェックではエラーを見逃してし まう確立が高く、あまりお勧めできない方法である。 ただし、目的とコストによるが、グループ内の特定用途のツールなどであれば、この あたりで妥協するのが現実的といえるかも知れない。 4つめとして、外的要因に依存する項目だけ "On Error Resume Next" モードにして 明示的にエラーチェックを行い、それ以外は "On Error Goto 0" のモードにしてプ ログラムを中断させる方法が考えられる外的要因に依存しない部分はおそらくは内部 エラーの扱いであり、処理としてはかなり適切といえる。ただし、VBScript の仕様 の問題で使いづらいのが問題である。 ようするに、以下のように記述できれば問題ないが、 [VBScript] ----------------------------------------------------------------------- 1: On Error Resume Next 2: Set excel = CreateObject("Excel.Application") 3: On Error Goto 0 4: If Err.Number <> 0 Then 5: ... 6: End If ----------------------------------------------------------------------- 実際には記述できない。 なぜなら、この場合、3行目の "On Error Goto 0" でエラー情報が初期化されてしま い、4 行目の判定が無効になってしまうためである。正しく動作するよう修正するに は "On Error Goto 0" を If 文の判定の中と外の両方に入れる必要があるが、記述 がかなり煩雑で分かりにくくなってしまう。 5番目としてエラーを拾わない方法がある。これは特に一時的なツールで有効である。 ここまで4通りの例外の処理の仕方について説明したが、その場限りのツールであれ ば当然その対処方法も異なる。一時的なツールの場合、エラーが発生したことが分か ればよいのであってそのためにはむしろエラーは拾わずシステムにまかせるのが一番 分かりやすい。(JavaScript、VBScript ともデフォルトの状態) システムの表示するエラー情報はプログラム内で取得できるよりも多くの情報が表示 されるのでその点でも有利である。 エラーを拾わない場合、リークに対する対処として必ず Office のインスタンスは可 視化しておくこと。エラーがあっても手動で閉じることができる上、結果もすぐに確 認できて便利である。 以上、特に VBScript のエラー処理について、まとめると以下のようになる。 エラーは正しく拾って処理するか、まったく拾わずシステムに任せる方法がある。 エラーの拾い方にはいくつかの方法がある。 VBScript において、先頭に "On Error Resume Next" の宣言だけを記述して、エラー 判定をまったく行わないというのは非常にまずいやり方で選択すべきではない。 10. リモート操作 ---------------------------------------------------------------------------- Application オブジェクト作成の対象となる Office アプリケーションがローカル コンピュータではなく、ネットワーク上のほかのコンピュータにある場合です。たと えば、MicrosoftR Access がインストールされていないローカル コンピュータで MicrosoftR Visual BasicR for Applications (VBA) コードを実行し、ネットワーク サーバー上の Access データベースからレポートを印刷することができます。Access がネットワーク サーバーにインストールされている場合、CreateObject 関数の servername 引数 (省略可能) でサーバー名を指定して、サーバーで動作する Access の Application オブジェクトを作成することができます。次はその例です。 Dim objAcApp As Object Set objAcApp = CreateObject("Access.Application", "MyServer1") CreateObject 関数の servername 引数は共有名のコンピュータ名の部分と同じです。 つまり、共有名 "\\MyServer1\Public" の servername 引数は "MyServer1" です。 Office アプリケーションをリモート サーバーとして正常に実行するには、サーバー として機能するコンピュータの分散コンポーネント オブジェクト モデル (DCOM) の 設定を (場合によってはクライアント コンピュータの設定も) 変更する必要があり ます。DCOM の設定を変更するには、Distributed COM Configuration ユーティリティ (Dcomcnfg.exe) を [スタート] メニューの [ファイル名を指定して実行] ボックス から実行します。DCOM の設定については、サポート技術情報検索の Web サイト (http://search.support.microsoft.com/kb/c.asp?ln=ja&lng=jpn&sd=gn) で「DCOM」 を検索してください。 ---------------------------------------------------------------------------- という説明が MSDN にあり、できるはず。 できるはずだが、具体的な方法が不明で、うまくいかない。 dcomcnfg.exe の設定がポイントのようではあるが、情報が少なく、まだ調べてない。 調査中 11. VBA(Visual Basic for Application) この文書では主に Office を外部から操作することについて説明している。そのため、 サンプルの言語も JavaScript と VBScript に限定してきた。 しかし、この節では Office 製品組み込みのスクリプト言語である VBA について少 しだけ説明する。 なお、Office のメニューでは VBA で記述されたスクリプトやその操作を「マクロ」 と表現しているが、この文書では VBA で統一して表記する。 VBA は VB と VBScript の中間的な存在といえる。文法や機能もかなり似ているが、 VBScript よりは VBA の方が若干細かな操作が可能である。 異なる点について、COM に関していえば、VBA では型の事前バインディング(early binding)をサポートする。COM の多くは型の事前バインディングと遅延バインディン グ(late binding)の両方に対応する。型の遅延バインディングをサポートする COM をオートメーションと呼ぶ。しかし、一部の COM は遅延バインディングをサポート しない。オートメーションでない COM は VBScript から使用することができない (JavaScript でも同じ)が、事前バインディングを行う VBA であれば使用することが 可能である(たぶん)。また、事前バインディングは速度的にも有利である。 他には例外処理をより真っ当に扱える(たぶん)。VBScript では例外は拾うか拾わな いかの2択しかなかった。VBA では "On Error GoTo