【VBA】複数ファイルの収集

処理の流れ

マクロ FileCopyMain を実行すると、以下の順番で動きます。

1. 入力チェック シートのB3(コピー元)とB4(コピー先)を読み取り、空欄がないか、コピー先が C:\D:\ などローカルドライブかを確認します。\\server\... のようなネットワークパスが指定されていたらブロックします。

2. フォルダ存在チェック コピー元フォルダが実際に存在するか確認します。コピー先フォルダが存在しない場合は「作成しますか?」と聞いてきます。

3. コピー元のファイル一覧をキャッシュ コピー元フォルダの中身を一度だけ全部読み込んで、メモリ上のリスト(Dictionary)に保存します。これが「キャッシュ」です(詳しくは後述)。

4. 実行確認ダイアログ コピー元、コピー先、対象ファイル数をまとめたメッセージボックスが出ます。「はい」を押さない限りコピーは始まりません。

5. 上書き確認 コピー先に同名ファイルがある場合、その一覧を表示して「上書きしてよろしいですか?」と確認します。

6. コピー実行 ファイルを1つずつコピーしながら、ステータスバーに進捗(「3/20 コピー中…」のように)を表示します。各ファイルの結果はC列に緑(成功)または赤(失敗・見つからない)で書き込まれます。

7. 完了報告 成功数・失敗数をまとめたメッセージが表示されます。

Dictionaryの検索

Dir()でフォルダ内のファイル名を1つずつ取得してDictionaryに入れていく処理ですが、これはファイル名の文字列を読み取るだけで、ファイルの中身を開いたりしません。100個程度のファイル名取得はネットワーク越しでも1回の通信で済むことがほとんどなので、体感としてはエクスプローラでそのフォルダを開くのと同じくらいの待ち時間です。

キャッシュ後の検索は、Dictionaryがハッシュテーブルという仕組みで動いているため、100個だろうが1000個だろうが1回の検索はほぼ一瞬です。20ファイル分の検索を合わせてもミリ秒レベルで終わります。

'=============================================================
' ファイルコピーツール VBAマク
' 使い方: Alt+F11 → 挿入 → 標準モジュール → このコードを貼り付
'=============================================================

Option Explicit

' --- 定数 ---
Private Const SHEET_NAME As String = "ファイルコピー"
Private Const ROW_SRC As Long = 3       ' コピー元パス
Private Const ROW_DST As Long = 4       ' コピー先パス
Private Const COL_PATH As Long = 2      ' パス列 (B列)
Private Const ROW_FILE_START As Long = 8 ' ファイル名開始
Private Const COL_NO As Long = 1        ' No.列 (A列)
Private Const COL_FILE As Long = 2      ' ファイル名列 (B列)
Private Const COL_RESULT As Long = 3    ' 結果列 (C列)

