先日、特定のエクセルウィンドウだけを並べて表示するVBA関数を作ったので紹介する。
きっかけ
読者の皆さんはVBAで2つのエクセルのウィンドウを並べたいと思ったことはないだろうか?
あるいは3つ並べて表示したい時はないだろうか?
整列させるだけなら、専用の命令が用意されており、Windows.Arrangeメソッド を実行するだけで完了である。
つまり、な~んにも、難しい話ではない。
が
実は、実際の現場では使いずらい重大な欠陥を抱えている。
それを回避するために試行錯誤した結果、API等を使わずに期待通りの動きを実現できたので、紹介しておくことにした。
Windows.Arrangeメソッド
docs.microsoft.com - Windows.Arrangeメソッド
例えば、下記のVBAコードを実行すると、Excelウィンドウが横並びに配置される。
Windows.Arrange xlArrangeStyleVertical
これはエクセルの「表示」>「整列」>「左右に並べて表示」に相当するコマンドである。
エクセルウィンドウが2つなら、下図のように50%:50%に整列される。
構文
Windows.Arrange ArrangeStyle, Activeworkbook, SyncHorizontal, syncvertical)
引数
名前 | 必須 / オプション | データ型 | 説明 |
---|---|---|---|
ArrangeStyle | 省略可能 | XlArrangeStyle | ウィンドウの並べ替え方法を XlArrangeStyle クラスの定数のいずれかで指定します。 |
ActiveWorkbook | 省略可能 | Variant | True を指定すると、作業中のブックのウィンドウのうち、現在表示されているウィンドウだけが整列されます。 False を指定すると、Excel のすべてのウィンドウが整列されます。 既定値は False です。 |
SyncHorizontal | 省略可能 | Variant | ActiveworkbookがFalseの場合、または省略された場合は無視されます。 True を指定すると、作業中のブックのウィンドウのいずれかを左右にスクロールするときに、他のウィンドウも同時にスクロールします。 False を指定した場合は、ウィンドウの同時スクロールは行われません。 既定値は False です。 |
SyncVertical | 省略可能 | Variant | ActiveworkbookがFalseの場合、または省略された場合は無視されます。 True を指定すると、作業中のブックのウィンドウのいずれかを上下にスクロールするときに、他のウィンドウも同時にスクロールします。 False を指定した場合は、ウィンドウの同時スクロールは行われません。 既定値は False です。 |
XlArrangeStyle列挙体
定数 | 整列方法 |
---|---|
xlArrangeStyleCascade | 重ねて表示 |
xlArrangeStyleTilede | 並べて表示 |
xlArrangeStyleHorizontal | 上下に並べて表示 |
xlArrangeStyleVertical | 左右に並べて表示 |
Windows.Arrangeの問題点と対処法
さて、下記のコードの何が不満か。実際に使った事がある人なら知っているかもしれない。
Windows.Arrange xlArrangeStyleVertical
1.全てのExcelウィンドウが整列されてしまう
最大の課題はこれではないだろうか。
Excelをアプリとして開発した時にこまるのが、ユーザーがシステムに関係ないExcelブックも開いていることだと思う。
これによって、環境依存・タイミング依存の不具合が起こることがよくある。
Windows.Arrange
メソッドも代表的な課題を抱えている機能の1つで、無関係なExcelウィンドウも整列の対象として含めてしまうのである。
例えば、並べたいのが2つでも、裏でもう一つエクセルを開いていたなら、3つなら33%,33%,33%となる。
対処法
Windows.Arrange
は「表示されているウィンドウしか整列しない」という性質がある。
従って実行する前に非表示にして、実行が終わってから再表示すれば良いのだ。
方法としては2つある。
Window.Visible = False
(非表示にする)Window.WindowState = xlMinimized
(最小化する)
更に、次項で説明するが、「マルチディスプレイ環境ではデスクトップ毎に整列が行われる」という性質もある。
つまり、
Window.Top = ????
,Window.Left = ????
でウィンドウを別のデスクトップに移動させる
という方法も考えられる。
方法は3パターンあるわけだが、
今回はWindow.Visible = False
(非表示にする)を採用した。
理由は、
- 最小化はWindowsのアニメーションが発生し、低速化しやすいと考えられるから。
- 別のデスクトップに移動させる処理が複雑になりそうだから。
- 無関係なウィンドウ配置を勝手に触るのは不親切だから。
というわけで、実際に実行する順番としては
- 不要なウィンドウを非表示にする
- Windows.Arrangeを実行する
- 非表示にしたウィンドウを復元する
となるが、もう一つ問題が発生する。
対象外のウィンドウを復元したことにより本命のウィンドウよりも前に表示されてしまうのだ。
だから、追加で
- 整列対象のウィンドウをアクティブにする
が必要となる。
2.マルチディスプレイ環境でデスクトップ毎に整列が行われてしまう
ウィンドウの整列は、デスクトップ毎に行われる。これは利便性を考えると仕方がない事だと思う。
ただ、昔はこれが問題となることは無かったと思う。
Excel2010まではExcelはMDIアプリケーションだったからだ。
https://ja.wikipedia.org/wiki/Multiple_Document_Interface
MDIではデスクトップ関係なく、巨大なExcelという矩形エリアが一つだけあって、その枠の中で整列を行えば良かった。だからマルチディスプレイ環境を考慮したプログラムは必要なかった。
ところが、Excel 2013からはSDIに変更されてしまった。
https://ja.wikipedia.org/wiki/Single_Document_Interface
MDIからSDIに変わったことでどんな問題が起きたかというと、マルチディスプレイに対応しなければならなくなったということである。
しかしSDIでは、ディスプレイ毎に異なる大きさのデスクトップが多数存在するようになってしまった。
Excelからしてみれば、どのデスクトップで整列すればええんや?って状態だろう。私だってそう思う。
なお、SDIに伴い問題は色々起きているが、エクセルの操作性は格段に良くなっており私は気に入っている。
動作テストで古いExcelを触ることもあるが、使いづらくて仕方がない。
マルチディスプレイ環境での Windows.Arrange
の動きは、次のようになっている。
- 各ウィンドウの左上座標がどのデスクトップに属するかを確認
- デスクトップ毎のウィンドウの数をカウント
- デスクトップ毎にウィンドウを整列
対処法
以上の結果より、並べて表示したいウィンドウが異なるデスクトップにあるならば、事前に同じデスクトップに持ってこさせる必要がある。
今回はターゲットのオブジェクト配列のうち、先頭ウィンドウの存在するデスクトップを基準とすることとした。
従って以下の様な手順になった。
- 整列対象の1つ目のウィンドウの座標を取得
- 2番目以降のウィンドウは左上座標を変更
Windows.Arrange
を実行
余談だが、この整列機能はOSに組み込まれているウィンドウ整列機能を呼び出しているのだと思われる。
昔はタスクを右クリックするだけでコンテキストメニューが表示できたが、「ジャンプリスト」が搭載されてからはお目にかかることがなくなってしまった。
だが、完全に死んだわけではないようで、タスクバーにて「グループ化」された状態のタスクをShift+右クリックしすると、昔ながらのコンテキストメニューが表示される。
ここから、左右に並べて表示したり、上下に整列したりができる。
さらに、昔はタスクバーCtrl+左クリックでの任意の窓だけを選ぶことができた。
ところが今ではタスクを選ぶ機能が消失しており、グループ化済みのグループに対してしか整列が出来なくなってしまっている。
今では、デスクトップ端に持っていけば自動拡大する機能が増えており便利になったとは言え、この機能は今でも復活して欲しいと願う。
当時、この機能の消失に関して誰も騒いでいないのが凄くショックだったので、つい思い出を書いてしまった。
3.ウィンドウの大きさの比率が選べない。
先に言っておくと、今回のプログラムでは本件の課題には対応していない。
ExcelVBAでウィンドウの整列を行う時に欲しいのが、ウィンドウごとのサイズ調整ではないだろうか。
元に私が過去に開発したツールでは、メインウィンドウの大きさは7割。チェックウィンドウの大きさは3割程度で表示したいということがよくあった。
しかし、Windows.Arrangeは常に同じ比率でしかウィンドウを整列してくれない。
対処法
下記の関数は汎用化を重視しており搭載にかかるコストが目的に合わなかったので実装しなかったが、
・ウィンドウ数を2に固定
・サブウィンドウの幅を400ポイントに固定
などと条件を絞れば比較的簡単に実装できる。
4.利便性の問題
これはArrangeの欠陥ではない。単に私が使いやすいように拡張したという話である。
普通に関数を作るとしたら、こうなるだろう。
Sub ExcelWindowArrange(Window配列, 配置スタイル)
だが、本関数はこうである。
Sub ExcelWindowArrange(複数のターゲットオブジェクト, 配置スタイル)
何がどう違うのか。それはソースコードをよく見れば分かる。
For Each obj In targets Select Case TypeName(obj) Case "Window": arrangeWindows.Add obj Case "Workbook": arrangeWindows.Add obj.Windows(1) Case "Worksheet": arrangeWindows.Add obj.Parent.Windows(1) '他の型への対応は必要なら作る Case Else: Debug.Print PROC_NAME, "No Defined TypeName: " & TypeName(obj): Stop End Select Next
要するに、「複数のターゲットオブジェクト」は
- 配列でもコレクションでもOK
- オブジェクトはWindowでも、Workbookでも、WorksheetでもOK
という、極めて高い柔軟性を備えている。
ちなみに配列の其々の要素の型が違っていても問題なく対応できる。
本来、ブックやシートからウィンドウは一意には決まらないが、「大半は2つも開かない」という理由で常に「1」を採用している。
未対応の場合は「Stop」で開発者向けにブレイクするのだ。配布用ならコメントアウトか、Err.Raiseでも良いと思う。
ソースコード
参考資料
開発途中のツイート
特定のエクセルウィンドウだけを並べて表示する汎用プロシージャが完成したやで
— ことりちゅん@えくせるちゅんちゅん (@KotorinChunChun) 2020年2月15日
注目
・2つ以上の窓に対応
・マルチディスプレイに対応
・No API
・引数
欠点
・大量に窓を開いていると一斉に最小化・復元をするので重いかも(←API使わないと無理ゲー) pic.twitter.com/UHm1Irr6FS
ロジックを整理(前処理と、本命の処理を分離)して分かりやすくしてみた。 pic.twitter.com/PnnEnqGJpQ
— ことりちゅん@えくせるちゅんちゅん (@KotorinChunChun) 2020年2月15日
個人的にはウィンドウ幅の比率orポイントを微調整しないと実用的にならない。
— ことりちゅん@えくせるちゅんちゅん (@KotorinChunChun) 2020年2月15日
でも、そのレベルの操作を行う関数は、既にAPIで作っているので、遊びプロシージャにまで導入する気にならず保留
まとめ
今回はAPI等の難しいプログラムを使わずに、Excelの純粋な命令だけで整列を制御してみた。
厳密にやろうとするならば、APIを駆使した関数を作ったほうが良いと思う。(かなり苦労したが、私は過去にAPI方式の関数を開発している)
しかし大量のウィンドウを操作しなければならなず、速度を求められる場面は稀だと思うので、大抵はこの関数でも十分耐えられると思う。
興味のある人は、APIにも挑戦してみて欲しい。
なお、この関数は先日開発した下記のアドインでも使用した。
ウィンドウの整列はVBAの処理の実行結果の表示などで重宝している。
以上
何か御座いましたらコメント欄、またはTwitterからどうぞ♪
それではまた来週♪ ちゅんちゅん(・8・)