指定フォルダ配下のファイル情報を取得
1.背景
パソコンやサーバ内のファイル情報は、図1-1のようにエクスプローラを使えば確認できます。但し目視しか出来ず、もし記録に残すのであればエクスプローラをプリントスクリーンしたりカメラで撮ったりするくらいです。デジタル値を取得するには、Windowsのコマンドプロンプト(Dos窓)で「Dir」コマンド等を使う方法がありますが、コマンドプロンプトは苦手だという人も多いようです。
図1-1
そこで今回は、指定フォルダー配下のファイル情報をExcelワークシート上に出力するアプリを紹介します。
2.システム概要
シート上の「フォルダ内検索」ボタンをクリックすると、図2-1の「フォルダーを指定するダイアログ」が開きます。図2-1
ダイアログ上で、情報を得たいフォルダーを選択(下のフォルダー名の場所に、フォルダー名が入力される)しOKボタンをクリックすると、指定したフォルダー配下を全て走査し、図2-2のようにファイル情報が書き出されます。
図2-2
今回取得している情報は以下の通りです。
またファイルだけで無く「サブフォルダー」も表示させており、その場合のフォルダー名は親フォルダーにしています。
1)ファイル名・・・・・・・・拡張子及びピリオドを含んだファイル名 2)フォルダー名・・・・・・・このファイルが所属しているフォルダー名 3)ファイルサイズ(KB)・・・キロバイト単位で切り上げした実サイズ。ディスク上のサイズではありません。 4)ファイル種類 5)ファイル属性・・・・・・・数値で表示していますが、その内容は図4-7で説明します。 6)作成日 7)最終アクセス日 8)更新日 |
なお、出力を途中で止めたい場合は、エスケープキー(ESCキー)を長押しして下さい。マクロが中断されますので、中止ボタンをクリックすると終了します。(但し途中のオブジェクトが残留してしまうはずなので、中断の多用はあまりお勧めしません)
3.プログラムの流れ
今回のシステムのプログラムの流れは、図3-1のようになります。図3-1
ボタンをクリックすると、図2-1のダイアログが表示されフォルダーの指定(図3-1の①)を行い、フォルダーが決まると前回表示したデータをクリア(図3-1の②)します。
続いて「ダイアログで指定したフォルダーを基準」として「情報出力ブロック③」に入り、まずその中のファイルについて1つ1つ順番④に情報を出力(図3-1の⑤)していきます。
ファイルの処理が全て完了したら、次に指定フォルダ内にある1つ目のサブフォルダの情報を出力(図3-1の⑦)した後、その場所から「1つ目のサブフォルダを基準」として「情報出力ブロック③」を呼び出します(図3-1の⑧)。
この「自分から自分を呼び出す」ことを「再帰呼び出し」と言います。
「再帰呼び出し」で呼び出された「情報出力ブロック③」では、「1つ目のサブフォルダ内」のファイルとサブフォルダの情報を出力していきます。
恐らく分かり難いと思いますので、実際のフォルダー(仮)で流れを追っていきます。
図3-2は、myDATAというフォルダがあり、その中には4つのファイルと2つのサブフォルダ(Folder1とFolder2)があることを示しています。またサブフォルダFolder1に下には3つのファイル、Folder2に下には6つのファイルと1つのサブフォルダ(Folder3)があり、Folder3の下には4つのファイルがある、と仮定します。
図3-2
ここで、myDATAフォルダをダイアログで指定した場合の処理の順番を表したものが図3-3です。丸数字が図3-1での処理工程で、丸英字が図3-2のフォルダー内のファイルおよびサブフォルダーになります。
また四角で囲った黒枠が「情報出力ブロック③」を表し、その中から「情報出力ブロック③」を再帰呼び出しし、更にまた再帰呼び出しする流れを示しています。
図3-3
つまり、対象フォルダ(myDATAフォルダ)での処理(情報出力ブロック③)は継続したまま、途中でサブフォルダ内の処理を別のプログラムとして「情情報出力ブロック③」を呼び出すのです。
自分自身を呼び出す、つまり「今処理しているプログラム自体を再帰呼び出しする」理由ですが、今回システムで言えば「基準の位置さえ変更すれば、同じ処理方法が使える」ためです。
最初に指定したmyDATAの「フォルダー内には、0~複数個のファイル、0~複数個のフォルダーが存在」しますし、その中の1つのサブフォルダーであるFolder1の「フォルダー内には、0~複数個のファイル、0~複数個のフォルダーが存在」します。つまり処理対象のフォルダーが変わるだけで処理工程は同じですので、同じプログラムを使用できるのです。
もしこれを別々なコードにしようとしても、「ダイアログで指定した任意のフォルダーの下に何個のサブフォルダーが存在するかは、プログラム作成時点では把握できない」ため、あらかじめ作っておくことが不可能です。
なお再帰呼び出しする際には、引き渡すものが必要です。今回の場合、処理の対象である「基準フォルダ」を渡すのと同時に、処理したデータを書き込む位置(書込みシート、行、先頭列)を一緒に渡し、指定された位置にデータを出力します。
また、図3-1では「全ファイルを1つずつ取り上げ」とか「全フォルダを1つずつ取り上げ」と書いていますが、実は「開けないサブフォルダー」が存在しますし、またWindowsのエクスプローラでは「通常見えないファイル・フォルダー」もあります。
「開けないサブフォルダー」を開こうとするとエラーが発生しますので、そのエラー処理対応が必要となります。また今回は、出力一覧をエクスプローラ相当に合わせているので、ファイル・フォルダーの属性を調べ(図3-1の⑨)て出力有無を制御しています。
ダイアログで指定したフォルダー配下の全てのファイル・フォルダーの処理が終了したら「情報出力ブロック」を抜け、終了(図3-1の⑩)します。
今回の再帰呼び出しは「呼出し元の自分を残したまま新たな自分を呼び出し」ていますが、OnTimeメソッド等を使って「自分が閉じた後に新たな自分を呼び出す」ことも「再帰呼び出し」と呼びます。 |
4.プログラム内容
4-1.ワークブック上の準備
今回は、ファイル情報をワークブック上に書き出しますので、図4-1のように情報タイトルを記入しボタンを配置します。図4-1
タイトル行は今回「3行目」とし、データは4行目以降に並べる事にしました。また列方向ではA列からデータを並べています。
なお一番上にCommandButtonを配置し、そのボタンに「標準モジュールのFileSearchプロシージャ」をマクロ登録します。
4-2.標準モジュール
4-2-1.走査フォルダーの指定
シート上のボタン(CommandButton)をクリックすることで呼び出されるプロシージャが図4-2です。- '========== ⇩① 走査フォルダーの指定 ====================
- Sub FileSearch()
- Dim DialogButton As Long 'ユーザーが表示ダイアログのどのボタンをクリックしたかの値
- Dim FolderName As String 'ユーザーが指定したフォルダー名
- Dim PasteSh As Worksheet '出力先のシート名
- Dim PasteRow As Long '出力する行の位置(データ開始行)
- Dim PasteCol As Long '出力する列の位置(データ開始列
- Set PasteSh = Sheet1 '出力先はSheet1(使う環境で調整して下さい)
- PasteRow = 4 '出力開始行=4行目から
- PasteCol = 1 '出力開始列=A列から
- With Application.FileDialog(msoFileDialogFolderPicker)
- DialogButton = .Show
- If DialogButton = 0 Then Exit Sub 'okボタンはー1、キャンセルボタンは0、右上×印も0
- FolderName = .SelectedItems(1) 'フォルダ名を取得
- End With
- Call SheetClear(PasteSh, PasteRow, PasteCol) '旧データをクリア
- Call FileInfo(FolderName, PasteSh, PasteRow, PasteCol) 'データ出力を開始
- End Sub
9~11行目では、走査したファイル・フォルダー情報を記入する場所の指定をしています。
9行目では、Excelのシートを指定します。ここでは「Sheet1」としていますが、マクロをAddin化して9行目を「ActiveSheet」指定にすることも可能です。その場合にはタイトル行も一緒に出力する工夫が必要かもしれません。
10行目は出力開始行の設定です。ここで使われる変数PasteRowは、20行目で「情報出力ブロック」に引数として渡された後、「出力する行を示す値」に変化し、データを1行出力するたびに「1ずつ増加」していきます。
11行目は出力開始列です。今回出力する種類は8種ですので、1列目(A列)から8列目(H列)までがデータ列になります。
13~17行目は、基準のフォルダーを指定する部分です。
14行目は、組み込みダイアログの1つである「msoFileDialogFolderPicker」をShowメソッドで表示させます。表示されるダイアログは図2-1のようなもので、フォルダーを選択すると下の「フォルダー名」欄にフォルダー名が入ります。
そして、ダイアログ右下のOKボタン又はキャンセルボタンをクリックするとダイアログは閉じられ、押したボタンの種類が戻り値として変数DialogButtonに代入されます。
戻り値は「OKボタン=-1」「キャンセルボタン=0」であり、ダイアログ右上の×印をクリックした場合もゼロが戻ります。
15行目は押されたボタンの種類を判別し、「キャンセルボタン」または「右上の×印」をクリックした場合は、プロシージャを抜け出します。
「OKボタン」を押された時だけ16行目に移り、「.SelectedItems(1)」で「ユーザーが指定したフォルダー名」をString型で受け取り、変数FolderNameに代入します。
今回使用する組み込みダイアログでは「1つの値(フォルダー名)しか選択できません」ので、「.SelectedItems(1)」以外の「(2)等」は存在しません。 また、ダイアログで「何も指定せずにOKボタンをクリック」した時には、1つ前の作業で指定したフォルダーとなります。その値は、「Application.FileDialog(msoFileDialogFolderPicker). InitialFileName」で取得できます。 1つ前も無かった時(Excel起動直後など)には、Excelの設定値「既存のローカルファイルの保存場所」(ファイル→オプション→保存→ブックの保存 で設定)となるようです。 |
19行目では、シート上のデータを消去しています。
図4-16の「SheetClearプロシージャ」を呼び出し、引数として「シート名」「データ行開始位置」「データ列開始位置」を渡します。
20行目では、「対象フォルダー名」及び「シート名」「データ行開始位置」「データ列開始位置」を引数にして、「情報出力ブロック」であるFileInfo(図4-3)プロシージャを呼び出します。
図3-3の一番上の長い四角枠が、20行目で呼び出している「情報出力ブロック」であり、全てのファイル・フォルダーの走査・出力が終了すると、22行目の「End Sub」に移りマクロ終了します。
4-2-2.情報出力ブロック
最初に図4-2の20行目から呼び出され、また自分自身(図4-3の74行目)からも呼び出されるのが図4-3です。- '========== ⇩② 情報出力ブロック ====================
- Private Sub FileInfo(myPath As String, PasteSh As Worksheet, PasteRow As Long, PasteCol As Long)
- Dim Fs As Object 'FileSystemObjectオブジェクトの生成用変数
- Dim myFolder As Object '調べるフォルダーのオブジェクト変数
- Dim myFile As Object '調べるフォルダー内の1つ1つのファイルオブジェクト
- Dim mySubFolder As Object '調べるフォルダー内の1つ1つのサブフォルダーオブジェクト
- Dim Fcount As Long '調べるフォルダー内のファイルの数量
- Dim isRead As Boolean '調べるフォルダーが読み込めるか否かのフラグ
- Dim temp(1 To 8) As Variant '1つのファイル・フォルダーの情報を格納する配列
- Set Fs = CreateObject("Scripting.FileSystemObject")
- Set myFolder = Fs.GetFolder(myPath)
- On Error Resume Next
- Fcount = myFolder.Files.Count
- If Err.Number = 0 Then isRead = True
- On Error GoTo 0
- If isRead = True Then
- For Each myFile In myFolder.Files
- If Not (DtoB(myFile.Attributes)(2) And DtoB (myFile.Attributes)(3)) Then
- temp(1) = Fs.GetFileName(myFile.path) 'ファイル名
- temp(2) = myFile.ParentFolder.path '親のパス名
- temp(3) = Application.WorksheetFunction.RoundUp (myFile.Size / 1024, 0) 'ファイルサイズ
- temp(4) = myFile.Type 'ファイルタイプ
- temp(5) = myFile.Attributes '属性
- temp(6) = myFile.DateCreated '作成日
- temp(7) = myFile.DateLastAccessed '最終アクセス日
- temp(8) = myFile.DateLastModified '更新日
- PasteSh.Cells(PasteRow, PasteCol).Resize(1, 8) = temp
- Erase temp
- PasteRow = PasteRow + 1
- End If
- Next myFile
- For Each mySubFolder In myFolder.SubFolders
- If Not (DtoB(mySubFolder.Attributes)(2) And DtoB(mySubFolder.Attributes)(3)) Then
- temp(1) = mySubFolder.Name
- temp(2) = mySubFolder.ParentFolder.path
' temp(3) = Application.WorksheetFunction.RoundUp (mySubFolder.Size / 1024, 0)- temp(4) = mySubFolder.Type
- temp(5) = mySubFolder.Attributes
- temp(6) = mySubFolder.DateCreated
- temp(7) = mySubFolder.DateLastAccessed
- temp(8) = mySubFolder.DateLastModified
- PasteSh.Cells(PasteRow, PasteCol).Resize(1, 8) = temp
- Erase temp
- PasteRow = PasteRow + 1
- Call FileInfo(mySubFolder.path, PasteSh, PasteRow, PasteCol)
- End If
- Next mySubFolder
- End If
- Set Fs = Nothing
- Set myFolder = Nothing
- End Sub
24行目で受け取る引数は、以下の4つです。
myPath :調べるフォルダー名(文字列型)
PasteSh :出力先のシート名(オブジェクト型)
PasteRow :出力先の行位置(数値)
PasteCol :出力先の先頭の列位置(数値)
33行目では、FileSystemObjectオブジェクトを生成し変数Fsに代入しています。
34行目では、第一引数の文字列myPathを引数にしてGetFolderで「調べるフォルダーのオブジェクト」を取得し、myFolderに代入しています。
36~39行目は、その調べるフォルダーの中身が読み取れない場合に、情報の収集と出力を取りやめる準備をするものです。
1つ1つのフォルダーには「Attributes」という「属性」を示す値が設定されており「見える」「見えない」が決まるのですが、それとは無関係に図4-4の様に、ファイルの中を見ようとすると「アクセスが拒否」される場合があります。
この拒否の原因は「Windousシステムが使用しているため」などがありますが、何の理由にしてもフォルダーの中が見えないのであれば、諦めるしかありません。
そこで、37行目で対象フォルダーの中に何個のファイルが存在するかを調べようと試み、失敗すれば「フォルダーの中が見られないんだな」と諦めることとしました。(読める・読めないのチェックは、フォルダー内を調べようとする行動であれば何でもOKだと思います。)
図4-4のようなエラーを検知するため、36行目に「On Error Resume Next」を置き、37行目のエラー有無を38行目で判断し、読み取れるのであれば42行目以降の「情報吸い上げ・出力工程」を実行するように、38行目で変数isReadにTrueを代入します。
(変数isReadは、30行目で宣言しただけですので、初期値はFalseになっています)
39行目では、できるだけ早くエラー処理を元の状態に戻すために「On Error GoTo 0」を実行します。
41行目は、変数isReadがTrue(=正常にフォルダー内が開ける)の時に、42~76行目を実行します。
なおFalseの時(=フォルダー内が見えない時)は、79~80行目で生成したオブジェクトを解除し終了させます。
42~76行目は、For~Next文とIf文が混ざっていますので、図4-5に整理しました。
図4-5
上半分の42~57行目が対象フォルダーの中の「ファイル」についての処理を行い、下半分の59~76行目が対象フォルダーの中の「サブフォルダー」についての処理を行っています。
実際の処理部分では、黄色の部分がファイル・フォルダーの情報を配列に格納している部分で、次に緑色部分で配列に収めた情報をシート上に書き出します。
書き出しが終了したら次の工程での準備のために、青色部分で配列内容を消去し、書き出し行位置を1つ下に移動させます。
オレンジ色の74行目は、サブフォルダーの中身を調査するために、第一引数(対象フォルダ)にサブフォルダ名を渡して、再帰呼び出しをします。
43行目および60行目の「属性による出力有無」については、あとで図4-7のところで説明します。
先に黄色の部分のファイル・フォルダーの情報ですが、コードに表した以外にもいくつかあります。
図4-6は「ファイル」についての表になっていますが、「myFile」を「myFolder」に変えればフォルダー情報が得られます。
No. | 情報 | メソッド | プロパティ |
---|---|---|---|
1 | ファイル名 | Fs.GetFileName (myFile.path) | myFile.Name |
2 | ファイル名(フルパスを含む) | Fs.GetAbsolutePathName (myFile.path) | myFile.Path |
3 | ファイル名(拡張子・ピリオド含まず) | Fs.GetBaseName (myFile.path) | - |
4 | 拡張子(ピリオド含まず) | Fs.GetExtensionName (myFile.path) | - |
5 | ドライブ名 | Fs.GetdriveName (myFile.path) | myFile.Drive.Path / myFile.Drive.DriveLetter |
6 | 親のパス名 | Fs.GetParentFolderName (myFile.path) | myFile.ParentFolder.path |
7 | ファイルサイズ(バイト) | - | myFile.Size |
8 | ファイルタイプ | - | myFile.Type |
9 | 属性 | - | myFile.Attributes |
10 | 作成日 | - | myFile.DateCreated |
11 | 最終アクセス日 | - | myFile.DateLastAccessed |
12 | 更新日 | - | myFile.DateLastModified |
今回は、これらの情報の内から「名前、フォルダ名、サイズ(KB)、種類、属性、作成日、最終アクセス日、変更日」の8種を出力しています。メソッド・プロパティのどちらを使っても、同じString型の情報が得られます。
なお、サイズについては、エクスプローラではKB(キロバイト)単位で表示されていますので、それに合わせました。また、エクスプローラではKB単位で切り上げ表示されているので、RoundUpを使って切り上げをしています。
また、サイズはファイルそのもののサイズであり、「ディスク上のサイズ」ではありません。「ディスク上のサイズ」は、クラスタサイズ(Windowsでは「アロケーションユニットサイズ」)単位となりますが、Excelからクラスタサイズを取得するのは大変そうなので、今回は諦めました。
また、フォルダーでのサイズは「対象フォルダー配下の全ファイルの合計サイズ」になるようですが、「サイズがエラーになるフォルダーも存在する」ので実行しない事にしました。ですので、サイズの列(今回ならC列)の値を合計することで、各フォルダーのサイズを計算して下さい。
53・70行目では、ファイル・フォルダー情報を格納した配列tempをシートに貼り付けています。
54・71行目では、配列tempの内容を消去します。消去しないと、フォルダー情報のサイズ欄に前のデータが残る可能性があります。
55・72行目では、出力する行位置を1つ下に下げています。
そして74行目では、サブフォルダー名を新たな基準フォルダー名にして再帰呼び出しをします。
では少し戻って、43・60行目の「属性による仕訳け」について説明します。
属性値は、図4-6のNo.9のように「Attributes」で得ることができます。
属性とは「読み取り専用」とか「隠しファイル」などというファイルの特徴で、図4-7の様な種類があります。
値 | FileAttribute列挙型の要素名 | 属性(特徴) |
---|---|---|
0 | Normal | 標準ファイル |
1 | ReadOnly | 読み取り専用ファイル |
2 | Hidden | 隠しファイル |
4 | System | システムファイル |
8 | Volume | ディスクドライブ、ボリュームラベル |
16 | Directory | フォルダ、ディレクトリ |
32 | Archive | アーカイブファイル |
1024 | Alias | リンク、ショートカット |
2048 | Compressed | 圧縮ファイル |
ファイル1つに対して特徴は1つではなく、複数の特徴が組み合わさっている場合もあります。その場合は値を足し合わせた合計値が属性値となりますが、その合計値は逆算・分解して「どの属性を持っているか」を知ることができます。
これは、各値が「2n」という形で表されているためで、図4-8の様に属性値を2進数で表現してみると、各桁が1か0かを調べる事で属性が含まれているかが分かります。
図4-8
図4-7の組み合わせは全部で256種類になります。しかし実際に何種類あるのか、私のPCの中のファイルの属性を調べてみたのが図4-9・図4-10・図4-11です。(最初から64種類までの組合せを全て並べたのが図4-9・図4-10で、それ以上が図4-11)
「●と〇」を記したところが実際に存在した属性値で、「・」のところでは見つかりませんでした。
また、●はエクスプローラで見えるファイル・フォルダー、〇は見えないファイル・フォルダーです。
図4-9
図4-10
図4-11
ファイル・フォルダーが見える・見えないは、エクスプローラの設定が関係しています。今回は以下の設定でトライしています。
1つ目は、エクスプローラの「表示」→「表示/非表示」→「隠しファイル」での設定で、上記表は隠しファイルを見える様にしています。
2つ目は、エクスプローラの「表示」→「オプション」→「表示」→「詳細設定」→「保護されたオペレーティング システム ファイルを表示しない」での設定で、こちらは推奨の通り「表示しない」にしてあります。
私は今まで、「隠しファイル」設定を「隠さない」にしておけば、全てのファイルが見えると思っていましたが、そうではありませんでした。上記の3つの表を眺めていると、「隠しファイル(値2)」と「システムファイル(値4)」の両方が属性になっているファイル・フォルダーはエクスプローラでは見えないようです。(どこのサイトにも記述が見つからないので暫定情報です)
ということで、今回出力される表示を「エクスプローラ表示に合わせる」ために、属性が「隠しファイル」且つ「システムファイル」の場合にはスルーさせることにし、43・60行目のIF文を設定しました。なお、「隠しファイル」が「DtoB(myFile.Attributes)(2)」、「システムファイル」が「DtoB(myFile.Attributes)(3)」に相当するのですが、それについては図4-12で説明します。
33行目で「FileSystemObjectオブジェクト」の生成を行っていますが、再帰呼び出しをされるたび、つまりサブフォルダーが深い場合には何回も同じこの行が実行され「FileSystemObjectオブジェクト」が重複して生成される気がします。 しかしMicrosoftの「FileSystemObject のプログラミング」では「FileSystemObjectのインスタンスは、ほかのインスタンスを何回作成しても1つしか作成されない」と説明しており、重複生成によりメモリを圧迫する等の心配は無いようです。 それでも気になる方は、オブジェクト変数Fsをモジュールレベル変数として宣言し、1回しか実行されないように図4-2内で生成するようにしてもOKです。 |
4-2-3.属性値の分解
図4-3の43行目・60行目から呼び出される「DtoB関数」が図4-12です。この関数は、引数に10進数の整数を渡すと、2進数相当の配列を返す関数です。配列の中は、2進数の1相当だったらTrueを、0相当だったらFalseが入ります。ファイル・フォルダーの属性値は、図4-8のように2進数で表現すると「どの属性が含まれているか」が分かります。ですので、この「DtoB関数」に属性値を渡し、返ってきた配列の中身を調べることで「どの属性が含まれているか」が分かることになります。
- '========== ⇩③ 属性値の分解 ====================
- Function DtoB(attrNO As Long) As Boolean()
- Dim Remainder As Long '2で割った余り。1か0になります。
- Dim arrayBin() As Boolean '2進数相当の各桁を入れる動的配列
- Dim i As Long 'カウンター変数(2進数の桁数を表す)
- i = 1
- Do
- Remainder = attrNO Mod 2
- attrNO = Int(attrNO / 2)
- ReDim Preserve arrayBin(1 To i)
- arrayBin(i) = CBool(Remainder)
- i = i + 1
- If (attrNO < 2) Then
- If (attrNO = 1) Then
- ReDim Preserve arrayBin(1 To i)
- arrayBin(i) = CBool(attrNO)
- End If
- Exit Do
- End If
- DoEvents: DoEvents
- Loop
- If UBound(arrayBin, 1) < 12 Then
- ReDim Preserve arrayBin(1 To 12)
- End If
- DtoB = arrayBin
- End Function
引数である attrNO は「属性値(Attributesの値)」です。その属性値を88行目以下のDo~Loop内で2進数相当に変換していきます。
ちなみに手計算では、図4-13の左側のように「元の値を2で割り、その商を下に書き次の値とする。余りは右端に書く」を商がゼロになるまで繰り返し行います(図4-13の中央)。計算が終わったら余りを下の方から拾い上げます(図4-13の右側)。
図4-13
この手計算の手法を計算式に置き換えたのが図4-14です。
図4-14
「元の値を2で割り、余りは右端に書く(=保存する)」が89行目の「Remainder = attrNO Mod 2」に相当し、「元の値を2で割り、その商を次の値とする」が90行目の「attrNO = Int(attrNO / 2)」になります。
これを2進数として考えると、2進数のブロックを1つずつ右側に移動(ワークシート関数のBITRSHIFTの動きのイメージ)し、右にはみ出た値(余り値)を下の桁から埋めていく、と言うことになります。
91行目は、1つ余りが出るたびに「余りの入れ物である配列のサイズを確保」し、92行目では、その確保した場所に余り値を代入しています。
通常は、余り値をそのまま配列に格納したり、文字列として最下桁からつなげて行くのですが、今回は図4-3の43行目・60行目で「属性有無の判断」に使用するため、「1または0」をCBool関数を使って「True または False」に変更し、If文を簡略化しています。
94行目はDo~Loopの脱出条件で、元の値が割る値より小さくなった時に抜け出します。手計算(図4-13)では元の値がゼロになるまで計算を続けていますが、94行目はその1つ前で判断を下している形です。
最後に残った「元の値」が1であれば、96行目で配列の枠を増やして、97行目でTrueを代入します。ここで処理している桁は一番上の桁ですので、もし「元の値」がゼロであるなら「一番上の桁に、ゼロを入れても入れなくても同じ」ですので、無視しています。
計算が終わったら99行目でDo~Loopを抜け出します。
104~106行目の処理は、今回システムでは必須です。例えば属性値が3であった場合、図4-15のようにIf文側で「配列の大きさが合っていない」不具合が発生してしまいます。
図4-15
ですのでIf文でエラーが出ないように、105行目で配列の大きさを広げています。広げる大きさは、今回の場合の必要最小限は3(隠しファイル=2、システムファイル=3)ですが、もしコード改造でIF文を変更してもエラーが出ないようにするため、図4-8の最大の12まで広げています。
なお広げた分については、配列はBoolean型で宣言されていますので全てFalseで埋められます。
処理が終了したら、データを入れた配列arrayBinを108行目で戻り値にします。
配列を戻り値にしているため、このFunctionの型は、83行目で「As Boolean( )」とカッコを付けて宣言しています。
4-2-4.シートのクリア
図4-2の19行目から呼び出される「旧データのクリア」が図4-16です。受け取る引数は、Psh(データ出力のシート)、Prow(データ出力開始行位置)、Pcol(データ出力開始列位置)です。
- '========== ⇩④ シートのクリア ====================
- Private Sub SheetClear(Psh As Worksheet, Prow As Long, Pcol As Long)
- Dim Rng As Range '←記述されているセル範囲
- Set Rng = Psh.Cells(Prow - 1, Pcol).CurrentRegion
- If Psh.FilterMode = True Then Psh.ShowAllData
- If Not (Rng.Rows.Count = 1 Or Rng.Rows.Count + Rng.Row = Prow) Then
- Rng.Offset(Prow - Rng.Row, 0).Resize(Rng.Rows.Count - (Prow - Rng.Row), Rng.Columns.Count).Clear
- End If
- End Sub
113行目では、データが記述されている範囲をCurrentRegionで取得します。「CurrentRegion」は選択セル範囲に対して「アクティブセル領域」を取得するプロパティです。そのCurrentRegionの基準となる「選択セル」をどの位置にするか(データ行のトップ=4行目、またはタイトル行=3行目)を考えてみます。
アクティブセル領域とは「選択セル範囲から、すべての方向の最初の空白行・空白列までの領域」で、今回システムで言えば図4-17の様になります。薄い緑色が「空白行・空白列」で、①は3行目を選択した場合、②は4行目を選択した場合です。
選択セルがタイトル行・データ行の一部ですので、①の場合も②の場合も同じアクティブセル領域になります。
図4-17
一方、データが無かった場合は、図4-18のようにタイトル行だけになります。この場合、選択セルが3行目なのか4行目なのかでアクティブセル領域は変わってきます。
③の場合は、①②と同様にセルに値がある範囲がアクティブセル領域になります。
しかし、④の場合は選択したセルも含まれますので、4行目もアクティブセル領域に入ります。
図4-18
また今回は3行目をタイトル行としていますが、タイトル行が無い場合も考えられます。
この場合も選択セルが3行目なのか4行目なのかでアクティブセル領域は変わってきます。
図4-19
最後に、タイトル行も無く、データも無い場合を考えます。何もない時には、図4-20のように選択セル自身がアクティブセル領域になります。
図4-20
以上をまとめたのが図4-21です。この表から、選択セル(=CurrentRegionの基準セル)を「データ先頭行(4行目)」にするか「その上(3行目)」にするかを考えます。
パッと見で、上半分の3行目に選択セルがある方が「(B)-1=(A)」が成り立ちますし、データ行は4行目からですので(C)が揃っているのも計算式を立て易そうです。また、データが無い場合(③⑦)は「(B)=1」で見分けられます。
(4行目の方でも、たぶんできるのでしょうが、If文で細かく分ける必要があることは推定できます)
選択セル 位置 | 番号 | タイトル行有無 | データ行有無 (A) | CurrentRegion の行数 (B) | CurrentRegion の先頭行 (C) |
---|---|---|---|---|---|
3行目 | ① | 有り | 有り=3 | 4 | 3 |
③ | 有り | 無し=0 | 1 | 3 | |
⑤ | 無し | 有り=3 | 4 | 3 | |
⑦ | 無し | 無し=0 | 1 | 3 | |
4行目 | ② | 有り | 有り=3 | 4 | 3 |
④ | 有り | 無し=0 | 2 | 3 | |
⑥ | 無し | 有り=3 | 3 | 4 | |
⑧ | 無し | 無し=0 | 1 | 4 |
ということで「3行目を選択セル」とする事とします。
ついでに図4-22のように「タイトル行を2行目に作られてしまった」時のこともチェックしておきます。
図4-22
以上、3行目を選択セルにした場合の「タイトル行の位置」によるCurrentRegion範囲を、図4-23に整理しました。
タイトル行 の位置 | 番号 | データ行有無 (A) | CurrentRegion の行数 (B) | CurrentRegion の先頭行 (C) |
---|---|---|---|---|
3行目 | ① | 有り=3 | 4 | 3 |
③ | 無し=0 | 1 | 3 | |
2行目 | ⑨ | 有り=3 | 5 | 2 |
⑩ | 無し=0 | 2 | 2 | |
なし | ⑤ | 有り=3 | 4 | 3 |
⑦ | 無し=0 | 1 | 3 |
(A)(B)(C)の値を見比べてみると、「(A)=(B)-(4-(C) )」が成り立ちます。(数値の4は、データ開始行の4です)
また、データ削除不要のデータ行ゼロを判断するには「(B)=1」だけでなく、「(B)+(C)=4」の条件も必要であることが分かります。
( ⑩の条件は(B)=(C)であるようにも見えますが、間違ってます。データ開始行が5行目と仮定して考えると分かります。)
以上を元に、もう一度コードを見ていきます。
113行目のCurrentRegionの選択セルは、図4-21から「データ開始行の1つ上」に決めたので、「Psh.Cells(Prow - 1, Pcol).CurrentRegion」としました。
そして118行目は「データ範囲のみ」を選択しているコードで、CurrentRegion範囲を「Prow - Rng.Row」だけ下に移動し、行方向のサイズは「Rng.Rows.Count - (Prow - Rng.Row)」にしています。
この範囲に対してデータの消去を行いますが、何のメソッドを使って消去をするかを考えます。
一般的には「範囲.ClearContents」や「範囲=" "」が良く使われます。しかし試運転してみると不具合点が見つかりました。
今回システムで、まず大量のデータがあるフォルダーを解析すると、シート右端のスクロールバーのMAX値がデータ行の下端になります。
次に少量データのフォルダーを解析すると、データ行数は少ないのですが、シート右端のスクロールバーのMAX値は大きいままで変わりません。
つまり「スクロールバーを少し動かしても、大きく動いてしまう(=敏感になる)」不具合が発生し、作業がやり難くなります。
この原因は、ClearContentsメソッド(「長さゼロの文字列を埋める」でも同じです)では、値は消去できても書式等は残ってしまうためです。その代わりに「Clearメソッド」を使用すると、書式まで消去しますのでスクロールバーも戻ってくれます。
(但し、上書き保存をするまでは、スクロールバーは戻りません。)
ということで、今回データの消去には「Clearメソッド」を使用しました。
その前の117行目は「データ行数がゼロだった時には、何もしない」コードです。条件は2つあり「CurrentRegion範囲が1行」または「CurrentRegion範囲の行数+CurrentRegion範囲の先頭行がデータ先頭行位置」です。
少し前に戻って115行目は、フィルタで絞り込んでいた時にはフィルタを解除しています。
これはフィルタを掛けたまま(行を見えない状態にしたまま)データ消去をしても、見えていない行に対してはデータが消えずに残ってしまうためです。
この現象は、データ範囲に対して一括で消去する時に発生します。Clearメソッド、ClearContentsメソッド、長さゼロの文字列を埋める方法 どれでも同じです。
但し1行ごとに消去していく方法(例えばFor~Nextを使う)であれば、時間が掛かっても見えない行のデータも消去してくれます。しかしそうやって全データを消去できたとしても、新しく出力したデータにしてみれば意味の無い絞り込みです。
ですので、データ範囲の消去を行う前にフィルタ解除をしています。
5.最後に
今回のシステムはファイル・フォルダーを書き出すだけのものですが、少し甘く見ていました。見えないフォルダーや開けないフォルダーの存在です。属性値(Attributes)だけでは説明できず、Attributes=16の普通のフォルダーに見えるものでも開けないものがあり、最終的にはエラー処理で対応せざるを得ませんでした。FileSystemObjectオブジェクトはファイルを扱う時には必須のものですが、このような情報は他のサイトでもあまり紹介されていないようです。
しかし、分からないこと・人がやっていないことだからこそ、新しそうな真実?を見つけるのは楽しく、つい時間を忘れてしまいます。
指定フォルダ配下のファイル情報を取得(it-042.xlsm)
セキュリティ向上を目的として「インターネット経由でダウンロードしたOfficeファイル(Excel等)のマクロは、既定でブロック」されるようにOfficeアプリケーションの既定動作が変更になりました。(2022年4月より切替開始) 解除の方法については「ダウンロードファイルのブロック解除方法」を参照下さい。 |