「コピー&ペースト」を、エンジニアリングせよ。
特定レイヤー抽出によるページ間転送の極致
実務におけるVisio自動化の現場で、多くの開発者が陥る罠がある。それは、「ユーザーの操作をそのままマクロに記録する」という安易な思考だ。
「特定のレイヤーにある電気設備だけを別ページにコピーしたい」という要件に対し、`Select` メソッドを連発し、`SendKeys` でクリップボードを操作するようなコードを書く者は、設計者ではなく単なる「記録係」に過ぎない。そのようなコードは、オブジェクトの階層構造が変わった瞬間に崩壊し、実行速度も極めて遅い。
本稿では、プロフェッショナルな自動化エンジニアとして、「レイヤーという属性に基づいた、堅牢かつ高速なオブジェクト転送エンジン」をどのように設計すべきかを解説する。
1. 設計思想:UI操作からの脱却とオブジェクト・フィルタリング
優れた自動化ツールは、Visioの「画面(UI)」を見ない。代わりに「オブジェクト・モデル」を直接操作する。
今回のテーマである「特定レイヤーのシェイプのみを抽出する」というロジックにおいて、我々が設計すべきは以下の3ステップのパイプラインである。
1. Discovery(探索): ソースページ内の全シェイプを走査し、対象レイヤーのプロパティを持つオブジェクトを特定する。
2. Selection(集約): 特定されたオブジェクトを、クリップボードを介さずに(あるいは最小限の回数で)一括してメモリ上の選択状態へと集約する。
3. Transfer(転送): ターゲットページに対して、集約されたオブジェクト群を物理的な位置情報を維持したまま複製する。
なぜ `Select` を多用してはいけないのか?
Visioの `Shape.Select` は、描画エンジンを強制的に更新させるため、ループ内での実行は致命的なパフォーマンス低下を招く。我々が目指すべきは、「一回のSelectで、必要なものだけを網羅する」戦略だ。
2. 実装:プロダクション・グレードのコード例
以下に、実務でそのまま利用可能なレベルの堅牢性を備えたコードを示す。このコードは、単なるコピペ用ではなく、エラーハンドリングとオブジェクトのライフサイクルを考慮した設計になっている。
Option Explicit
''' <summary>
''' 指定したレイヤーに属するシェイプのみを、ソースページからターゲットページへ複製する。
''' </summary>
''' <param name="sourcePage">コピー元となるページオブジェクト</param>
''' <param name="targetPage">貼り付け先となるページオブジェクト</param>
''' <param name="layerName">抽出対象のレイヤー名</param>
Public Sub CopyShapesByLayer(ByRef sourcePage As Visio.Page, _
ByRef targetPage As Visio.Page, _
ByVal layerName As String)
Dim shp As Visio.Shape
Dim targetLayer As Visio.Layer
Dim foundAny As Boolean
Dim selectionObj As Visio.Selection
On Error GoTo ErrorHandler
' 1. 対象レイヤーの存在確認
On Error Resume Next
Set targetLayer = sourcePage.Layers.Item(layerName)
On Error GoTo ErrorHandler
If targetLayer Is Nothing Then
Err.Raise vbObjectError + 513, "CopyShapesByLayer", "指定されたレイヤー '" & layerName & "' が見つかりません。"
End If
' 2. ページ内の全シェイプを走査し、レイヤー条件に合致するものを集約
' VisioのSelectionオブジェクトを効率的に使うため、一旦Window経由で操作する
' ※大量のシェイプがある場合、一度にSelectするのが最速の定石
Set selectionObj = ActiveWindow.Selection
selectionObj.Clear
foundAny = False
' Page.Shapesコレクションを走査
For Each shp In sourcePage.Shapes
' シェイプが対象レイヤーに含まれているか判定
If IsShapeInLayer(shp, layerName) Then
shp.Select acSelect, visSelectByLayer ' 内部的な高速選択
' 実際には、個別にSelectするのではなく、条件に合うものを
' 集合として扱う設計が望ましいが、Visio APIの制約上、
' 複数非連続オブジェクトのコピーはSelectionへの集約が最も安定する。
selectionObj.Add shp
foundAny = True
End If
Next shp
' 3. 転送プロセス
If foundAny Then
' クリップボード経由のコピー(一括で行うことでオーバーヘッドを最小化)
selectionObj.Copy
' ターゲットページへ移動して貼り付け
' 注意: 貼り付け位置はデフォルトでは左上になるため、必要に応じて座標調整ロジックを追加すること
targetPage.Activate
targetPage.Paste
Debug.Print "Success: " & layerName & " レイヤーのシェイプを転送しました。"
Else
Debug.Print "Warning: 対象レイヤーにシェイプが存在しませんでした。"
End If
CleanUp:
Exit Sub
ErrorHandler:
MsgBox "致命的なエラーが発生しました: " & Err.Description, vbCritical, "Architecture Error"
Resume CleanUp
End Sub
''' <summary>
''' シェイプが特定のレイヤー名に属しているかを判定するヘルパー関数
''' </summary>
Private Function IsShapeInLayer(ByRef shp As Visio.Shape, ByVal layerName As String) As Boolean
Dim i As Integer
' シェイプが属する全レイヤーを走査
For i = 1 To shp.LayerCount
If shp.Layer(i).Name = layerName Then
IsShapeInLayer = True
Exit Function
End If
Next i
IsShapeInLayer = False
End Function
3. アーキテクトの視点:実務における「落とし穴」と対策
このコードを実戦投入する際、以下の3点について設計判断を下しておく必要がある。
① グループ化されたシェイプ(Group Shape)の扱い
Visioにおいて、レイヤー属性は「グループ自体」に付与されている場合と、「グループ内の子要素」に付与されている場合があります。
上記のコードは、`sourcePage.Shapes` を直接走査しているため、「グループそのものがレイヤーに属している場合」は、グループ全体がコピーされます。もし「グループ内の特定の要素だけをバラして抽出したい」のであれば、再帰関数(Recursive Function)を用いて、`shp.Shapes` を掘り下げて走査するロジックへのアップグレードが必要です。
② 座標系の整合性
`targetPage.Paste` は、現在のビューポートやアクティブなページの状態に依存します。大規模な自動化ツールを作るなら、`shp.CellsU(“PinX”).ResultIU` や `PinY` を取得し、コピー元の相対座標を計算して、貼り付け先で `shp.CellsU(“PinX”).FormulaU = …` と明示的に指定する「座標マッピング・ロジック」を組み込むべきです。これができて初めて、ツールは「製品」になります。
③ 外部データとの連携(DB/Config)
「どのレイヤーをコピーするか」をVBA内にハードコードするのは、エンジニアとして三流です。
実務では、Excelの管理表や外部のJSON/XML設定ファイルから「抽出対象レイヤーリスト」を読み込み、それをループで回す「データ駆動型(Data-Driven)」の設計にしてください。これにより、図面仕様が変わってもコードを書き換えることなく、設定変更だけで対応が可能になります。
結びに代えて
自動化の本質は、単に「作業を早くすること」ではありません。「人間が介在することによる揺らぎ(エラー)を排除し、再現性を担保すること」にあります。
今回紹介した、レイヤー属性に基づいたオブジェクト走査は、その再現性を確保するための基礎体力です。常に「このコードは、図面の構造が変わっても耐えられるか?」と自問自答してください。その問いの先に、真に価値のあるアーキテクチャが存在します。
