「先生,難しい顔をしてどうしたんですか?」「幻の金属が見つからないのだ。どこにあるのだろう」「仕事中に携帯でゲームするのはやめてください! 次の方,どーぞー」
「あるフォルダの下にあるファイルを,サブフォルダの中にあるファイルも含めてリストにしようと思ったのですが,不特定の階層のサブフォルダに対応できません。VBAでは無理なのでしょうか?」 |
そんなことはありません。VBAにできないことはないです,って言うのはちょっと大げさですが,それくらいのことならできますよ。ファイル/フォルダの検索なら「再帰呼び出し」が定番です。
再帰呼び出し=無限ループ?
皆さんは,再帰呼び出しという言葉を聞いたことがあるでしょうか? 再帰呼び出しは,プロシジャや関数が内部で自分自身を呼び出すことです。決まったアルゴリズムや処理パターンなど再帰的に表現できる処理を記述するときによく使われる手法です。
再帰呼び出しのコードを見ると,例えばSubプロシジャtest1の中でtest1をCallするといった形になっていて,無限ループになりそうな気がします。
しかし実際には,同じ名前のプロシジャを呼び出したとしても,引数や変数,プロシジャの戻り先などはメモリー上の別の領域に確保されて,別のプロシジャとして動作するのです。また,当然ながら無限ループにならないようにプログラムを作る必要があります。とはいえ,言葉だけで再帰呼び出しを理解するのは困難です。実際にサンプルで動きを追いかけて,どうなるかを体感してください。
すべてのサブフォルダを
リストアップするマクロ
指定したフォルダ内のすべてのファイル/サブフォルダのリストを作るマクロの場合,事前に数や階層がわからないサブフォルダをどうやって検索するかがポイントになります。すべてのサブフォルダを一気に返してくれる便利なメソッドはVBAにはありません。ファイルやフォルダを操作するライブラリFileSystemObjectのGetFolderメソッドを使えば,指定したフォルダの一つ下のサブフォルダを取得できます。したがって,GetFolderメソッドをフォルダの数だけ実行すれば,すべてのフォルダをリストアップできそうです。このような場合,再帰呼び出しの手法を用いるとうまく解決できます。
サンプルとして,ワークブックがあるフォルダ下のすべてのサブフォルダのPathをワークシートに記録するマクロを作りました(図1[拡大表示])*1。このマクロは,大きく三つの部分に分かれています。一つは,モジュールの宣言部です。ここには,モジュール内にあるすべてのプロシジャで参照可能な変数を宣言します。今回は,前述の通りFileSystemObjectをバインドします。このオブジェクトは,プライベート変数に代入してすべてのプロシジャから呼び出せるようにします。また,フォルダのPathを記録するセルの行番号を代入する変数もここで宣言します。
二つ目はメインルーチンです。フォルダのPathを取り出す処理は別のプロシジャにまとめて部品化します。メインルーチンの主な役割は,このプロシジャに引数を与えて呼び出すことです。三つ目は,フォルダのPathを取り出してリストアップするプロシジャです。
では具体的にコードを見ていきましょう。リスト1[拡大表示]は,変数の宣言部とメインルーチン(MakeSubFolderList)です。最初に,モジュールの宣言エリアに,先ほど触れた二つの変数(myFSOとmyRow)を宣言します(1)。これらの変数は,このモジュール内で共通の変数となり,マクロ終了までその値を保持します。FileSystemObjectは,Newキーワードを使って,myFSOの宣言と同時にバインドしています。
リスト1●モジュール内のプロシジャで参照可能な変数の宣言とメインルーチン(MakeSubFolderList)
Dim myFSO As New FileSystemObject Dim myRow As Long Sub MakeSubFolderList() Dim myCurrentFolderName As String myCurrentFolderName = ThisWorkbook.Path myRow = 1 Call GetSubFolderList(myCurrentFolderName) Set myFSO = Nothing End Sub |
メインルーチンでは,まずSubプロシジャであるGetSubFolderListに渡す先頭のPathを格納する変数を宣言します(2)。その変数に,このワークブックのPathを取得して格納します(3)。そして,取得したPathを記録するセルの行番号として,1をmyRowにセットします(4)。ここまででできたら,(2)を引数として,GetSubFolderListプロシジャをCallします(5)。フォルダのPathのリストアップが終了し,制御が戻ってきたらオブジェクト型変数myFSOを空にします(6)。
再帰呼び出しは
穴のない条件分岐内に
GetSubFolderListプロシジャはリスト2[拡大表示]を見てください。最初に引数で受け取ったフォルダのPathをセルに代入します(1)。そしてセルの行番号を保持するmyRowをインクリメントします(2)。
リスト2●サブフォルダのPathのリストを作成するプロシジャ(GetSubFolderList)
Sub GetSubFolderList(myFolderName As String) Dim myFolder As Folder Dim mySubFolder_1 As Folder Dim mySubFolder_2 As Folder ThisWorkbook.Worksheets(1).Cells(myRow, 1).Value = _ myFolderName myRow = myRow + 1 With myFSO Set myFolder = .GetFolder(myFolderName) For Each mySubFolder_1 In myFolder.SubFolders ThisWorkbook.Worksheets(1).Cells(myRow, 1) _ .Value = mySubFolder_1.Path myRow = myRow + 1 If mySubFolder_1.SubFolders.Count > 0 Then For Each mySubFolder_2 In _ mySubFolder_1.SubFolders Call GetSubFolderList(mySubFolder_2.Path) Next mySubFolder_2 End If Next mySubFolder_1 End With Set myFolder = Nothing Set mySubFolder_1 = Nothing Set mySubFolder_2 = Nothing End Sub |
次のWithブロックで,myFSOを使ってフォルダを取得します。まず,現在のフォルダのPathを引数として,GetFolderメソッドを呼び出します。そうすると,そのフォルダをオブジェクトとして取得できます(3)。このオブジェクトを使って,For Each...Nextステートメントでサブフォルダを順次取り出し,オブジェクト型変数(mySubFolder_1)に代入します(4)。取り出したサブフォルダのPathをセルに代入し(5),myRowをインクリメントします(6)。取り出したサブフォルダの中にさらにサブフォルダがある場合は(7),For Each...Nextステートメントで,サブフォルダの中のサブフォルダを順次取り出し(8),そのサブフォルダのPathを引数として,GetSubFolderListをCallします(9)。これが再帰呼び出しですね。最後にオブジェクト型変数を空にします(10)。
再帰呼び出しで気になるのが無限ループです。今回の場合,これを回避しているのが,(7)の条件分岐です。再帰呼び出しのプロシジャを呼び出すステートメントは,「サブフォルダがあるならば」という条件分岐の中にあります。条件分岐がなければ無限ループになってしまいます。再帰呼び出しを利用する場合は,穴のない条件分岐を作る必要があります。
しかし,人はミスをするもの。私も何度か失敗をしました。再帰呼び出しのマクロをデバッグする際には,PCを再起動する羽目に陥る可能性があります。必ず実行前にファイルを保存しましょう。
古庄 潤 |