LazarusCompleteGuide:8.3

From 흡혈양파의 번역工房
Jump to navigation Jump to search

디렉터리에서 검색하기

표준 라자루스 파일 대화창은 사용자가 작업할 파일을 하나 또는 이상을 선택하도록 할 때 이상적이다. 하지만 사용자가 많은 파일 중 하나를 선택해야 하거나, 디렉터리 트리에서 하위디렉터리와 파일까지 검색하기엔 대화창이 너무 작다. 따라서 작업 디렉터리를 먼저 선택할 필요가 있다 (예: TSelectDirectoryDialog). 이후 선택된 디렉터리부터 원하는 파일이나 디렉터리에 대한 자동 검색이 시작되고, 일치하는 엔트리가 발견되는 임시 위치, 즉 문자열리스트나 커스텀 레코드에 보관된다. 파스칼은 그러한 파일 검색 연산을 처리하도록 FindFirst/FindNext/FindClose 프로시저와 함수를 제공한다. 검색 액션은 FindFirst 의 호출부터 시작하고 (하나 이상의 파일이나 디렉터리를 찾아야 하는 경우), FindNext 의 반복 호출이 계속된다. 각 검색 연산은 FindClose로 끝이 나야 한다. 이러한 함수들은 델파이에서도 정확히 동일한 방식으로 사용된다. 경로 구분자에 올바른 상수를 제공하는 것이 중요하다. 재귀적 디렉터리 검색은 모든 운영체제에 중첩될 수 있다 (OS/2 제외). 그렇지만 OS/2를 위한 라자루스는 없으므로 이 문제를 염려할 필요는 없다. 앞서 언급한 루틴은 fileutilh.inc 파일에서 찾을 수 있으며, 아래와 같이 정의된다:

function FindFirst(const Path: String; Attr: LongInt; out Rslt: TSearchRec): Longint;
function FindNext(var Rslt: TSearchRec): Longint;
procedure FindClose(var F: TSearchRec);


TSearchRec 레코드는 동일한 파일에 정의되지만 선언은 운영체제에 따라 좌우된다:

type
  TSearchRec = record
    Time: Longint;
    Size: Int64;
    Attr: Longint;
    Name: TFileName;
    ExcludeAttr: Longint;
{$ifdef unix}
    FindHandle: Pointer; Mode: TMode;
    PathOnly: AnsiString {$ifndef VER2_2} Deprecated {$endif};
{$else unix}
    FindHandle: THandle;
{$endif unix}
{$if defined(Win32) or defined(WinCE) or defined(Win64)}
    FindData: TWin32FindData;
{$endif}
{$ifdef netware_clib}
    FindData: TNetwareFindData;
{$endif}
{$ifdef netware_libc}
    FindData: TNetwareLibcFindData;
{$endif}
{$ifdef MacOS}
    FindData: TMacOSFindData;
{$endif}
  end;


FindFirst에서 우리는 찾고자 하는 항목의 파일 속성을 Attr 에 명시할 필요가 있다. 이에 이용 가능한 상수도 동일한 파일에 정의된다. 이는 검색 연산에 사용되어야 한다:

const
{ File attributes }
  faReadOnly  = $00000001;
  faHidden    = $00000002;
  faSysFile   = $00000004;
  faVolumeId  = $00000008;
  faDirectory = $00000010;
  faArchive   = $00000020;
  faSymLink   = $00000040;
  faAnyFile   = $0000003f;


검색 결과를 이용해 원하는 것이 무엇이냐에 따라 TStringList 혹은 메모 창, 아니면 크기, 속성, 마지막 접근일자 등의 필드가 포함되어 특별하게 생성된 레코드 중 무엇을 원하는지 명시해야 한다.


아래 예제 프로그램은 디렉터리를 검색하는 방법을 설명한다. 이는 모든 디렉터리 엔트리를 메모에 표시한다:

procedure TForm1.button1Click(Sender: TObject);
  var  sr: TSearchRec;
        s: String;
begin
  if SelectDirectoryDialog1.Execute then       //set source directory
  begin
    s := UTF8ToSys(SelectDirectoryDialog1.FileName); // normalise file name
    Memo1.Lines.Add('Contents of ' + SelectDirectoryDialog1.FileName + ':');
    if FindFirst(s + '*,*', faAnyFile, sr) = 0 then  // what is there?
    begin
      Memo1.Lines.Add(sr.Name);      // ... add more
      while FindNext(sr) = 0 do Memo1.Lines.Add(sr.Name);
    end;
    FindClose(sr);
  end;
end;


