境界線を支配せよ:Visio VBAによる「真のオートフィット」実装の設計思想
業務自動化の現場において、ツールが「プロフェッショナル」か「単なるスクリプト」かを分ける境界線はどこにあるか?
それは、「例外的なデータ入力に対していかに優雅に振る舞えるか」という一点に集約される。
Visioを用いた図面生成自動化において、最も頻繁に発生する「美しくない」事象。それは、生成されたシェイプがページからはみ出している、あるいは逆に、広大な空白地帯がページに残されているという、レイアウトの不整合だ。
多くの開発者は、Visioの標準機能や、単純なループ処理でシェイプの座標を追うだけの「素人レベルのコード」を書いて、場当たり的な修正を繰り返す。しかし、我々エンジニアが目指すべきは、シェイプの集合体が持つ数学的なBounding Box(境界矩形)を計算し、ページサイズを動的に決定する堅牢なアルゴリズムである。
本記事では、単なるコピペ用コードの提供ではなく、オブジェクトのライフサイクルと計算の精度を考慮した、プロダクションレベルのオートフィット実装手法を伝授する。
1. なぜ「単純なループ」では失敗するのか
初心者が陥る典型的な設計ミスは、`Shape.X` や `Shape.Y` をそのままページサイズと比較しようとすることだ。
Visioの座標系において、`PinX` や `PinY` はシェイプの「中心点」を指す。シェイプのサイズ(Width/Height)を考慮せずに座標だけを抽出しても、シェイプの端がページを突き抜けている事実に気づけない。
また、大規模な図面(数千のシェイプが存在する場合)において、描画更新を制御せずに座標計算を繰り返すと、計算コストが指数関数的に増大し、アプリケーションがフリーズしたような挙動を示す。
設計者が考慮すべき変数は以下の3点だ:
1. 中心点座標ではなく、端点(Extremes)を計算しているか?
2. 余白(Margin)を数学的に定義しているか?
3. 描画更新(ScreenUpdating)を適切に制御しているか?
2. アルゴリズムの設計:Bounding Boxの抽出
実装すべきロジックのフローは以下の通りである。
1. 初期化: 全シェイプを走査するため、最小値(Min)を無限大、最大値(Max)をマイナス無限大として定義する。
2. 走査: 全シェイプの `PinX – Width/2` (左端), `PinX + Width/2` (右端), `PinY – Height/2` (下端), `PinY + Height/2` (上端) を計算し、全体の最小・最大を更新し続ける。
3. マージン適用: 計算された境界に、ユーザーが指定した余白を加算する。
4. ページ設定: 計算された幅と高さを、Visioの `PageSheet` に書き込む。
3. プロダクションレベルの実装例
以下に、実務でそのまま組み込める、エラーハンドリングとパフォーマンス最適化を施したコードを示す。
Option Explicit
''' <summary>
''' ページ内の全シェイプを解析し、ページサイズを動的に最適化する。
''' </summary>
''' <param name="marginPoints">ページ端からの余白(ポイント単位)</param>
Public Sub AutoFitPageToShapes(Optional ByVal marginPoints As Double = 36#)
Dim vsoPage As Visio.Page
Dim vsoShape As Visio.Shape
Dim minX As Double, maxX As Double
Dim minY As Double, maxY As Double
Dim currentLeft As Double, currentRight As Double
Dim currentBottom As Double, currentTop As Double
Dim hasShapes As Boolean
Set vsoPage = ActivePage
hasShapes = False
' パフォーマンス最適化: 描画更新を停止
Application.ScreenUpdating = False
On Error GoTo ErrorHandler
' 1. 境界値の初期化 (極端な値で初期化することで最初の比較を確実に通す)
minX = 1E+30: maxX = -1E+30
minY = 1E+30: maxY = -1E+30
' 2. 全シェイプの走査
' 注意: レイヤー等で非表示のシェイプを除外したい場合は、ここで条件分岐を入れる
For Each vsoShape In vsoPage.Shapes
' ページそのものや、バックグラウンドレイヤーのシェイプを無視するロジックが必要な場合がある
If vsoShape.CellsU("Type") <> visTypeGroup Then ' グループ化されたシェイプの扱いには注意
' 各シェイプの端点を計算 (PinX/PinYは中心点であることを念頭に置く)
currentLeft = vsoShape.CellsU("PinX").ResultIU - (vsoShape.CellsU("Width").ResultIU / 2)
currentRight = vsoShape.CellsU("PinX").ResultIU + (vsoShape.CellsU("Width").ResultIU / 2)
currentBottom = vsoShape.CellsU("PinY").ResultIU - (vsoShape.CellsU("Height").ResultIU / 2)
currentTop = vsoShape.CellsU("PinY").ResultIU + (vsoShape.CellsU("Height").ResultIU / 2)
' 全体の境界を更新
If currentLeft < minX Then minX = currentLeft
If currentRight > maxX Then maxX = currentRight
If currentBottom < minY Then minY = currentBottom
If currentTop > maxY Then maxY = currentTop
hasShapes = True
End If
Next vsoShape
' 3. シェイプが存在する場合のみページサイズを変更
If hasShapes Then
Dim newWidth As Double
Dim newHeight As Double
' 幅と高さの算出 + マージン
newWidth = (maxX - minX) + (marginPoints * 2)
newHeight = (maxY - minY) + (marginPoints * 2)
' ページ設定の更新
' VisioのPageSheetにおけるWidth/Heightはポイント単位
With vsoPage.PageSheet
.CellsU("PageWidth").ResultIU = newWidth
.CellsU("PageHeight").ResultIU = newHeight
' ページ内の図面を中央に配置するためのオフセット調整が必要な場合がある
' ここでは単純に、計算された境界の左下を基準にページを再設定する設計思想を取る
' 必要に応じて、PrintScale等の設定も検討せよ
End With
Debug.Print "Page Resized: W=" & newWidth & " H=" & newHeight
Else
Debug.Print "No shapes found. Skipping resize."
End If
CleanUp:
Application.ScreenUpdating = True
Exit Sub
ErrorHandler:
MsgBox "Error in AutoFitPageToShapes: " & Err.Description, vbCritical
Resume CleanUp
End Sub
4. アーキテクトの視点:実務における「落とし穴」
このコードをプロダクション環境に投入する際、以下の3点に留意せよ。これを知っているかどうかが、シニアエンジニアへの分水嶺となる。
A. レイヤーによる制御(Layer Filtering)
業務用のVisioファイルには、「印刷対象外の注釈レイヤー」や「設計用のガイドレイヤー」が存在することが多い。上記のコードは全シェイプを走査しているため、それらも計算に含めてしまう。
`If vsoShape.Layer.Name <> “NonPrintLayer” Then` のように、レイヤー属性に基づいたフィルタリングをロジックに組み込む設計が必須だ。
B. グループ化されたシェイプの再帰的探索
`vsoPage.Shapes` は、ページ直下のシェイプのみを返す。シェイプがグループ化されている場合、その内部にあるシェイプの境界を正しく取得するには、再帰的な探索(Recursive Search)が必要になる。大規模な図面を扱うなら、グループ内の子要素まで走査する関数を別途用意すべきだ。
C. データベース・外部連携時の整合性
もし、このオートフィット機能を「データベースから読み込んだデータに基づいて図面を自動生成するプロセス」の一部として組み込むなら、「計算結果のバリデーション」を忘れてはならない。
例えば、計算されたページサイズが、出力予定のPDF規格(A4, A3等)を大幅に逸脱する場合、エラーを投げるか、あるいは標準的な用紙サイズへスケーリングするロジックが必要になる。
結びに代えて
「動くコード」を書くのは簡単だ。しかし、「堅牢で、拡張性があり、例外に強いコード」を書くには、オブジェクトの性質を深く理解し、常に最悪のケース(空のページ、巨大なシェイプ、非表示レイヤー)を想定しなければならない。
自動化の本質は、人間が介在すべきではない「退屈な調整作業」から人間を解放することにある。そのための道具(ツール)を作る責任を、忘れないことだ。
