複数フォルダのExcel・Word・Powerpointファイルを開いて保存して閉じるだけのマクロ

Only Open/Save/Close Excel/Word/Powerpoint Files of Multiple Folders At Once (Using Windows DIR Command)


 

はじめに

今回は一風変わったマクロです。というか、実用性は考慮しておりません。ただ「開いて、保存して、閉じるだけ」のマクロです。

しかし、その割に非常に長いマクロです。「開く、保存する、閉じる」の3つの作業のうち、「開く」に全力を注いでいます。

ただ開くだけなら簡単だろうと思われるでしょうが、そうでもありません。「保存するために」開くにはそれなりの工夫が必要です。

複数のフォルダ(サブフォルダを含む)に入っているファイルの中からエクセル・ワード・パワポのファイルだけを探し出し、開いて保存して閉じるプログラムです。
そのためのテクニックとして、今回は「FileSystemObject」オブジェクトはつかわずに、Windowsの「DIR」コマンドをつかっています。

また、パスワードの領域には立ち入っていませんが、それ以外の考慮すべき問題はすべて考慮しました。

・動作は無保証です。
・パスワードがかかっているファイルが存在すると、エラーでプログラムが止まります。
・動作確認は、Windows 10 + Excel 2016、Windows 7 + Excel 2010でおこなっています。
・ファイルはここからダウンロードしてください。

 

本マクロのソースコード

Option Explicit

#If VBA7 Then
    Private Declare PtrSafe Function GetInputState Lib "user32" () As LongPtr
#Else
    Private Declare Function GetInputState Lib "user32" () As Long
#End If

Public Sub S_OnlyOpenSaveCloseFiles_Main()
    Dim myStatus As Boolean: myStatus = F_ShowStatusBar
    
    Dim TargetPath As String
    TargetPath = F_GetTargetPath
    If TargetPath = "" Then Exit Sub
    
    Call S_ShowMessageForWatchingStatusBar
    
    Dim StartingTime As Double: StartingTime = Timer
    
    Switch = True
    
    Dim TargetList() As Variant
    TargetList = F_GetTargetList(TargetPath)
    
    If F_IsEmptyArray(TargetList) = True Then Exit Sub
    
    If F_CheckFileAttribute(TargetList) = True Then Exit Sub
    
    If F_IsOpenedFile(TargetList) = True Then Exit Sub
    
    Dim Files As Long
    Files = F_OnlyOpenSaveCloseFiles_Core(TargetList)
    
    Switch = False
    
    Call S_HideStatusBar(myStatus)
    
    Dim ElapsedTime As Double: ElapsedTime = Timer - StartingTime
    
    Call S_ShowResulting(Files, ElapsedTime)
End Sub

Private Function F_ShowStatusBar() As Boolean
    F_ShowStatusBar = True
    With Application.CommandBars("Status Bar")
        If .Visible = False Then
            F_ShowStatusBar = False
            .Visible = True
        End If
    End With
End Function

Private Function F_GetTargetPath() As String
    With Application.FileDialog(msoFileDialogFolderPicker)
        .Title = "開いて保存して閉じるだけのファイルのある" & _
                 "フォルダ(サブフォルダを含む)を指定してください。"
        .ButtonName = "フォルダ指定(&S)"
        .InitialView = msoFileDialogViewList
        .AllowMultiSelect = False
        
        Select Case .Show
            Case True
                F_GetTargetPath = .SelectedItems(1)
            Case False
                MsgBox "キャンセルが押されたので終了します。", _
                       vbExclamation, "フォルダ名取得中"
                F_GetTargetPath = ""
                Exit Function
        End Select
    End With
End Function

Private Sub S_ShowMessageForWatchingStatusBar()
    MsgBox "プログラム実行中は左下のステータスバーに注目して下さい。" & _
           vbCrLf & "パスワード設定の進捗状況が表示されます。", _
           vbInformation, "開いて保存して閉じるだけのマクロ"
End Sub

Private Property Let Switch(ByVal Flag As Boolean)
    With Application
        .ScreenUpdating = Not Flag
        .EnableEvents = Not Flag
        .DisplayAlerts = Not Flag
        .Calculation = _
            IIf(Flag, xlCalculationManual, xlCalculationAutomatic)
        .PrintCommunication = Not Flag
    End With
End Property

Private Function F_GetTargetList(ByVal TargetPath As String) As Variant()
    Application.StatusBar = _
        "開いて保存して閉じる途中… 進捗状況:ファイル一覧取得中..."
    
    Dim myDir As String: myDir = CurDir()
    
    ChDrive TargetPath: ChDir TargetPath
    Dim List As String: List = Environ("TEMP") & "Dir.tmp"
    
    Dim myCmd As String
    Const TARGET_EXTENSION As String = "*.xls? *.doc? *.ppt?"
    myCmd = "Dir " & TARGET_EXTENSION & " /b/s/a:-d > " & _
        """" & List & """"
    
    On Error Resume Next
    
    With CreateObject("WScript.Shell")
        Const ARGUMENT_WINDOW_MINIMIZED As Long = 7
        .Run "cmd /c" & myCmd, ARGUMENT_WINDOW_MINIMIZED, True
    End With
    
    If Err.Number <> 0 Then
        MsgBox "エラー番号:" & Err.Number & vbCrLf & _
               "エラー内容:" & Err.Description, _
               vbCritical, "ファイル一覧取得中"
        Exit Function
    End If
    
    On Error GoTo 0
    
    ChDrive myDir: ChDir myDir
    
    If FileLen(List) < 1 Then
        MsgBox "該当するファイルがありません。", vbExclamation, _
               "ファイル一覧取得中"
        Exit Function
    End If
    
    Dim temp() As Byte
    Open List For Binary As #1
        ReDim temp(1 To LOF(1))
        Get #1, , temp
    Close #1
    
    Kill List
    
    Dim FileList() As String
    FileList() = Split(StrConv(temp, vbUnicode), vbCrLf)
    F_GetTargetList = WorksheetFunction.Transpose(FileList)
End Function