이 예제는 매우 단순하게 유지하였다. 프로그램은 파일과 디렉터리를 구별하지 않으며, 하위 디렉터리로 드릴다운(drill down)하지 않는다. 전체 디렉터리 구조를 검색하려면 디렉터리를 찾기 위한 외부 루프(outer loop)와 디렉터리에 포함된 파일을 찾기 위한 내부 루프(inner loop)를 생성해야 한다. 이런 경우, FindFirst/FindNext 의 두 개의 블록을 이용하는데, 각 블록은 각각 FileClose 로의 호출을 이용해 닫혀야 한다.


다음 예제는 조금 더 복잡해진 재귀적 디렉터리 검색을 보여준다:

unit UnitMain;
{$apptype gui}
{$mode objfpc}{$H+}

interface

uses Classes, SysUtils, FileUtil, LResources, Forms, Controls,
     Graphics, Dialogs,StdCtrls;
type { TForm1 }
  TForm1 = class(TForm)
    Button1: TButton;              // start button
    Memo1 : TMemo;                 // display results in a memo
    SelectDirectoryDialog1 : TSelectDirectoryDialog; // to obtain starting folder
    procedure Button1Click(Sender: TObject);// event that initiates the search
  private
    DirectoryList: TStringList;    // holds the search results
    DosError: Integer;             // helper variable without which the result
                                   // is harder to read
    procedure Scan(const dir, mask: String); // procedure to do the work of searching
  end;

var  Form1: Tform1;

implementation
{$R *.lfm}

procedure TForm1.Scan(const dir, mask: String);
(* The actual search routine, with only a single string list for folders and files. This can be changed in the procedure's
   first block by adding a different folder list variable (to be defined).                                                 *)

  var  sr: TSearchRec; // a record needed both by FindFirst and FindNext
       s : String;     // helper variable
begin
// To construct the stringlist properly we must ensure that the directory always has a Directory Separator at the end
  s = Dir;                                              // (whichever: / or \)
  if s[Length(s)] <> DirectorySeparator then
    s := s + DirectorySeparator;         // only if no Windows drive letter
  if sr.Name <> '' then                  // if the first sr.Name is not empty
    if sr.Name[Length(sr.Name)] = DirectorySeparator then
      s := s + sr.Name                   // proper string construction
    else
      s := s + DirectorySeparator + sr.Name;
  Directorylist.Add(s);                  // copy from the helper variable into the list

// After the initial directory is entered in the string list, then all the files in the directory follow. The VolumeID
// (drive letters of DOS / Windows and its subdirectories) are ignored here Only file entries are read:

  DosError := FindFirst(Dir + DirectorySeparator + mask,
              faAnyfile mod faDirectory mod faVolumeID, sr);
                  // mod with the two specified attributes masked

  while DOSError = 0 do
  begin
    if Dir[Length(Dir)] = DirectorySeparator then
      Directorylist.Add(Dir + sr.Name)
    else
      Directorylist.Add(Dir + DirectorySeparator + sr.Name);
    DosError := FindNext(sr); // iterate through the entire list
  end; { while }

  try
    SysUtils.FindClose(sr);   // at the end of the search close everything
  Except                      // each FindFirst requires a FindClose
  end; { try }

// If the files are processed in a directory, in the next Subdirectory level the directories "." (current directory) and
// ".." (parent directory) must be ignored.

  DosError := FindFirst(Dir+DirectorySeparator+'*.*',faDirectory,sr);

  while DosError = 0 do
  begin
    if (sr.Attr and faDirectory = faDirectory) then    // directory found!
    begin
      if (sr.Name <> '.') and (sr.Name <> '..') then   // ignore pseudo-entries
      begin
        if Dir[Length(Dir)] = DirectorySeparator then
          // The current routine is called recursively. First, for direct drive specification, for example C:\)
          Scan(Dir + sr.Name, Mask) // recursive search in the directory
        else
          // search all subdirectory paths
          Scan(Dir + DirectorySeparator + sr.Name, Mask); // recursive search
      end; { if }
    end; { if }
    DosError := FindNext(sr);
  end; { while }

  try
    SysUtils.FindClose(sr);
  Except
  end; { try }
end;

procedure TForm1.Button1Click(Sender: TObject);
// Initiate the processing in the OnClick event of the button
  Var  i: Integer;
begin
  if SelectDirectoryDialog1.Execute then
  begin
// The string list is created outside of the working procedure and needs to be released again at the end. The working procedure
// always refers to the global DirectoryList in the window class created and referenced here:

    DirectoryList := TStringList.Create;        // create list

// The actual work procedure. TSelectDirectoryDialog provides a path with an appended path separator so the
// search string is attached directly here. In the example we search for *.*, although any kind of mask can be specified.
// Searching only for files and not for the names of subdirectories:

    Scan(UTF8ToSys(SelectDirectoryDialog1.FileName), '*.*');// working...

