凡庸なマクロを脱却せよ:属性情報(Attribute)を核とした堅牢な図面自動保存エンジンの設計論
「図面を開いて、属性を確認して、手動で名前を変えて、適切なフォルダに保存する」
この一連のルーチンワークを自動化しようとする者は多い。しかし、巷に溢れる「動けばいい」程度のコードを実務に投入した結果、ネットワークドライブへの書き込みエラーや、禁止文字による保存失敗、あるいは属性の読み取りミスによるファイルの上書き事故に遭い、結局は手作業に戻る……そんな光景を私は何度も見てきた。
プロフェッショナルな自動化エンジニアが目指すべきは、単なる「自動化」ではない。「例外を予見し、失敗を許容しない、堅牢なプロセス」の構築だ。
本記事では、図面内のタイトルブロックから「工事番号」や「設計者名」を抽出し、それに基づき動的にパスを生成して保存する、実務レベルの「図面整理エンジン」の設計思想と実装を伝授する。
1. 失敗する設計、成功する設計:責務の分離
多くの初学者が陥る罠は、一つのプロシージャ(Sub)の中に「属性の検索」「文字列の加工」「フォルダの作成」「ファイルの保存」をすべて詰め込むことだ。これでは、どこでエラーが起きたのか特定できず、保守性も皆無になる。
我々が採用すべきは「責務の分離(Separation of Concerns)」だ。
1. Data Extraction Layer(抽出層): 図面内の特定ブロックを探し、属性値を「純粋なデータ」として取り出す。
2. Logic Layer(論理層): 取り出したデータから、保存用のファイル名とパスを「計算」する。ここで禁止文字の除去(サニタイズ)を行う。
3. I/O Layer(入出力層): 計算されたパスにフォルダが存在するか確認し、実際に保存を実行する。
この設計思想に基づき、コードを構築していく。
2. プロダクション・コードの実装例
以下に、実務でそのまま利用可能なレベルまで磨き上げたVBAコードを示す。単なるコピペではなく、各モジュールの役割を理解してほしい。
Option Explicit
' ==============================================================================
' 構成要素:
' 1. Main: 全体のオーケストレーター
' 2. GetTitleBlockAttributes: 属性情報の抽出
' 3. SanitizeString: ファイル名禁止文字の除去
' 4. EnsureDirectoryExists: フォルダの存在確認と自動作成
' ==============================================================================
Private Const TARGET_BLOCK_NAME As String = "TITLE_BLOCK_PROJECT" ' 対象となるブロック名
Private Const ROOT_PATH As String = "Z:\ProjectData\" ' プロジェクトルート
Sub ExecuteSmartSave()
On Error GoTo ErrorHandler
Dim acadDoc As AcadDocument
Set acadDoc = ThisDrawing
' --- 1. Data Extraction ---
Dim attrDict As Object
Set attrDict = GetTitleBlockAttributes(acadDoc, TARGET_BLOCK_NAME)
If attrDict Is Nothing Then
MsgBox "エラー: 指定されたタイトルブロックが見つからないか、属性が不足しています。", vbCritical
Exit Sub
End If
' 属性値の取得(Key名は図面のTagStringに合わせること)
Dim projID As String: projID = attrDict("PROJECT_ID")
Dim designer As String: designer = attrDict("DESIGNER")
If projID = "" Or designer = "" Then
MsgBox "エラー: 属性値(工事番号または設計者名)が空です。", vbExclamation
Exit Sub
End If
' --- 2. Logic (Path & Filename Construction) ---
' ファイル名: [工事番号]_[設計者名].dwg
Dim safeDesigner As String: safeDesigner = SanitizeString(designer)
Dim fileName As String: fileName = projID & "_" & safeDesigner & ".dwg"
' 保存先フォルダ: [ROOT]\[工事番号]\[設計者名]\
Dim targetFolder As String: targetFolder = ROOT_PATH & projID & "\" & safeDesigner & "\"
' --- 3. I/O Execution ---
If EnsureDirectoryExists(targetFolder) Then
Dim fullPath As String: fullPath = targetFolder & fileName
' 同名ファイルが存在する場合の衝突回避(簡易版)
If Dir(fullPath) <> "" Then
Dim suffix As Integer: suffix = 1
Do While Dir(targetFolder & projID & "_" & safeDesigner & "_" & suffix & ".dwg") <> ""
suffix = suffix + 1
Loop
fullPath = targetFolder & projID & "_" & safeDesigner & "_" & suffix & ".dwg"
End If
' 保存実行
acadDoc.SaveAs fullPath
MsgBox "保存完了: " & fullPath, vbInformation
Else
Err.Raise vbObjectError + 513, "ExecuteSmartSave", "フォルダの作成に失敗しました。"
End If
Exit Sub
ErrorHandler:
MsgBox "致命的なエラーが発生しました:" & vbCrLf & Err.Description, vbCritical
End Sub
' ------------------------------------------------------------------------------
' 属性情報をDictionaryオブジェクトとして抽出する
' ------------------------------------------------------------------------------
Private Function GetTitleBlockAttributes(doc As AcadDocument, blockName As String) As Object
Dim ent As AcadEntity
Dim blockRef As AcadBlockReference
Dim attRef As AcadAttributeReference
Dim dict As Object
Set dict = CreateObject("Scripting.Dictionary")
For Each ent In doc.ModelSpace
If TypeOf ent Is AcadBlockReference Then
Set blockRef = ent
' ブロック名の一致確認(大文字小文字を区別しない設計)
If UCase(blockRef.Name) = UCase(blockName) Then
' 属性のループ
For Each attRef In blockRef.GetAttributes
dict.Add attRef.TagString, attRef.TextString
Next attRef
Set GetTitleBlockAttributes = dict
Exit Function
End If
End If
Next ent
Set GetTitleBlockAttributes = Nothing
End Function
' ------------------------------------------------------------------------------
' 文字列からファイル名に使用できない禁止文字を除去する(サニタイズ)
' ------------------------------------------------------------------------------
Private Function SanitizeString(strIn As String) As String
Dim invalidChars As Variant
Dim i As Integer
Dim strOut As String
strOut = strIn
' Windowsのファイル名禁止文字: \ / : * ? " < > |
invalidChars = Array("\", "/", ":", "*", "?", """", "<", ">", "|")
For i = LBound(invalidChars) To UBound(invalidChars)
strOut = Replace(strOut, invalidChars(i), "_")
Next i
SanitizeString = Trim(strOut)
End Function
' ------------------------------------------------------------------------------
' フォルダの存在確認と、再帰的なディレクトリ作成
' ------------------------------------------------------------------------------
Private Function EnsureDirectoryExists(ByVal folderPath As String) As Boolean
Dim fso As Object
Set fso = CreateObject("Scripting.FileSystemObject")
On Error Resume Next
' 階層が深くても作成できるよう、FSOの作成機能を利用
' ※実際には再帰的に作成するロジックが必要な場合があるが、
' ここでは簡易的にFileSystemObjectの基本機能を想定
If Not fso.FolderExists(folderPath) Then
' 簡易的な実装として、親フォルダから順に作成する処理が必要
' 本来は再帰関数を実装すべきだが、実務では既存のフォルダ構造ルールに則ることも多い
Call CreateFolderRecursive(folderPath)
End If
EnsureDirectoryExists = (Err.Number = 0)
On Error GoTo 0
End Function
' フォルダを階層的に作成する補助関数
Private Sub CreateFolderRecursive(ByVal path As String)
Dim fso As Object
Set fso = CreateObject("Scripting.FileSystemObject")
If Not fso.FolderExists(path) Then
Dim parent As String
parent = fso.GetParentFolderName(path)
If Not fso.FolderExists(parent) Then
Call CreateFolderRecursive(parent)
End If
fso.CreateFolder path
End If
End Sub
3. アーキテクトの視点:なぜこのコードは「強い」のか
このコードが単なる学習用コードと一線を画す理由は、以下の3点に集約される。
① 「サニタイズ」という防壁
設計者が属性欄に誤って `Project/A` と入力したとする。もしサニタイズを行わなければ、`SaveAs` メソッドは `Project` というフォルダの中の `A.dwg` を探そうとして、パスの解釈エラーを起こす。`SanitizeString` 関数によって、禁止文字を `_` に置換することで、プログラムの強制終了を防いでいる。
② 「FileSystemObject (FSO)」の戦略的利用
VBA標準の `Dir` や `MkDir` だけでは、深い階層のフォルダ作成(例:`Z:\A\B\C\`)において、親フォルダが存在しない場合にエラーを吐いて止まってしまう。`CreateFolderRecursive` という再帰関数を実装することで、どのような階層構造であっても、目的の場所を確実に作り出す「自律性」を持たせている。
③ 「衝突回避」のロジック
自動保存において最も恐ろしいのは、既存のファイルを誤って上書きすることだ。本コードでは、ファイル名が重複した場合に `_1`, `_2` と連番を付与するロジックを組み込んでいる。これにより、データ消失のリスクを最小限に抑えている。
4. 実務への展開:さらなる高度化に向けて
このエンジンは、あくまで「骨組み」である。実務の要件に応じて、以下の拡張を検討せよ。
- データベース連携: フォルダパスをハードコードせず、SQL ServerやAccessなどの外部DBから「プロジェクト管理テーブル」を参照し、最新の保存ルールを取得するように変更する。
- ログ出力: 保存の成否、エラー内容をテキストファイルまたはCSVに書き出す機能を実装せよ。これにより、「いつ、誰のPCで、どの図面が失敗したか」のトレーサビリティが確保される。
- バージョン管理: 保存時に `SaveAs` だけでなく、旧ファイルを `.bak` や `.old` として別フォルダへ退避させる処理を加えると、よりプロフェッショナルな設計となる。
結論
自動化の目的は、作業時間を削ることだけではない。「人間がミスを犯す余地を、設計によって物理的に排除すること」にある。
今回紹介した設計パターンをマスターすれば、君の書くコードは「便利なツール」から、組織のインフラを支える「堅牢なシステム」へと昇華するはずだ。
