†Sibylのお部屋†
作成開始日 2021.12.26
最終更新日 2023.02.06
rc:=FindFirst('c:\mydir\*.*', faAnyFile, myrec); //myrecはTSearchRec型 While rc=0 Do Beginここで、rcはLongInt型の戻値で、成功ならば0、失敗ならば負の値が返されるそうだ(実際は正の値の事もあるような気がするが…)。が、このループは、な〜んかしっくりこない。初回のループ判断および最初のファイル情報の処理が、FindFirstの結果を使う点にかなり異和感を感じる。ループの外の出来事をループ内に持ち込む感じがイヤ。(〜取得したファイル情報の処理〜) rc:=FindNext(myrec); End; FindClose(myrec);
それに、この構造では、ループ内でContinue命令を使うと無限ループに陥る。しかし、一定の条件(ファイル名とか作成日時とか)で、処理をスキップするような場合は頻繁にあるので、Continueが封じ手になるのはけっこうキツイ。
repeat〜untilループを使えばもうちょっと奇麗になるが、エントリがない場合にエラーになる?該当エントリがなくても、一度は処理ルーチンを通ることになるので…該当エントリがない場合は事前にハネるという方法もあるが、但し書きの多い読み難いソースになるなぁ…
んじゃ、どんな形がスッキリするのかと言うと…
FindFirst('c:\mydir\*.*', faAnyFile, myrec); While FindNext(myrec)=0 do Beginこれだと、もの凄〜くスッキリした感じになるのだが、最初のエントリを読み飛ばすことになる。しかし、HPFSの最初のエントリは「.」で、そもそも使い途がない。なので、読み飛ばしはむしろ好都合。FATやLANドライブもサブディレクトリであれば同様である。ところが、FAT/LANドライブのルートだけは例外で「.」も「..」も存在しない。したがって、実在する最初のエントリが読み飛ばされてしまう。これでは使い物にならない(u_u;)(〜取得したファイル情報の処理〜) End; FindClose(myrec);
【追記】HPFSでも、システムドライブ(C:)のルートには「.」「..」が存在しなかった。データドライブには存在しているのだが…
いっそ、TSearchRec型のリスト(または動的配列)を作成しておいて、そこに全ての取得データを一時保管してから、処理に掛かる方が良いかも知れない。これをユーザー関数にするのが一番奇麗な形かな?「FileList:=GetFileList(path, attr)」みたいに。
FindFirst('c:\mydir\*.*', faAnyFile, myrec); If myrec.name<>'.' thenリスト(TList型)の扱いはけっこう面倒臭いので、具体的なコードは後述(予定)。(リストに追加) ; While FindNext(myrec)=0 do(リストに追加) ; FindClose(myrec);(〜取得したファイル情報の処理〜)
プロパティ | データ型 | 備 考 |
SearchRec.Name | String | ファイル名/ディレクトリ名 |
SearchRec.Attr | Byte | ファイル属性(faReadOnly、faHidden、faDirectory等) |
SearchRec.Time | LongInt | 日時情報(内部形式)FileDateToDateTimeで標準形式に変換 |
SearchRec.Size | LongInt | ファイルサイズ |
SearchRec.HDir | LongWord | 恐らくファイルハンドルだと思うが詳細不明 |
属性や日時に関しては補足が必要だろうが、ここでは割愛。要するに、さまざまなデータが一度に取得できる、と言うこと。
FindFirst('c:\mydir\myimage.jpg', faAnyFile, myrec);これは何かと言うと、実は「myimage.jpg」というファイルの各種属性を取得する命令である。ある特定のファイルのサイズや作成日時を知りたいときは、このコマンド1つで取得可能なのである。これはけっこう便利だ−−と言うよりも、そもそも、「FindFirst」はそうした用途のための命令なのではないだろうか?つまり、そもそもはループで一覧を取得するような使い方を想定してなかったのではないか?
プロシージャ名も「FindFirst」ではなく、単なる「Find」あるいは「FindFile」などの方が相応しい気がする。
なお、2番目の引数「faAnyFile」は全てのファイル(隠しファイルなども含む)を検索対象とする、と言う意味である。ここに「0」を指定すれば通常のファイルのみが検索対象になる。
// myrecをTSearchRec型、fnameを文字列型配列、nをInteger型とする n:=1; FindFirst('c:\mydir\*.*', faDirectory, myrec); fname[n]:=myrec.Name; // 例としてディレクトリ名/ファイル名を取得 While FindNext(myrec)=0 do Begin inc(n); fname[n]:=myrec.Name; // 2番目以降のディレクトリ名/ファイル名 ..... End;この処理には突っ込み所が満載だ。まず、「faDirectory」という属性指定。これは、検索対象に「ディレクトリを
属 性 | 意 味 | |
0 | 通常ファイルのみ(何で定数定義されていないんだ?) | |
faAnyFile | すべてのファイルを含む | |
faArchive | faMustArchive | アーカイブファイルを含む/アーカイブファイルのみ |
faDirectory | faMustDirectory | ディレクトリを含む/ディレクトリのみ |
faHidden | faMustHidden | 隠しファイルを含む/隠しファイルのみ |
faReadOnly | faMustReadOnly | 読取専用ファイルを含む/読取専用ファイルのみ |
faSysFile | faMustSysFile | システムファイルを含む/システムファイルのみ |
つまり、「faDirectory」を指定すると検索対象は「通常ファイル+ディレクトリ」となる。字面からはディレクトリのみのように見えてしまうのが困ったところ。それに、ファイルとディレクトリの両方を検索対象にするなら「faAnyFile」でもいいんじゃね?と言いたいところだが、これだと隠しファイルやシステムファイルも取得してしまう。
次に考えるべきは、ディレクトリ一覧の最初の要素である。未だにFindFirst/Nextで取得されるエントリの順序がどうなっているのか良く判らないのだが(ディレクトリ名/ファイル名混在の名前順?)、確実なのは最初のエントリが「.」(カレントディレクトリ)で、次が「..」(親ディレクトリ)であること。検索対象にディレクトリが含まれていれば、これは動かないようだ。
つまり「FindFirst('*.*', faDirectory, myrec);」で取得されるのは必ず「.」なのである。これって要らないよね?「..」の方はまだ使い途があるが、「.」はまったく不要と言ってよい。すなわち、「FindFirst」は不要なエントリを読み飛ばしてくれるわけだ。変態仕様だと思ったが、そう考えるとなかなか便利である。
無論、便利なことばかりではない。使用頻度の高い「FindFirst('*.txt', 0, myrec);」のような使い方では、最初のファイルと2番目以降のファイルは、別々に処理しなければならない。処理としてカッコ悪いのである。FindFirst/FindNextを使って、REXXの「SysFileTree」関数互換のユーザー関数を自作するのも手かも知れない。
最後に、「FindNext」のループの問題を指摘しておくと、エントリの取得に成功すると戻り値が「0」という仕様は混乱を招きやすい。DOS系コマンドの共通ルールかも知れんが、ソースの可読性を考えると、やっぱり適当な自作関数を被せる方が良いと思う。
しかし、TStringsクラスの「Add」メソッドと「Insert」メソッドを使い分ければ、もう少しスマートにまとめることができる。たとえば、ListBox1にディレクトリとファイルを別けて登録するには、次のようにする。この場合、0〜n-1がディレクトリ、n〜がファイルとなる。
n:=0; //ディレクトリを追加する位置のインジケータ FindFirst('e:\downlad\*.*', faDirectory, myrec); //1項目の[.]は読み飛ばし While FindNext(myrec)=0 Do Begin if myrec.attr=faDirectory then Begin ListBox1.Items.Insert(n,'['+myrec.Name+']'); //ディレクトリ名は[]で囲む Inc(n); End else ListBox1.Items.Add(myrec.Name) End;
Procedure TForm1.GetFileList(path:string; attr:byte; List:TList); Var rec : TSearchRec; prec:^TSearchRec; Begin FindFirst(path, attr, rec); If rec.name<>'.' then Begin new(prec); prec^:=rec; List.Add(prec); End; While FindNext(rec)=0 do Begin new(prec); prec^:=rec; List.Add(prec); End; FindClose(rec); End;で、これをどうやって使うのかと言うと、第1引数にパスを、第2引数に属性を指定すれば 、FListファイルに一覧が取得できる。以下の例では、その後でファイル/ディレクトリごとにまとめている。ソート等はその後の処理になる。
///// ファイル一覧の読み込み FList.Clear; //FListはTList型で事前に生成されているものとする GetFileList('c:/mydir/*.*', faDirectory, FList); //FListにファイル一覧を取得 //ここから後は追加の処理例 //ファイル/ディレクトリの分離、ディレクトリ数のカウント、ファイルサイズの合計 filesize:=0; m:=0; For n:=0 to FList.Count-1 do If TSearchrec(Flist[n]).Attr=faDirectory then Begin Flist.Exchange(n,m); inc(m); End Else filesize:=filesize+TSearchRec(FList[n]).size; //ファイルサイズの合計 iDir:=m-1; //ディレクトリの末尾のインデックス iFil:=m; //ファイルの先頭のインデックス
なので、取得時にマスクを掛けるのではなく、いったん全てのファイル/ディレクトリを取得した後に、マスクで振り分ける方法を取るしかないと思う。あるいは「全ディレクトリ取得」と「マスク付きファイル取得」の2度ループを回すか?−−faMustDirectoryが正常ならば、だが。SPCCのファイル関連のダイアログあたりを使えば簡単な方法を見つけられるかも知れないが…
とりあえず、コマンドラインではEditFileName関数が使用できそうだ。EditFileName関数はもともとファイルの拡張子の付け替えをするためのものらしい;
st:=EditFileName('myfile.txt','*.bak') //'myfile.bak'が得られるこの機能を利用すれば、マスクを掛けることが可能になる。もし、元ファイル名と、拡張子を付け替えたファイル名が同じならば、そのファイルの拡張子は、マスクで指定した拡張子と同じだと判断できる。ちょっとややこしいが、こう言うこと;
fname:='mypic.jpg' If fname=EditFileName(fname','*.jpg') then ... //真になる(mypic.jpg = mypic.jpg) If fname=EditFileName(fname','*.png') then ... //偽になる(mypic.jpg <> mypic.png)ちなみに、EditFileName関数のマスクは拡張子だけでなく、「s*.*」とか「a*.txt」のようにファイル名の本体部分に対しても指定が可能。したがって、ファイル名一般のマスクとして使用できる。これを使って、ファイル一覧を取得する関数を書き直すと…(流石に今回は処理の重複がカッコ悪いので、While rc=0ループで一元化した)。
//----------------------------------------------------------------------------- Procedure TForm1.GetFileList(path,mask:string; attr:byte; List:TList); //----------------------------------------------------------------------------- Var rec : TSearchRec; prec :^TSearchRec; rc : LongInt; fname: String; Begin rc:=FindFirst(path, attr, rec); While rc=0 Do Begin fname:=rec.name; //ファイル名 If fname='.' then FindNext(rec); //[.] 除外 If pos('.',fname)=0 then fname:=fname+'.'; //拡張子なしファイル対策 If (fname=EditFileName(fname,mask)) //maskに該当するか、 | (rec.attr=faDirectory) Then //ディレクトリの場合は追加 Begin new(prec); prec^:=rec; List.Add(prec); End; rc:=FindNext(rec); End; End