「オブジェクトがありません」と言われる理由
まず、「オブジェクトがありません。」というエラーが出力されてしまうのは、 GetObject
に失敗した場合、そのエラーによる停止を抑制しても Set
文の実行が完了しないため、 objExcel
変数の値が Empty
のままになり Is Nothing
での評価が行えないせい ですね。つまり、態々 On Error Resume Next
で GetObject
のエラーを抑制している意味自体が無くなってしまって居りますから、代わりに IsEmpty(objExcel)
で評価するか、 Dim objExcel
をした直後に Set objExcel = Nothing
として初期値を代入しておくと良いかと存じます。
Empty
と Nothing
の差については良い解説記事が見当たらないため、御自身で御調べ頂きたいのですが、簡単に言ってしまえば、前者が「初期化していない Variant であり、 中身の型すら不明 である」ことを意味するのに対し、後者は「参照先のオブジェクトは 存在しないが、型はオブジェクト型 である」ことを意味しています。 Is
演算子は オブジェクト型変数の参照先が同一オブジェクトかどうか を判定しますから、ここに Empty
値を持ち込むと「そもそもオブジェクト型ではないから比較できないよ」というエラーが生じているという寸法です。 VBScript では全ての変数が Variant である、という VBA との違いを踏まえずに VBA 的な書き方をされているようにも御見受けされますので、もしそこに誤解があるようでしたら御注意下さいませ。
また、こういった形で、起こっている状況自体が分からなくなり、余計な不具合を持ち込んでしまう原因となる為、少なくとも開発中は エラー抑制は行わない ことを推奨致します。
GetObject
でファイルを指定する方法
次に、何故 GetObject
で特定の Excel ブックを対象としたオブジェクトを開けないのか、正しく開くにはどうすれば良いかなのですが、これは少々、背景の説明がややこしくなります。
結論としては、公式に Office automation server の GetObject および CreateObject の動作にて示されている様に、 Microsoft Office の automation を扱うのであれば ファイル名を指定する場合は、 ProgID を指定せずに GetObject
を実行する 、つまり GetObject(fileName)
とするのが基本的に正解となります。
但しこの場合、 「ファイルをダブルクリックする」のとほぼ同じ動作をする ため、「ファイルを指定した GetObject
」自体には成功しても、「そのファイルが起動中であれば」という条件を満たすことは出来なくなります。もしも開いていない場合、勝手に新しいインスタンスを立てて該当のファイルを開いてしまうからです。
その点を踏まえた解決策は後に述べますが、このように「ProgID を指定しない」GetObject
が適切と言えるのは、以下の 2 点が理由となります。
1) ProgID がそもそも異なる
実際に上記の通り、ファイルパスだけから GetObject
をしてみると分かるのですが、 GetObject(,"Excel.Application")
とは異なり、得られるオブジェクトは Application オブジェクト ではなく Workbook オブジェクト となります。
これは単純に、 ファイルと対応するオブジェクトは Application
ではなく Workbook
だから という事実から来ているだけなのですが、それ故に指定するべき ProgID は、少なくとも "Excel.Application"
ではなくなります。指定するとしたら、 "Excel.Sheet"
あるいは "Excel.SheetMacroEnabled"
が適切です。
2) GetObject
の内部動作が異なる
では "Excel.SheetMacroEnabled"
を指定すればよいか? と言うと、そうでもありません。恐らく今回の要件で「正常に動作すべき条件」を整えて行うと、やはり GetObject
に失敗してしまうはずです。これは Microsoft の GetObject
の内部動作に関する解説 を御覧頂くと分かるのですが、実のところ、 ProgID を指定するか否かで、 GetObject
の動作が大きく変わってしまう事が理由です。
簡単に言えば、 ProgID を指定しない場合は前述の通り、「ファイルが既に開いていればそのインスタンスを取得し、開いていなければ開く」 (COM 的には、ファイルモニカを使用して BindToObject
を呼び出し、 Running Object Table に既知のオブジェクトがあるかを探しに行く) 一方で、 ProgID を指定した場合は「起動している Excel を使い、常にそのファイルを新しく開こうとする」 (COM的には、 ProgID から CoCreateInstance
でオブジェクトを作成し、 IPersistFile.Load
でファイルを読み込もうとする) という違いがあります。
この結果、仮に適切な ProgID を指定したとしても、それを指定した GetObject
呼び出しは、 既に同じ名前のファイルが開いている場合は Excel の「同じ名前のファイルは新しく開けない」という制限によって失敗してしまう ことになります。
ある Excel ブックが開いていることを確かめるにはどうすればよいか
結局のところ、単に GetObject
にパスを指定するだけでは、仰るような「既にその Excel ブックが開いているとき」という条件判断を実現することが出来ません から、何らかの方法でそれを判定する必要があります。
残念ながら、これを確実に行う容易な方法は存在しないかと思われます。確実に行うには、実行されている全ての Excel のインスタンスを列挙する必要がありますが、 VBScript からはそれを実現する手段が存在しないからです。 GetObject
で得られる "Excel.Application"
インスタンスは「最初に見つかった」ものでしかなく、他に同じインスタンスが実行されていないことは保証されませんし、他にあり得るインスタンスを得る方法も存在しないかと存じます。
そのため、 こちらの Stack Overflow 記事 に解説が御座いますが、どうしても実現をしようとするならば、 Windows API を用いて次のステップで全ての Excel インスタンスを列挙する ことになります。同じような事は こちらの方も日本語でやっていらっしゃいます ね。
FindWindowEx
を使い、ウィンドウクラスが "XLMAIN"
であるウィンドウを探す
- さらに、その中にある
"EXCEL7"
クラスのウィンドウを探す
- 見つかったウィンドウが開いているブックに対応するので、それぞれに対して
AccessibleObjectFromWindow
を呼び、 IDispatch
を取得する
ただこれは、現行の Excel 本体のウィンドウ構造に依存している事や、 VBScript からは直接 WIndows API を扱えないために Excel 等、他のコンポーネント経由で Windows API を叩く ようなアクロバティックな手法が必要になる事から、あまり御勧めできるような方法でも御座いません。
現実的には、 「多くの状況で、 Excel のインスタンスは単一であろう」 という楽観的な前提の下、 GetObject
した Excel の Application オブジェクトから該当のファイルが開かれているかを確認し、開かれていれば処理を実行する といった実装になってしまうかと思われます。仮に条件が整っているはずなのに失敗した場合、複数の Excel インスタンスが実行されているということですから、その状況を手動で解決してから再実行することになるでしょう。コードとしては、次のような形の事を言っています。
vbscript
1' 対象のファイルとマクロ名
2Dim fileName, macroName
3fileName = "D:\Book1.xlsm"
4macroName = "Book1.xlsm!TestJob"
5
6
7' Excel の Application オブジェクトを取得
8Dim objExcel
9On Error Resume Next
10Set objExcel = GetObject(, "Excel.Application")
11On Error Goto 0
12
13If IsEmpty(objExcel) Then
14 WScript.Echo( _
15 "Excel インスタンスに接続できませんでした。" & _
16 "Excel が実行されていることを確認してください。" _
17 )
18 WScript.Quit
19End If
20
21
22' 該当のファイルが開いているかを確認
23Dim objTargetWb, objFSO
24Set objFSO = CreateObject("Scripting.FileSystemObject")
25
26For Each workbook In objExcel.Workbooks
27 If objFSO.BuildPath(workbook.Path, workbook.Name) = fileName Then
28 Set objTargetWb = workbook
29 End If
30Next
31
32If IsEmpty(objTargetWb) Then
33 WScript.Echo( _
34 "対象の Excel ブックが開いていることを確認できませんでした。" & _
35 "Excel が一つだけ実行され、かつ実際にブックが開かれていることを" & _
36 "確認してください。" _
37 )
38 WScript.Quit
39End If
40
41
42' マクロを実行する
43objExcel.Run macroName
どうしてもこういった運用での対応が許容できない場合は、前述のような Windows API を用いた実装にチャレンジしてみるのも宜しいかと存じます。
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。
2020/03/14 06:21
2020/03/15 00:31
2020/03/18 10:29