Private Function F_IsEmptyArray(ByRef TargetList() As Variant) _
  As Boolean
    On Error GoTo HandleError

    Application.StatusBar = "開いて保存して閉じる最中… " & _
        "進捗状況:ファイル一覧検証中..."
    
    Select Case IsArray(TargetList)
        Case True
            If UBound(TargetList) >= 0 Then
                F_IsEmptyArray = False
            Else
                F_IsEmptyArray = True
                Application.StatusBar = False
                MsgBox "配列が空なのでプログラムを終了します。", _
                       vbExclamation, "空の配列判定中"
            End If
        Case False
            F_IsEmptyArray = True
            Application.StatusBar = False
            MsgBox "配列が空なのでプログラムを終了します。", _
                   vbExclamation, "空の配列判定中"
    End Select
    
    Exit Function
    
HandleError:
    F_IsEmptyArray = True
    Application.StatusBar = False
    MsgBox "配列が空なのでプログラムを終了します。", _
           vbExclamation, "空の配列判定中"
End Function

Private Function F_CheckFileAttribute _
  (ByRef TargetList() As Variant) As Boolean
    Application.StatusBar = "開いて保存して閉じる最中… " & _
        "進捗状況:ファイル属性確認・変更中..."
        
    Dim Path As Variant
    For Each Path In TargetList
        If Path <> "" And Path <> ThisWorkbook.FullName _
          And Not LCase(Mid(Path, InStrRev(Path, "\") + 1)) Like _
          "personal.xls?" Then
            
            Dim myAttribute As VbFileAttribute
            myAttribute = GetAttr(Path)
            Const FILE_ATTRIBUTE_COMPRESSED As Long = &H800
            Const FILE_ATTRIBUTE_ENCRYPTED As Long = &H4000
            
            myAttribute = myAttribute And Not _
               (FILE_ATTRIBUTE_COMPRESSED Or _
                FILE_ATTRIBUTE_ENCRYPTED)
                
            If (myAttribute And vbReadOnly) = vbReadOnly Then
                myAttribute = (myAttribute And Not vbReadOnly)
            End If
               
            On Error Resume Next
            
            SetAttr PathName:=Path, Attributes:=myAttribute
            
            If Err.Number <> 0 Then
                If Len(Path) > 50 Then
                    Path = Left$(Path, InStr(Path, "\")) & _
                        String(6, ".") & _
                        Mid$(Path, InStr(Len(Path) - 40, "\"))
                End If
                MsgBox "ファイルの読み取り専用属性を解除できない" & _
                       "ものがあるため、処理を終了します。" & _
                       vbCrLf & _
                       "ファイル名:" & Path & vbCrLf & _
                       "エラー番号:" & Err.Number & vbCrLf & _
                       "エラー内容:" & Err.Description, _
                       vbCritical, "読み取り専用属性解除不能"
                F_CheckFileAttribute = True
                Application.StatusBar = False
                Exit Function
            End If
            
            On Error GoTo 0
        End If
    Next Path
End Function

Private Function F_IsOpenedFile(ByRef TargetList() As Variant) As Boolean
    Dim Path As Variant
    For Each Path In TargetList
        If Path <> "" And Path <> ThisWorkbook.FullName _
          And Not LCase(Mid(Path, InStrRev(Path, "\") + 1)) Like _
          "personal.xls?" Then
            On Error Resume Next
            
            Open Path For Append As #1
            Close #1
            
            If Err.Number <> 0 Then
                If Len(Path) > 50 Then
                    Path = Left$(Path, InStr(Path, "\")) & _
                           String(6, ".") & _
                           Mid$(Path, InStr(Len(Path) - 40, "\"))
                End If
                
                MsgBox "すでに開いているファイルがあるため、" & _
                       "処理を終了します。" & vbCrLf & _
                       "開いているファイルを閉じてから、" & _
                       "再度プログラムを実行してください。" & _
                       vbCrLf & "ファイル名:" & Path & _
                       vbCrLf & "エラー番号:" & Err.Number & _
                       vbCrLf & "エラー内容:" & Err.Description, _
                       vbCritical, "開いているファイルの確認中"
                F_IsOpenedFile = True
                Application.StatusBar = False
                Exit Function
            End If
            
            On Error GoTo 0
        End If
    Next Path
End Function

Private Function F_OnlyOpenSaveCloseFiles_Core _
  (ByRef TargetList() As Variant) As Long
    Application.StatusBar = "開いて保存して閉じる最中… " & _
                            "進捗状況:" & UBound(TargetList) - 1 & _
                            "件中 1件目の処理準備中..."
    Dim Path As Variant
    For Each Path In TargetList
        If Path <> "" Then
            Dim Extension As String
            Extension = LCase(Mid(Path, InStrRev(Path, ".") + 1))
            
            Dim File As Object, cnt As Long
            
            On Error Resume Next
            
            Select Case Extension
                Case "xls", "xlsx", "xlsm"
                    Set File = Workbooks.Open _
                              (Filename:=Path, UpdateLinks:=False, _
                               IgnoreReadOnlyRecommended:=True)
                    
                    If GetInputState() = True Then DoEvents
                    
                    With File
                        If .ReadOnly = False Then
                            If ThisWorkbook.FullName <> .FullName _
                              And Not LCase(.Name) Like _
                              "personal.xls?" Then
                              
                                If .MultiUserEditing = True Then
                                    .ExclusiveAccess
                                End If
                                
                                If .CheckCompatibility = True Then
                                    .CheckCompatibility = False
                                End If
                                
                                cnt = cnt + 1
                                
                                Application.StatusBar = _
                                "開いて保存して閉じる最中… " & _
                                "進捗状況:" & _
                                UBound(TargetList) - 1 & _
                                "件中 " & cnt & "件目処理中 / " & _
                                "処理ファイル名:" & .FullName
                                
                                Call S_HandleExcelWorkbook(File)
                            End If
                        End If
                    End With
                    
                    Set File = Nothing
                Case "doc", "docx", "docm"
                    Dim wdApp As Object
                    Set wdApp = CreateObject("Word.Application")
                    With wdApp
                        .DisplayAlerts = wdAlertsNone
                        .Visible = False
                    End With
                    
                    Set File = wdApp.Documents.Open(Filename:=Path)
                    
                    If GetInputState() = True Then DoEvents
                    
                    With File
                        If .ReadOnly = False Then
                            cnt = cnt + 1
                            
                            Application.StatusBar = _
                            "開いて保存して閉じる最中… " & _
                            "進捗状況:" & _
                            UBound(TargetList) - 1 & _
                            "件中 " & cnt & "件目処理中 / " & _
                            "処理ファイル名:" & .FullName
                            
                            Call S_HandleWordDocument(File)
                        End If
                    End With
                    
                    Set File = Nothing
                Case "ppt", "pptx", "pptm"
                
                    Dim ppApp As Object
                    Set ppApp = _
                        CreateObject("Powerpoint.Application")
                    ppApp.DisplayAlerts = ppAlertsNone
                    
                    Set File = ppApp.Presentations.Open _
                              (Filename:=Path, WithWindow:=msoFalse)
                    
                    If GetInputState() = True Then DoEvents
                    
                    With File
                        If .ReadOnly = msoFalse Then
                            cnt = cnt + 1
                            
                            Application.StatusBar = _
                            "開いて保存して閉じる最中… " & _
                            "進捗状況:" & _
                            UBound(TargetList) - 1 & _
                            "件中 " & cnt & "件目処理中 / " & _
                            "処理ファイル名:" & .FullName
                            
                            Call S_HandlePowerpointPresentation(File)
                        End If
                    End With
                    
                    Set File = Nothing
            End Select
            
            On Error GoTo 0
        End If
    Next Path
    
    wdApp.Quit
    Set wdApp = Nothing
    
    ppApp.Quit
    Set ppApp = Nothing
    
    F_OnlyOpenSaveCloseFiles_Core = cnt
    
    Application.StatusBar = False
End Function

Private Sub S_HandleExcelWorkbook(ByVal File As Object)
    With File
        .Save
        .Close
    End With
End Sub

Private Sub S_HandleWordDocument(ByVal File As Object)
    With File
        .Saved = False
        .Save
        .Close
    End With
End Sub

Private Sub S_HandlePowerpointPresentation(ByVal File As Object)
    With File
        .Save
        .Close
    End With
End Sub

Private Sub S_HideStatusBar(ByVal myStatus As Boolean)
    If myStatus = False Then
        Application.CommandBars("Status Bar").Visible = False
    End If
End Sub

Private Sub S_ShowResulting(ByVal Files As Long, ByVal ElapsedTime As Double)
    MsgBox "開いて保存して閉じるだけの処理終了" & vbCrLf & _
           "処理ファイル数は " & Files & " 個です。" & vbCrLf & _
           "処理時間は " & Round(ElapsedTime, 2) & " 秒です。", _
           vbInformation, "開いて保存して閉じるだけのマクロ"
End Sub

 

「GetInputState」関数

#If VBA7 Then
    Private Declare PtrSafe Function GetInputState Lib "user32" () As LongPtr
#Else
    Private Declare Function GetInputState Lib "user32" () As Long
#End If

「GetInputState」関数は、Windwos API関数の1つです。Windows APIは、Windowsの機能をVBAやC#、VB.NETなどのプログラムから使うための関数です。

イベントキューという抽象的な「場所」にイベント、ここでは「ファイルを開く」というイベントが待っていないかどうか調べる関数です。

この関数が必要な理由は後ほど説明します。

 

メインプロシージャ

Public Sub S_OnlyOpenSaveCloseFiles_Main()
    Dim myStatus As Boolean: myStatus = F_ShowStatusBar
    
    Dim TargetPath As String
    TargetPath = F_GetTargetPath
    If TargetPath = "" Then Exit Sub
    
    Call S_ShowMessageForWatchingStatusBar
    
    Dim StartingTime As Double: StartingTime = Timer
    
    Switch = True
    
    Dim TargetList() As Variant
    TargetList = F_GetTargetList(TargetPath)
    
    If F_IsEmptyArray(TargetList) = True Then Exit Sub
    
    If F_CheckFileAttribute(TargetList) = True Then Exit Sub
    
    If F_IsOpenedFile(TargetList) = True Then Exit Sub
    
    Dim Files As Long
    Files = F_OnlyOpenSaveCloseFiles_Core(TargetList)
    
    Switch = False
    
    Call S_HideStatusBar(myStatus)
    
    Dim ElapsedTime As Double: ElapsedTime = Timer - StartingTime
    
    Call S_ShowResulting(Files, ElapsedTime)
End Sub

メインプロシージャは非常にシンプルにしました。具体的な処理はすべてサブプロシージャにまかせて、変数の受け渡しに徹しているので、処理の流れを追うことができます。

このとき、分かりやすいプロシージャ名を付けるのが肝心です。
プロシージャ名からサブプロシージャで何をしているのかわかるような名前を付けます。
英語がダメという方は日本語のプロシージャ名でもかまいません。
自分がわかりやすいプロシージャ名を付けるのが重要です。

私は、サブプロシージャ名の頭には、接頭辞として、「S_」を、ファンクションプロシージャ名の頭には、接頭辞として、「F_」を付けています。
これは、かんたんプログラミング Excel2000 VBA 応用編の45ページの記事「サブルーチンのタイトルの付け方」で推奨されていたやり方です。

かんたんプログラミング Excel2000 VBA 応用編
VBAが有するキーワードは非常に膨大で、見慣れない用語を目にしたときには、それがキーワードなのか、もしくはサブルーチンなのか、時に迷うことがあります。(途中略)サブルーチンのタイトルに若干工夫を凝らすだけで、このように「VBAのキーワードと混同しないステートメント」が記述することを覚えておいてください。

私は、この本を読んでから18年経った今も覚えていて、ちゃんと接頭辞を付けています。

また、大きなプログラムを書くときは、プロシージャの分割が必須になります。分割せずにメインプロシージャにすべて詰め込むと、非常に見づらくなります。

私が実際にこのプログラムを書いたときは、上から順番に1つ命令文を書くと、そのサブプロシージャを書き、出来上がると、次に取り掛かりました。つまり、上から順番に、かつメインプロシージャとサブプロシージャを同時に書いていきました。

プロシージャをどう分割したらいいかわからない、という方には私と同じやり方をおススメします。

 

「F_ShowStatusBar」ファンクションプロシージャ

Private Function F_ShowStatusBar() As Boolean
    F_ShowStatusBar = True
    With Application.CommandBars("Status Bar")
        If .Visible = False Then
            F_ShowStatusBar = False
            .Visible = True
        End If
    End With
End Function

「F_ShowStatusBar」は、ファンクションプロシージャの名前であると同時に、Boolean型の変数でもあります。その変数は、「True」のときにステータスバーを表示している状態を表すようにしています。「False」の場合は非表示の状態です。

最初に「F_ShowStatusBar = True」として、ステータスバーを表示しているものと考え、その上で、ステータスバーが非表示の場合には、変数「F_ShowStatusBar」に「Flase」を格納すると同時にステータスバーを表示しています。

変数「F_ShowStatusBar」に「False」を格納することは、最終的に「S_HideStatusBar」の動作に関わってきます。

 

「F_GetTargetPath」ファンクションプロシージャ

Private Function F_GetTargetPath() As String
    With Application.FileDialog(msoFileDialogFolderPicker)
        .Title = "開いて保存して閉じるだけのファイルのある" & _
                 "フォルダ(サブフォルダを含む)を指定してください。"
        .ButtonName = "フォルダ指定(&S)"
        .InitialView = msoFileDialogViewList
        .AllowMultiSelect = False
        
        Select Case .Show
            Case True
                F_GetTargetPath = .SelectedItems(1)
            Case False
                MsgBox "キャンセルが押されたので終了します。", _
                       vbExclamation, "フォルダ名取得中"
                F_GetTargetPath = ""
                Exit Function
        End Select
    End With
End Function

「F_GetTargetPath」ファンクションプロシージャは、開いて保存して閉じる対象(Target)となるファイルの存在するフォルダのパス名、すなわち「TargetPath」を取得(Get)します。

「Application.FileDialog(msoFileDialogFolderPicker)」は、以前にも説明していますが、フォルダを選択するためのダイアログです。
ここでは、「.AllowMultiSelect = False」として、1つのフォルダだけを選択するようにしています。
選択するのは1つのフォルダだけですが、本プログラムで処理するのは、ここで選択したフォルダのサブフォルダを含むすべてのフォルダのすべてのファイルが対象です。

「F_GetTargetPath = .SelectedItems(1)」で選択されたフォルダ名を、変数名でもある「F_GetTargetPath」に格納しています。

 

「S_ShowMessageForWatchingStatusBar」サブプロシージャ

Private Sub S_ShowMessageForWatchingStatusBar()
    MsgBox "プログラム実行中は左下のステータスバーに注目して下さい。" & _
           vbCrLf & "パスワード設定の進捗状況が表示されます。", _
           vbInformation, "開いて保存して閉じるだけのマクロ"
End Sub

メッセージボックスを表示するだけのサブプロシージャですので、メインプロシージャに書いてもかまいません。

私はメインプロシージャをできるだけシンプルにしたかったので、サブプロシージャの中にMsgBox関数を書きました。

 

「Switch」プロパティプロシージャ

Private Property Let Switch(ByVal Flag As Boolean)
    With Application
        .ScreenUpdating = Not Flag
        .EnableEvents = Not Flag
        .DisplayAlerts = Not Flag
        .Calculation = _
            IIf(Flag, xlCalculationManual, xlCalculationAutomatic)
        .PrintCommunication = Not Flag
    End With
End Property

プロパティプロシージャは、主にクラスモジュールで使われ、標準モジュールではめったに使われません。
ですが、標準モジュールでの使い方を紹介したのがこちらのページ。

VBA マクロ高速化のために停止すべき3項目 – t-hom’s diary

ここに書いてあることを多少アレンジして、ほぼ完全コピーしたのが「Switch」プロパティプロシージャです。

目的は、プログラムの高速化・自動化です。

上のページと違うところは、「.DisplayAlerts = Not Flag」と「.PrintCommunication = Not Flag」が入っているところと、プロシージャ名です。

 

「F_GetTargetList」ファンクションプロシージャ

Private Function F_GetTargetList(ByVal TargetPath As String) As Variant()
    Application.StatusBar = _
        "開いて保存して閉じる途中… 進捗状況:ファイル一覧取得中..."
    
    Dim myDir As String: myDir = CurDir()
    
    ChDrive TargetPath: ChDir TargetPath
    Dim List As String: List = Environ("TEMP") & "Dir.tmp"
    
    Dim myCmd As String
    Const TARGET_EXTENSION As String = "*.xls? *.doc? *.ppt?"
    myCmd = "Dir " & TARGET_EXTENSION & " /b/s/a:-d > " & _
        """" & List & """"
    
    On Error Resume Next
    
    With CreateObject("WScript.Shell")
        Const ARGUMENT_WINDOW_MINIMIZED As Long = 7
        .Run "cmd /c" & myCmd, ARGUMENT_WINDOW_MINIMIZED, True
    End With
    
    If Err.Number <> 0 Then
        MsgBox "エラー番号:" & Err.Number & vbCrLf & _
               "エラー内容:" & Err.Description, _
               vbCritical, "ファイル一覧取得中"
        Exit Function
    End If
    
    On Error GoTo 0
    
    ChDrive myDir: ChDir myDir
    
    If FileLen(List) < 1 Then
        MsgBox "該当するファイルがありません。", vbExclamation, _
               "ファイル一覧取得中"
        Exit Function
    End If
    
    Dim temp() As Byte
    Open List For Binary As #1
        ReDim temp(1 To LOF(1))
        Get #1, , temp
    Close #1
    
    Kill List
    
    Dim FileList() As String
    FileList() = Split(StrConv(temp, vbUnicode), vbCrLf)
    F_GetTargetList = WorksheetFunction.Transpose(FileList)
End Function

Windowsの「DIR」コマンドを使って、ファイル一覧を求める、本プログラムの中でも、重要なプロシージャです。

Application.StatusBar = _
    "開いて保存して閉じる途中… 進捗状況:ファイル一覧取得中..."

ステータスバーにメッセージを表示する命令です。進捗状況に応じて、適切なメッセージを表示させることが重要です。メッセージを消去する場合は「Application.StatusBar = False」とします。

Dim myDir As String: myDir = CurDir()

現在のディレクトリ(カレント・ディレクトリ)を変数「myDir」に格納します。これは、後ほど出てくる命令でカレント・ディレクトリを変更してしまうため、元のカレント・ディレクトリに戻すために必要な作業です。

ChDrive TargetPath: ChDir TargetPath
Dim List As String: List = Environ("TEMP") & "Dir.tmp"

上に述べたように、カレント・ディレクトリを変数「TargetPath」に格納しているパスに変更しています。
普通は「Cドライブ」だと思いますが、そうでないレア・ケースにも対応するようにしています。

「ChDrive」は、カレント・ドライブを変更する命令です。引数にパス名などを指定した場合、最初の1文字が表すドライブに変更します。
重要な点は、カレント・ディレクトリを変更したい場合は、先にカレント・ドライブを変更しておく必要がある、という点です。

カレント・ドライブを変更せずにカレント・ディレクトリを変更しても無視されます。

Dim myCmd As String
Const TARGET_EXTENSION As String = "*.xls? *.doc? *.ppt?"
myCmd = "Dir " & TARGET_EXTENSION & " /b/s/a:-d > " & _
    """" & List & """"

変数・定数の宣言と、初期値の代入です。
「myCmd」にはWindowsの「DIR」コマンドの文字列を代入します。
「TARGET_EXTENSION」には検索する拡張子を代入しています。「*.xls?」は拡張子の先頭3文字が「xls」で始まる3文字または4文字の拡張子を持つ、すべてのファイルを表します。つまり、よく使われるエクセルファイルを表します。
同様に「*.doc?」はよく使われるワードファイルを表し、「*.ppt?」はよく使われるパワポファイルを表します。

「myCmd」には「DIR」コマンドの文字列を代入しますが、その解説には次のページがふさわしいと思います。

Windowsのdirコマンドでファイル名の一覧を取得する:Tech TIPS – @IT

また、「””””」の意味がわからない方は、次のページを読むといいでしょう。

Office TANAKA – Excel VBA Tips[ダブルコーテーションの表示]

    On Error Resume Next
    
    With CreateObject("WScript.Shell")
        Const ARGUMENT_WINDOW_MINIMIZED As Long = 7
        .Run "cmd /c" & myCmd, ARGUMENT_WINDOW_MINIMIZED, True
    End With
    
    If Err.Number <> 0 Then
        MsgBox "エラー番号:" & Err.Number & vbCrLf & _
               "エラー内容:" & Err.Description, _
               vbCritical, "ファイル一覧取得中"
        Exit Function
    End If
    
On Error GoTo 0

「CreateObject」関数で、Windows Script Host(WSH)のスクリプト実行環境を作成し、「Run」コマンドでファイル名一覧を作成します。その際、コマンドプロンプトのウィンドウは最小化して、見えなくしています。
また、「7」という数字が何を表しているのかわかるようにするため、それを説明する定数名「ARGUMENT_WINDOW_MINIMIZED」を使って、ウィンドウを最小化する引数ですよ、ということを表しています。

ChDrive myDir: ChDir myDir

この命令で、カレント・ドライブ、カレント・ディレクトリを元の状態に戻します。

If FileLen(List) < 1 Then
    MsgBox "該当するファイルがありません。", vbExclamation, _
           "ファイル一覧取得中"
    Exit Function
End If

変数「List」は「DIR」コマンドの出力結果であるファイル名のリストの一時ファイルです。「FileLen」関数は、「List」ファイルのバイト数を返します。
それが1より小さい、つまり0バイトであるということは、ファイルの中身は何もない、ということです。
したがって、該当するパス名は1つもない、ということになり、「MsgBox」関数で該当するファイルはありません、と表示します。

Dim temp() As Byte
Open List For Binary As #1
    ReDim temp(1 To LOF(1))
    Get #1, , temp
Close #1

Kill List

一時ファイル「List」は中身を見ればわかりますが、パス名が書いてあるテキストファイルです。それをバイナリモードで開いて、バイト型の配列「temp()」に書き込んでいます。「ReDim temp(1 To LOF(1))」としているのは、ファイルサイズ分の読み込み領域を確保するためです。最後に一時ファイル「List」を削除しています。

    Dim FileList() As String
    FileList() = Split(StrConv(temp, vbUnicode), vbCrLf)
    F_GetTargetList = WorksheetFunction.Transpose(FileList)
End Function

VBAでは、ユニコードで文字を扱いますので、「temp」の文字コードをユニコードに変換します。そのうえで、キャリッジリターン・ラインフィード(CRLF)で文字列を区切ります。そうすると、人間の目にも読めるし、VBAからも読めるファイル名一覧のリストが出来上がります。最後にワークシート関数「Transpose」を使っているのは、横に長い配列になっているリストを、縦に長いリストに変換するための処置です。「動的配列の次元を入れ替える」といいます。

 

「F_IsEmptyArray」ファンクションプロシージャ

Private Function F_IsEmptyArray(ByRef TargetList() As Variant) _
  As Boolean
    On Error GoTo HandleError

    Application.StatusBar = "開いて保存して閉じる最中… " & _
        "進捗状況:ファイル一覧検証中..."
    
    Select Case IsArray(TargetList)
        Case True
            If UBound(TargetList) >= 0 Then
                F_IsEmptyArray = False
            Else
                F_IsEmptyArray = True
                Application.StatusBar = False
                MsgBox "配列が空なのでプログラムを終了します。", _
                       vbExclamation, "空の配列判定中"
            End If
        Case False
            F_IsEmptyArray = True
            Application.StatusBar = False
            MsgBox "配列が空なのでプログラムを終了します。", _
                   vbExclamation, "空の配列判定中"
    End Select
    
    Exit Function
    
HandleError:
    F_IsEmptyArray = True
    Application.StatusBar = False
    MsgBox "配列が空なのでプログラムを終了します。", _
           vbExclamation, "空の配列判定中"
End Function

ファイル名一覧の一時ファイルは、「TargetList」という名前の配列変数に格納されました。その配列が空でないかどうか確認するファンクションプロシージャです。

取得中だったファイル一覧の内容を確認する段階になったので、ステータスバーのメッセージを「ファイル一覧検証中」に変更します。

配列「TargetList」を「IsArray」関数で本当に配列かどうか判定します。「True」なら配列であり、「False」なら配列ではありません。
配列を「UBound」関数の引数に渡して、戻り値が「0」以上ならば、その配列は空ではありません。「0」未満であれば、その配列は空です。

このファンクションプロシージャでエラーが発生する場合は、そもそも配列ではありません。プログラムのユーザには細かい差異は関係ありませんので、「空でない配列」以外はすべて「空の配列」ということでエラーを出しています。

 

「F_CheckFileAttribute」ファンクションプロシージャ

Private Function F_CheckFileAttribute _
  (ByRef TargetList() As Variant) As Boolean
    Application.StatusBar = "開いて保存して閉じる最中… " & _
        "進捗状況:ファイル属性確認・変更中..."
        
    Dim Path As Variant
    For Each Path In TargetList
        If Path <> "" And Path <> ThisWorkbook.FullName _
          And Not LCase(Mid(Path, InStrRev(Path, "\") + 1)) Like _
          "personal.xls?" Then
            
            Dim myAttribute As VbFileAttribute
            myAttribute = GetAttr(Path)
            Const FILE_ATTRIBUTE_COMPRESSED As Long = &H800
            Const FILE_ATTRIBUTE_ENCRYPTED As Long = &H4000
            
            myAttribute = myAttribute And Not _
               (FILE_ATTRIBUTE_COMPRESSED Or _
                FILE_ATTRIBUTE_ENCRYPTED)
                
            If (myAttribute And vbReadOnly) = vbReadOnly Then
                myAttribute = (myAttribute And Not vbReadOnly)
            End If
               
            On Error Resume Next
            
            SetAttr PathName:=Path, Attributes:=myAttribute
            
            If Err.Number <> 0 Then
                If Len(Path) > 50 Then
                    Path = Left$(Path, InStr(Path, "\")) & _
                        String(6, ".") & _
                        Mid$(Path, InStr(Len(Path) - 40, "\"))
                End If
                MsgBox "ファイルの読み取り専用属性を解除できない" & _
                       "ものがあるため、処理を終了します。" & _
                       vbCrLf & _
                       "ファイル名:" & Path & vbCrLf & _
                       "エラー番号:" & Err.Number & vbCrLf & _
                       "エラー内容:" & Err.Description, _
                       vbCritical, "読み取り専用属性解除不能"
                F_CheckFileAttribute = True
                Application.StatusBar = False
                Exit Function
            End If
            
            On Error GoTo 0
        End If
    Next Path
End Function

上述した部分で、ファイル一覧の配列が空でないことを確認した後は、それぞれのファイルの属性を調べます。

具体的には、読み取り専用属性ではないか、圧縮属性ではないか、暗号属性ではないか、をそれぞれ調べます。

これらの属性を有していると、ファイルがうまく保存できない可能性があるので、手は抜けません。

調べた結果、これらの属性があることがわかったら、「ビット演算」という計算方法を使って、それぞれの属性を解除します。

このビット演算が本プログラムの中で一番難しいかもしれません。このブログを読む方は、VBA初心者の方だと思います。となると、他言語の経験はないと考えたほうがいいと思います。すると、当然「ビット演算」の経験もないでしょうから、基礎もなく、いきなり「ビット演算」に挑むのは、無謀というものです。

VBA ビット演算 – Tipsfound

このページの「ビットを下ろす」までを理解すれば、本プログラムを理解できるでしょう。「ビットシフト」はここでは扱いませんので、関係ありません。

Dim myAttribute As VbFileAttribute
myAttribute = GetAttr(Path)
Const FILE_ATTRIBUTE_COMPRESSED As Long = &H800
Const FILE_ATTRIBUTE_ENCRYPTED As Long = &H4000

まず変数「myAttribute」を「VbFileAttribute」列挙型で宣言し、それぞれのファイル名(パス)の属性を取得します。

圧縮属性と暗号属性は「VbFileAttrbute」列挙型に存在しないので、自前で定数を定義します。

「FILE_ATTRIBUTE_COMPRESSED」は圧縮属性、「FILE_ATTRIBUTE_ENCRYPTED」暗号属性です。

myAttribute = myAttribute And Not _
   (FILE_ATTRIBUTE_COMPRESSED Or _
    FILE_ATTRIBUTE_ENCRYPTED)

変数「myAttribute」と圧縮属性・暗号属性の定数を、「And Not」でビット演算して、圧縮属性・暗号属性のフラグを下ろして、圧縮属性・暗号属性を解除します。

If (myAttribute And vbReadOnly) = vbReadOnly Then
    myAttribute = (myAttribute And Not vbReadOnly)
End If

続いて、読み取り専用属性を有していれば、「And Not」でフラグを下ろして、読み取り専用属性を解除します。

On Error Resume Next

SetAttr PathName:=Path, Attributes:=myAttribute

「SetAttr」ステートメントで、読み取り専用属性・圧縮属性・暗号属性を解除した属性を、ファイルに設定します。

            If Err.Number <> 0 Then
                If Len(Path) > 50 Then
                    Path = Left$(Path, InStr(Path, "\")) & _
                        String(6, ".") & _
                        Mid$(Path, InStr(Len(Path) - 40, "\"))
                End If
                MsgBox "ファイルの読み取り専用属性を解除できない" & _
                       "ものがあるため、処理を終了します。" & _
                       vbCrLf & _
                       "ファイル名:" & Path & vbCrLf & _
                       "エラー番号:" & Err.Number & vbCrLf & _
                       "エラー内容:" & Err.Description, _
                       vbCritical, "読み取り専用属性解除不能"
                F_CheckFileAttribute = True
                Application.StatusBar = False
                Exit Function
            End If
            
            On Error GoTo 0
        End If
    Next Path
End Function

この部分は、エラー処理です。「If Len(Path) > 50 Then」以下の部分は、パスが長い場合に一部を省略して「……」に置き換えて表示する命令です。

 

「F_IsOpenedFile」ファンクションプロシージャ

Private Function F_IsOpenedFile(ByRef TargetList() As Variant) As Boolean
    Dim Path As Variant
    For Each Path In TargetList
        If Path <> "" And Path <> ThisWorkbook.FullName _
          And Not LCase(Mid(Path, InStrRev(Path, "\") + 1)) Like _
          "personal.xls?" Then
            On Error Resume Next
            
            Open Path For Append As #1
            Close #1
            
            If Err.Number <> 0 Then
                If Len(Path) > 50 Then
                    Path = Left$(Path, InStr(Path, "\")) & _
                           String(6, ".") & _
                           Mid$(Path, InStr(Len(Path) - 40, "\"))
                End If
                
                MsgBox "すでに開いているファイルがあるため、" & _
                       "処理を終了します。" & vbCrLf & _
                       "開いているファイルを閉じてから、" & _
                       "再度プログラムを実行してください。" & _
                       vbCrLf & "ファイル名:" & Path & _
                       vbCrLf & "エラー番号:" & Err.Number & _
                       vbCrLf & "エラー内容:" & Err.Description, _
                       vbCritical, "開いているファイルの確認中"
                F_IsOpenedFile = True
                Application.StatusBar = False
                Exit Function
            End If
            
            On Error GoTo 0
        End If
    Next Path
End Function

次は、すでに開いているファイルがないか確認します。本プログラムを開始する時点ですでに開いているファイルがあると、そのファイルに対して本プログラムを実行することができません。

そこで、このプロシージャを用いて、開いているファイルをチェックして、もし開いているファイルがある場合は、本プログラムを終了します。

Open Path For Append As #1
Close #1

ファイルを追記モードで開きます。追記モードで開くと、ファイルを開こうとしたとき、そのファイルが他のアプリケーションによって開かれていた場合は、エラーを返します。

そのエラーを手掛かりに、エラーが出た場合は、本プログラムを終了します。

 

「F_OnlyOpenSaveCloseFiles_Core」ファンクションプロシージャ

Private Function F_OnlyOpenSaveCloseFiles_Core _
  (ByRef TargetList() As Variant) As Long
    Application.StatusBar = "開いて保存して閉じる最中… " & _
                            "進捗状況:" & UBound(TargetList) - 1 & _
                            "件中 1件目の処理準備中..."
    Dim Path As Variant
    For Each Path In TargetList
        If Path <> "" Then
            Dim Extension As String
            Extension = LCase(Mid(Path, InStrRev(Path, ".") + 1))
            
            Dim File As Object, cnt As Long
            
            On Error Resume Next
            
            Select Case Extension
                Case "xls", "xlsx", "xlsm"
                    Set File = Workbooks.Open _
                              (Filename:=Path, UpdateLinks:=False, _
                               IgnoreReadOnlyRecommended:=True)
                    
                    If GetInputState() = True Then DoEvents
                    
                    With File
                        If .ReadOnly = False Then
                            If ThisWorkbook.FullName <> .FullName _
                              And Not LCase(.Name) Like _
                              "personal.xls?" Then
                              
                                If .MultiUserEditing = True Then
                                    .ExclusiveAccess
                                End If
                                
                                If .CheckCompatibility = True Then
                                    .CheckCompatibility = False
                                End If
                                
                                cnt = cnt + 1
                                
                                Application.StatusBar = _
                                "開いて保存して閉じる最中… " & _
                                "進捗状況:" & _
                                UBound(TargetList) - 1 & _
                                "件中 " & cnt & "件目処理中 / " & _
                                "処理ファイル名:" & .FullName
                                
                                Call S_HandleExcelWorkbook(File)
                            End If
                        End If
                    End With
                    
                    Set File = Nothing
                Case "doc", "docx", "docm"
                    Dim wdApp As Object
                    Set wdApp = CreateObject("Word.Application")
                    With wdApp
                        .DisplayAlerts = wdAlertsNone
                        .Visible = False
                    End With
                    
                    Set File = wdApp.Documents.Open(Filename:=Path)
                    
                    If GetInputState() = True Then DoEvents
                    
                    With File
                        If .ReadOnly = False Then
                            cnt = cnt + 1
                            
                            Application.StatusBar = _
                            "開いて保存して閉じる最中… " & _
                            "進捗状況:" & _
                            UBound(TargetList) - 1 & _
                            "件中 " & cnt & "件目処理中 / " & _
                            "処理ファイル名:" & .FullName
                            
                            Call S_HandleWordDocument(File)
                        End If
                    End With
                    
                    Set File = Nothing
                Case "ppt", "pptx", "pptm"
                
                    Dim ppApp As Object
                    Set ppApp = _
                        CreateObject("Powerpoint.Application")
                    ppApp.DisplayAlerts = ppAlertsNone
                    
                    Set File = ppApp.Presentations.Open _
                              (Filename:=Path, WithWindow:=msoFalse)
                    
                    If GetInputState() = True Then DoEvents
                    
                    With File
                        If .ReadOnly = msoFalse Then
                            cnt = cnt + 1
                            
                            Application.StatusBar = _
                            "開いて保存して閉じる最中… " & _
                            "進捗状況:" & _
                            UBound(TargetList) - 1 & _
                            "件中 " & cnt & "件目処理中 / " & _
                            "処理ファイル名:" & .FullName
                            
                            Call S_HandlePowerpointPresentation(File)
                        End If
                    End With
                    
                    Set File = Nothing
            End Select
            
            On Error GoTo 0
        End If
    Next Path
    
    wdApp.Quit
    Set wdApp = Nothing
    
    ppApp.Quit
    Set ppApp = Nothing
    
    F_OnlyOpenSaveCloseFiles_Core = cnt
    
    Application.StatusBar = False
End Function

「Core」とあるとおり、本プログラムの「中心」部分です。このプロシージャで、ファイルを「開く」「保存する」「閉じる」処理をおこないます。

Dim Extension As String
Extension = LCase(Mid(Path, InStrRev(Path, ".") + 1))

変数「Extension」に拡張子の文字列を格納します。その拡張子を元に処理を分岐します。

Select Case Extension
  Case "xls", "xlsx", "xlsm"
  ~~~~~~~~~~~~~
  Case "doc", "docx", "docm"
  ~~~~~~~~~~~~~
  Case "ppt", "pptx", "pptm"
  ~~~~~~~~~~~~~
End Select

変数「Extension」に格納した拡張子の文字列にもとづいて、Select Case 文で処理を分岐します。

エクセル・ワード・パワポの3つの場合に場合分けして処理を進めます。

Set File = Workbooks.Open _
          (Filename:=Path, UpdateLinks:=False, _
           IgnoreReadOnlyRecommended:=True)

最初はエクセルです。オブジェクト変数「File」に開いたエクセルファイルを格納します。

引数「UpdateLinks」は、リンクの更新方法を指定します。「False」を指定すると、外部参照、リモート参照ともに更新しません。

引数「IgnoreReadOnlyRecommended」に「True」を指定すると、読み取り推奨メッセージを非表示にします。

If GetInputState() = True Then DoEvents

「DoEvents」関数は、主に低速PC対策の命令です。

時間のかかる処理を実行している場合、ユーザが行った操作は処理が終了するまでオペレーティングシステムに渡りません。そのとき、PCが十分に高速であれば何も問題は起こりませんが、PCが低速の場合、最悪動作が止まって、いわゆる「固まる」状態になります。そんな場合に、処理を中断するときは、DoEvents関数で一時的にWindowsに制御を移します。

すると、PCは命令を受付けるようになり、例えば「Esc」キーを受付けたり、強制終了を受付けたりします。

ところが、そんな便利な「DoEvents」関数も問題があります。

Windowsに制御を渡すと、イベントキューの中身を調べるために極端にスピードが低下します。

そこで「GetInputState」関数を使います。これはイベントキューに待機中のイベントがあるかどうかだけを調べるので、待機中のイベントがあるときだけWindowsに制御を渡します。したがって、余分な確認処理をせずに済み、スピードも落ちなくなります。

With File
    If .ReadOnly = False Then
        If ThisWorkbook.FullName <> .FullName _
          And Not LCase(.Name) Like _
          "personal.xls?" Then
            ~~~~~~~~~~~~~~
        End If
    End If
End With

ここでは、開いたファイルを「ふるい」にかけています。ファイルが読み取り専用でなく、本プログラムを保存しているファイルとも違い、個人用マクロブックでもないときだけ、処理をします。

If .MultiUserEditing = True Then
    .ExclusiveAccess
End If

If .CheckCompatibility = True Then
    .CheckCompatibility = False
End If

上記の処理で「ふるい」にかけたファイルですが、まだまだ「保存する」ためには障害が残っています。

共有ブックとして開かれている場合は、色々と制限があるので、排他モードに変更します。

「ExclusiveAccess」メソッドを実行すると、排他モードに変更されます。

まだあります。Excel 2003以前のブックを開くとき、ブックの保存時に互換性チェックが自動的に実行されるかどうかを聞いてくるダイアログが開くときがあります。

そのダイアログが開かないようにするためには、「.CheckCompatibility = False」とします。

cnt = cnt + 1

Application.StatusBar = _
"開いて保存して閉じる最中… " & _
"進捗状況:" & _
UBound(TargetList) - 1 & _
"件中 " & cnt & "件目処理中 / " & _
"処理ファイル名:" & .FullName

Call S_HandleExcelWorkbook(File)

「cnt = cnt + 1」で、件数を1カウントアップします。

ステータスバーの表示は、1件ごとに変化します。全体の件数を「UBound(TargetList) – 1」としているのは、「TargetList」には、空白行が1行必ず含まれるからです。

ここまでしておいて、オブジェクト変数「File」をサブプロシージャ「S_HandleExcelWorkbook」に渡して処理します。ここでは「保存して閉じる」処理をサブプロシージャに任せています。

ワード・パワポもやり方は同じなので、違う部分だけを説明します。

Dim wdApp As Object
Set wdApp = CreateObject("Word.Application")
With wdApp
    .DisplayAlerts = wdAlertsNone
    .Visible = False
End With

ここでは、オブジェクト変数「wdApp」に「Word.Application」オブジェクトを格納して、警告表示を非表示にして、「Word.Application」オブジェクトそのものも非表示にしています。

Dim ppApp As Object
Set ppApp = _
    CreateObject("Powerpoint.Application")
ppApp.DisplayAlerts = ppAlertsNone

Set File = ppApp.Presentations.Open _
          (Filename:=Path, WithWindow:=msoFalse)

ワードと同じように、オブジェクト変数「ppApp」に「Powerpoint.Application」オブジェクトを格納して、警告表示を非表示にしています。

ワードと違うのは、「Powerpoint.Application」オブジェクトの非表示にするやり方です。

「.Visible = msoFalse」という命令は、MSDNには記述があるものの、実際に実行すると、エラーが出ます。

ですので、パワポファイルを開くときに、パラメータで「WithWindow:=msoFalse」として、オブジェクトそのものを非表示にしています。

F_OnlyOpenSaveCloseFiles_Core = cnt

Application.StatusBar = False

変数「F_OnlyOpenSaveCloseFiles_Core」に件数を代入し、ステータスバーの表示を元に戻して終了です。

 

「S_HandleExcelWorkbook」サブプロシージャ

Private Sub S_HandleExcelWorkbook(ByVal File As Object)
    With File
        .Save
        .Close
    End With
End Sub

開いたファイルを引数として受け取って、保存して閉じるだけです。

実際には、ほぼ何もしないプログラムですが、このサブプロシージャこそアイデアの出し所で、次から次へとファイルが開かれるわけですから、そこで何をプログラミングするかはあなた次第というところです。

 

「S_HandleWordDocument」サブプロシージャ

Private Sub S_HandleWordDocument(ByVal File As Object)
    With File
        .Saved = False
        .Save
        .Close
    End With
End Sub

エクセルとほぼ同じですが、ワードは1度内容を変更しないと上書き保存できないので、「.Saved = False」として、疑似的に内容を変更した状態を作っています。

ところが、Word 2016では、このような作業をしなくても上書き保存できます。

Word 2010、またはWord 2013までの仕様ではないでしょうか。

 

「S_HandlePowerpointPresentation」サブプロシージャ

Private Sub S_HandlePowerpointPresentation(ByVal File As Object)
    With File
        .Save
        .Close
    End With
End Sub

パワポの場合は、エクセルと同じです。

 

「S_HideStatusBar」サブプロシージャ

Private Sub S_HideStatusBar(ByVal myStatus As Boolean)
    If myStatus = False Then
        Application.CommandBars("Status Bar").Visible = False
    End If
End Sub

変数「myStatus」の状態を調べ、元々ステータスバーが非表示だった場合に限って、ここでステータスバーを非表示にしています。

 

「S_ShowResulting」サブプロシージャ

Private Sub S_ShowResulting(ByVal Files As Long, ByVal ElapsedTime As Double)
    MsgBox "開いて保存して閉じるだけの処理終了" & vbCrLf & _
           "処理ファイル数は " & Files & " 個です。" & vbCrLf & _
           "処理時間は " & Round(ElapsedTime, 2) & " 秒です。", _
           vbInformation, "開いて保存して閉じるだけのマクロ"
End Sub

最後に処理ファイル数と処理時間を表示して、本プログラムは終了です。

 

おわりに

今回は「開いて保存して閉じるだけ」のマクロでしたが、様々なテクニックが凝縮されているのがおわかりいただけたでしょうか。

私は、VBA初心者の方には、1つ1つのテクニックを細切れに説明するのではなく、1つのプログラムを作り上げる中で様々なテクニックを有機的につなげているものをお見せするのが必要ではないかと思っています。

今後もそのようなプログラムを作っていきたいと思います。

コメントを残す