'=============================================================
' メインルーチ
'=============================================================
Public Sub FileCopyMain()

    Dim ws As Worksheet
    Dim srcFolder As String
    Dim dstFolder As String
    Dim fileNames() As String
    Dim fileCount As Long
    Dim i As Long

    On Error GoTo ErrHandler

    ' シート取
    Set ws = ThisWorkbook.Worksheets(SHEET_NAME)

    ' パス取得・整
    srcFolder = Trim(CStr(ws.Cells(ROW_SRC, COL_PATH).Value))
    dstFolder = Trim(CStr(ws.Cells(ROW_DST, COL_PATH).Value))

    ' 末尾に \ を付
    If Right(srcFolder, 1) <> "\" Then srcFolder = srcFolder & "\"
    If Right(dstFolder, 1) <> "\" Then dstFolder = dstFolder & "\"

    ' --- バリデーション ---

    ' 空チェッ
    If srcFolder = "\" Or dstFolder = "\" Then
        MsgBox "コピー元フォルダとコピー先フォルダを入力してください。", vbExclamation, "入力エラー"
        Exit Sub
    End If

    ' コピー先がローカルドライブかチェッ
    If Not IsLocalPath(dstFolder) Then
        MsgBox "コピー先はローカルドライブ(C:\, D:\ 等)を指定してください。" & vbCrLf & _
               "ネットワークパス(\\で始まるパス)は指定できません。" & vbCrLf & vbCrLf & _
               "現在の指定: " & dstFolder, vbExclamation, "コピー先エラー"
        Exit Sub
    End If

    ' コピー元フォルダ存在チェッ
    If Dir(srcFolder, vbDirectory) = "" Then
        MsgBox "コピー元フォルダが見つかりません:" & vbCrLf & srcFolder, vbExclamation, "パスエラー"
        Exit Sub
    End If

    ' コピー先フォルダ存在チェッ
    If Dir(dstFolder, vbDirectory) = "" Then
        Dim ans As VbMsgBoxResult
        ans = MsgBox("コピー先フォルダが存在しません。作成しますか?" & vbCrLf & dstFolder, _
                     vbYesNo + vbQuestion, "フォルダ作成")
        If ans = vbYes Then
            MkDirRecursive dstFolder
        Else
            Exit Sub
        End If
    End If

    ' ファイル名一覧を取
    fileCount = GetFileList(ws, fileNames)

    If fileCount = 0 Then
        MsgBox "コピー対象ファイルが1つも入力されていません。", vbExclamation, "入力エラー"
        Exit Sub
    End If

    ' 結果列クリ
    ClearResults ws

    ' --- 実行前確認 ---
    Dim confirmMsg As String
    confirmMsg = "以下の内容でファイルコピーを実行します。" & vbCrLf & vbCrLf
    confirmMsg = confirmMsg & "【コピー元】" & vbCrLf & srcFolder & vbCrLf & vbCrLf
    confirmMsg = confirmMsg & "【コピー先】" & vbCrLf & dstFolder & vbCrLf & vbCrLf
    confirmMsg = confirmMsg & "【対象ファイル数】" & fileCount & "" & vbCrLf & vbCrLf
    confirmMsg = confirmMsg & "実行してよろしいですか?"

    If MsgBox(confirmMsg, vbYesNo + vbQuestion, "実行確認") <> vbYes Then
        Exit Sub
    End If

    ' --- コピー元ファイル一覧をキャッシュ(ネットワーク負荷軽減) ---
    ' ネットワークドライブは Dir() が遅いため、1回だけ列挙してDictionaryに格
    Application.StatusBar = "コピー元フォルダを読み込み中(ネットワークドライブの場合、時間がかかります)..."
    DoEvents

    Dim srcFileDict As Object
    Set srcFileDict = CreateObject("Scripting.Dictionary")
    srcFileDict.CompareMode = vbTextCompare  ' 大文字小文字を無

    Dim dirFile As String
    dirFile = Dir(srcFolder & "*.*")
    Do While dirFile <> ""
        If Not srcFileDict.Exists(LCase(dirFile)) Then
            srcFileDict.Add LCase(dirFile), dirFile  ' Key=小文字, Value=実際の名
        End If
        dirFile = Dir()
    Loop
    Application.StatusBar = False

    ' --- 既存ファイル上書き確認 ---
    Dim overwriteList As String
    Dim overwriteCount As Long
    overwriteCount = 0
    overwriteList = ""

    For i = 0 To fileCount - 1
        Dim actualName As String
        actualName = FindInCache(srcFileDict, fileNames(i))
        If actualName <> "" Then
            If Dir(dstFolder & actualName) <> "" Then
                overwriteCount = overwriteCount + 1
                overwriteList = overwriteList & "  " & actualName & vbCrLf
            End If
        End If
    Next i

    If overwriteCount > 0 Then
        Dim owMsg As String
        owMsg = "コピー先に以下の " & overwriteCount & " ファイルが既に存在します。" & vbCrLf
        owMsg = owMsg & "上書きしてよろしいですか?" & vbCrLf & vbCrLf
        owMsg = owMsg & overwriteList

        If MsgBox(owMsg, vbYesNo + vbExclamation, "上書き確認") <> vbYes Then
            Exit Sub
        End If
    End If

    ' --- コピー実行 ---
    Application.StatusBar = "ファイルコピー処理中..."
    Application.ScreenUpdating = False

    Dim successCount As Long, failCount As Long, skipCount As Long
    successCount = 0: failCount = 0: skipCount = 0

    For i = 0 To fileCount - 1
        Dim rowNum As Long
        rowNum = ROW_FILE_START + i

        DoEvents  ' UIの応答を維
        Application.StatusBar = "コピー中... (" & (i + 1) & "/" & fileCount & ") " & fileNames(i)

        ' キャッシュから大文字小文字を無視して検
        actualName = FindInCache(srcFileDict, fileNames(i))

        If actualName = "" Then
            ' ファイルが見つからな
            SetResult ws, rowNum, "見つかりません", RGB(200, 0, 0)
            failCount = failCount + 1
        Else
            ' コピー実
            On Error Resume Next
            FileCopy srcFolder & actualName, dstFolder & actualName

            If Err.Number = 0 Then
                SetResult ws, rowNum, "コピー完了", RGB(0, 128, 0)
                successCount = successCount + 1
            Else
                SetResult ws, rowNum, "エラー: " & Err.Description, RGB(200, 0, 0)
                failCount = failCount + 1
                Err.Clear
            End If
            On Error GoTo ErrHandler
        End If
    Next i

    Application.ScreenUpdating = True
    Application.StatusBar = False

    ' --- 完了メッセージ ---
    Dim resultMsg As String
    resultMsg = "ファイルコピーが完了しました。" & vbCrLf & vbCrLf
    resultMsg = resultMsg & "  成功: " & successCount & "" & vbCrLf
    resultMsg = resultMsg & "  失敗: " & failCount & "" & vbCrLf
    resultMsg = resultMsg & "  合計: " & fileCount & ""

    If failCount > 0 Then
        MsgBox resultMsg, vbExclamation, "完了(一部エラーあり)"
    Else
        MsgBox resultMsg, vbInformation, "完了"
    End If

    Exit Sub

