2012年8月30日木曜日

VBAでWordのウィンドウを水平方向に並べようとしたらWin32APIを連呼していた

【概要】

にて、Wordのウィンドウを水平方向に並べる(敷き詰める)マクロとともに、『Word2010に関してはApplication.Documents プロパティの挙動がおかしいため、正しく動かないことがある』という話が載っていました。

 手元のWord2010で確認したところ、たしかに正しく動かなかったので、Word2010におけるApplication.Documents プロパティの挙動について確認したところ、『Application.Documents プロパティは使えない』という結論に至ったので代替案を考えました。
 また、Word2000 ~ Word2013Preview のいずれのVersionにおいても、Wordのウィンドウを(プライマリモニタにおいて)水平方向に並べることができるようにマクロに手を加えてみました。

【Word2007以前の Application.Documents プロパティ】

まずはじめに、Word2007以前のApplication.Documents プロパティの挙動を確認しました。手元の環境で調べた限りでは、だいたい以下の通り。
  1. Application.Documents(Index) で、現在開いているDocumentオブジェクトにアクセスができる
  2. Indexには、"番号"もしくは"ファイル名"を用いることができる
  3. Index の番号は、現在開いているDocumentに対して
    新しく開いた順に1から連番で振られる
  4. どれか1つDocumentを閉じると、即座に
    残っているDocumentに対して新しく開いた順に1から連番で振り直される
このうち、1. 2.に関しては、MSDNに記載されている仕様の通りです。
 一方、3. 4.に関しては、特にMSDNに記載されているわけではないので、「たまたまこうなった」という程度の話かもしれません。(とりあえずは、常識的な挙動かなぁという印象を受けます。)

【Word2010の Application.Documents プロパティ】

MSDNを確認すると、Word2010の仕様として、保護ビューで表示されているDocumentに関しては、Appication.Documentsには含まれず、ProtectedViewsプロパティに含まれることが記載されています。もっとも、この仕様は今回の件には関係ありませんが…。

 Word 2010 Documents collection corrupted if work with several documents にて議論されているように、Word2010においては Application.Documents の挙動がかなり歪です。また、場合によっては、現在開いてるDocumentの情報を正しく保持しておらず、MSDNの記載とさえ一致していません。バグと言っても構わないレベルのような気がします。
 フォーラムでの議論内容を実際に手元で確認した限りでは、
  1. Application.Documents(Index) で、現在開いているDocumentオブジェクトにアクセスができる
  2. Indexには、"番号"もしくは"ファイル名"を用いることができる
  3. Index の番号は、現在開いているDocumentに対して
    新しく開いた順に1から連番で振られる
  4. どれか1つDocumentを閉じても、即座には
    残っているDocumentに対して新しく開いた順に1から連番で振り直されない
    1. Index 番号が飛び飛びになる(ことがある)
    2. Index 番号が存在しないDocumentが生じる (ことがある)
    3. Index 番号を複数持つ Documentが生じる (ことがある)
  5. それなりに時間が経つと、 現在開いているDocumentに対して
    新しく開いた順に1から連番で振り直されている
1. ~ 3.に関しては、Word2007以前と同様です。
 4. の Documentを閉じたときの挙動がかなり奇妙です。正直、奇妙というより、バギー過ぎて使い物にならないと考えた方がいいレベルだなと思いました。
 5. に関しても、数十秒から数分程度かかる場合まであり、どの程度の時間が経過すると、Index番号が振り直されるのかよくわかりません。

【Application.Documents の注意点】

そもそもの問題として、Application.Documentsでは、(当該コードを走らせている)単一のプロセスにおいて開かれているDocumentしか取得できません。このため、画面上に表示されているDocumentが、2つ以上のプロセスにわたっている場合には、画面上に表示されているDocument全てを操作するのに、Application.Documents は使えません。

 デフォルトの関連付けでは、Wordファイルを開く際には、1つのプロセス上で新しいウィンドウを作成するようになっているので、この点は一見問題にならなさそうに思えるのですが、実際にファイルを閉じたり開いたりしていると、たまに別プロセスで起動することがあり、この問題の発生確率は低くないと思われます。(もしかすると自分の環境だけかもしれませんが…。原因は未確認です。)
 また、コマンドラインオプションを使えば(*)別プロセスでの起動が可能なので、複数のプロセスが立ち上がっている可能性は考慮しておいた方がいいように思えます。

(*) Word のコマンド ライン スイッチ /n または /wで別プロセスを起動可能。

【Application.Documents に代わる代替策】

 以上の仕様を確認してみて、Application.Documentsは今回のマクロのように、Wordのウィンドウを操作するのには適さないという結論に至りました。そこで、いずれのVer.でも確実に使える代替方法を考えた結果、
  1. Win32APIのEnumWindowsを用いてWordのウィンドウハンドルを拾い
  2. MoveWindowでリサイズする
方法が良いかなと思いました。この方法がベストなのかはよく分かりませんが、自分の中では最終的にこの方法に落ち着きました。(実際のところ、Win32APIを使わないで済むように色々試してはみたのですが、いい方法が見つかりませんでした…。スマートな解決法をゆるく募集しております。)

 実際に、書いたコードは以下の通りです。
 1つ目のファイルは、Wordのウィンドウハンドルを取得するプロシージャ(GetAllWindowHandlesOfWord) を作成するためだけのもので、2つ目のファイルで、このプロシージャを用いて実際にウィンドウを水平方向に並べています。


Win32APIを使うにあたって、
EnumWindows関数 に関しては、列挙(EnumWindows by VBA) を、
MoveWindows関数 に関しては、「Win32API(C言語)編 第60章 ウィンドウの操作①」 を、
SystemParametersInfo関数 に関しては「エクセルVBAで画面の大きさを取得する方法!」 を、
参考にしました。各記事の作者の方々にはこの場を借りてお礼を申し上げます。

 なお、上記コードでは、プライマリモニタにウィンドウを並べる仕様になっています。マルチディスプレイ環境において、他のディスプレイに並べたい場合は、SystemPatametersInfoに代えて、GetSystemMetrics, GetMonitorInfo あたりを使うと良いかもしれません。(未検証です。) 

 また、上記コードは、64bit環境ではそのままでは動作しません。Win32APIを参照するためのDeclare の後ろに PtrSafeを記載してください。

【ウィンドウを並べる際の注意点】

 オリジナルのマクロは簡易的な仕様なので、色々考慮されていないのは当然なのですが、参考までに、汎用的に使えるプログラムを作る場合に考慮したほうが良いのではないかなと思う点を下記に列挙してみました。
  • 作業領域の左上の座標が(0,0)でない場合
    • タスクバーの位置が左または上の場合
  • マルチディスプレイ環境において、XY座標がマイナスの場合
    • プライマリモニタの左または上にモニタがある場合
  • マルチディスプレイ環境において、ディスプレイのサイズがそれぞれ異なる場合
    • Wordのウィンドウが異なるサイズのディスプレイ上に配置されている場合
 また、これは手元の環境特有かもしれないのですが、Word2002においては、ウィンドウを画面を最大化した場合に、Application.Width 及び.Heightプロパティでは最大化したウィンドウサイズを拾えない(通常時のウィンドウサイズを拾う)という挙動を示しました。もしかすると、ウィンドウを最大化して作業領域の大きさを取得するというテクニックはOfficeのVer.によっては使えないのかもしれません。