【実務・中級編】【実務中級者向け】図面内の「特定ブロック」の有無で保存先を自動振り分け:属性情報を利用したフォルダ整理術 – AutoCAD VBA解析バイブル

スポンサーリンク
スポンサーリンク

凡庸なマクロを脱却せよ:属性情報(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` として別フォルダへ退避させる処理を加えると、よりプロフェッショナルな設計となる。

結論

自動化の目的は、作業時間を削ることだけではない。「人間がミスを犯す余地を、設計によって物理的に排除すること」にある。

今回紹介した設計パターンをマスターすれば、君の書くコードは「便利なツール」から、組織のインフラを支える「堅牢なシステム」へと昇華するはずだ。

タイトルとURLをコピーしました