ErrHandler:
    Application.ScreenUpdating = True
    Application.StatusBar = False
    MsgBox "予期しないエラーが発生しました。" & vbCrLf & vbCrLf & _
           "エラー番号: " & Err.Number & vbCrLf & _
           "内容: " & Err.Description, vbCritical, "エラー"
End Sub

'=============================================================
' ローカルパス判定(ドライブレターで始まるか
'=============================================================
Private Function IsLocalPath(ByVal path As String) As Boolean
    IsLocalPath = False
    If Len(path) >= 2 Then
        Dim firstChar As String
        firstChar = UCase(Left(path, 1))
        If firstChar >= "A" And firstChar <= "Z" Then
            If Mid(path, 2, 1) = ":" Then
                ' ネットワークドライブのマッピングも除外したい場合
                ' 以下のチェックを有効にしてくださ
                ' (ただし、マッピングされたドライブはローカル扱いにする場合はコメントアウト
                '
                ' Dim drv As String
                ' drv = Left(path, 2)
                ' Dim fso As Object
                ' Set fso = CreateObject("Scripting.FileSystemObject")
                ' If fso.DriveExists(drv) Then
                '     If fso.GetDrive(drv).DriveType = 3 Then ' Network
                '         IsLocalPath = False
                '         Exit Function
                '     End If
                ' End If

                IsLocalPath = True
            End If
        End If
    End If
End Function

'=============================================================
' キャッシュ済みDictionaryからファイルを検索(大文字小文字無視
' ネットワークドライブへのDir()は1回だけに抑えるための仕組
'=============================================================
Private Function FindInCache(ByVal dict As Object, ByVal targetName As String) As String
    Dim key As String
    key = LCase(Trim(targetName))
    If dict.Exists(key) Then
        FindInCache = dict(key)  ' 実際のファイル名を返
    Else
        FindInCache = ""
    End If
End Function

'=============================================================
' ファイル名一覧を取
'=============================================================
Private Function GetFileList(ByVal ws As Worksheet, ByRef fileNames() As String) As Long
    Dim count As Long
    count = 0
    Dim row As Long
    row = ROW_FILE_START

    ' まず件数をカウン
    Do While row <= 1000  ' 安全上
        Dim val As Variant
        val = ws.Cells(row, COL_FILE).Value
        If IsEmpty(val) Or Trim(CStr(val)) = "" Then
            Exit Do
        End If
        count = count + 1
        row = row + 1
    Loop

    If count = 0 Then
        GetFileList = 0
        Exit Function
    End If

    ReDim fileNames(0 To count - 1)

    Dim i As Long
    For i = 0 To count - 1
        fileNames(i) = Trim(CStr(ws.Cells(ROW_FILE_START + i, COL_FILE).Value))
    Next i

    GetFileList = count
End Function

'=============================================================
' 結果列をクリ
'=============================================================
Private Sub ClearResults(ByVal ws As Worksheet)
    Dim row As Long
    row = ROW_FILE_START
    Do While row <= 1000
        If IsEmpty(ws.Cells(row, COL_FILE).Value) Or Trim(CStr(ws.Cells(row, COL_FILE).Value)) = "" Then
            Exit Do
        End If
        ws.Cells(row, COL_RESULT).Value = ""
        ws.Cells(row, COL_RESULT).Font.Color = RGB(0, 0, 0)
        row = row + 1
    Loop
End Sub

'=============================================================
' 結果セルに値と色を設
'=============================================================
Private Sub SetResult(ByVal ws As Worksheet, ByVal row As Long, ByVal text As String, ByVal clr As Long)
    ws.Cells(row, COL_RESULT).Value = text
    ws.Cells(row, COL_RESULT).Font.Color = clr
End Sub

'=============================================================
' フォルダを再帰的に作
'=============================================================
Private Sub MkDirRecursive(ByVal path As String)
    Dim fso As Object
    Set fso = CreateObject("Scripting.FileSystemObject")

    If Not fso.FolderExists(path) Then
        ' 親フォルダを先に作
        Dim parentPath As String
        parentPath = fso.GetParentFolderName(path)
        If Not fso.FolderExists(parentPath) Then
            MkDirRecursive parentPath
        End If
        fso.CreateFolder path
    End If
End Sub

コメント

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