// The file names are collected by the end of this routine. The disadvantage is that during the processing nothing is displayed,
// the search is dramatically faster than if a window is constantly refreshed. A Message "Searching ..." would reduce the
// user's anxiety

    for i := 1 to DirectoryList.Count do
      Memo1.Lines.Add(SysToUTF8(DirectoryList[i-1]));

// ... any other processing is also possible here. It is important that the file names displayed in the memo are
// normalized so that special characters are displayed properly.

    DirectoryList.Free;    // the list is released at the end
    end;
end;

initialization
{$I UnitMain.lrs}
end.


이 프로그램은 그다지 복잡하지 않은 편이며 주석문(comment)를 빼면 그다지 길지도 않다. 하지만 프로그램에 검색 함수의 훌륭한 기반을 형성하며, 쉽게 확장이 가능하다. 검색 결과를 격자 또는 트리뷰로 표시할 수도 있다. 여기서 디스플레이에 사용된 메모는 간단한 예제에 불과하다. Scan () 과 같은 캡슐화된 프로시저의 단점은 큰 디렉터리 구조를 작업하는 동안 별도의 진행 표시가 없다는 점이다. 따라서 스캐닝 프로시저가 시작될 때 사용자에게 피드백을 어느 정도 제공할 수 있겠다:

StatusBar1.SimpleText := 'Searching ' + s;
Update;     // force window to update


이는 사용자가 프로그램이 여전히 활성화되었는지 볼 수 있도록 해준다. 검색이 진행되는 동안 메모 필드에 문자열을 쓸 경우 프로그램의 속도가 많이 느려지므로 좋은 생각이 아니다.

StatusBar1.SimpleText := '';

위의 코드를 이용해 상태 표시줄에 검색 활동에 관하여 사용자에게 알려주고 검색 액션을 실행한 루틴에서 제거하는 편이 낫다.


프리 파스칼에는 (따라서 라자루스 또한) 파일을 처리하기 위한 함수들이 훨씬 더 많이 포함되어 있다.


본문에서는 가장 유용한 함수 일부만 소개한다. 실행되는 함수는 이름부터 명확해야 하며, 본문에 열거하는 이유는 유용하기 때문이다. 이 함수들은 모두 SysUtils 유닛에서 찾을 수 있다:

function ChangeFileExt(const FileName: String; const Extension: String): String;
function CreateDir(const NewDir: String): Boolean;
function DeleteFile(const FileName: String): Boolean;
function DirctoryExists(const Directory: String): Boolean;
function ExeSearch(const Name, DirList: String) : String;
function ExtractFileDir(const FileName: String): String;
function ExtractFileDrive(const FileName: String): String;
function ExtractFileExt(const FileName: String): String;
function ExtractFileName(const FileName: String): String;
function FileExists(const FileName: String): Boolean;
function FileSearch(const Name, DirList: String;
                                  ImplicitCurrentDir: Boolean = True): String;
function FileIsReadOnly(const FileName: String): Boolean;
function RenameFile(const OldName, NewName: String): Boolean;


아래 두 가지 루틴은 파일의 속성을 검색하거나 설정하는 데 사용할 수 있다. 속성은 파일 속성 상수 집합체(collection)으로부터의 ORed 조합이다. 가능한 상수로는 faReadOnly (파일 읽기만 가능), faHidden (파일이 숨겨짐; Unix에서 숨김 파일의 이름은 점으로 시작), faSysFile (파일이 시스템 파일임; Unix에서는 텍스트, 블록 또는 FIFO 파일이 가능), faVolumeld (볼륨 라벨, Windows 특정적 상수), faDirectory (파일이 디렉터리임), faArchive (파일이 아카이브임, Windows 특정적)을 들 수 있다.

function FileGetAttr(const FileName: String): Longint;
function FileSetAttr(const Filename: String; Attr: Longint): Longint;


아래 루틴은 파일의 일자를 읽고 쓴다:

function FileGetDate(Handle: THandle): Longint; // you must assign a file handle
function FileSetDate(Handle: THandle; Age: Longint): Longint;
function FileSetDate(const FileName: String; Age: Longint): Longint; // no file handle here


그리고 아래 함수는 파일의 마지막 접근 일자를 읽는다:

function FileAge(const FileName: String): Longint;


파일일자는 특별한 포맷으로 검색된다. 하지만 이 포맷을 표준 파스칼 TDataTime 포맷으로 변환하는 루틴들이 있다:

function FileDateToDateTime(Filedate: Longint): TDateTime;
function DateTimeToFileDate(DateTime: TDateTime): Longint;