サーチ縁尋開発記

「サーチ縁尋」は、特定の文字列を含むファイルを探す検索ソフトです。ここでは、このサーチ縁尋のプログラム上のポイントを開発記としてまとめてみましたので、ソースを読む時の参考にしてください。

「サーチ縁尋」では、検索対象のディレクトリと検索キー、ファイルサイズ制限を指定して検索を開始します。検索処理自体は、Delphiの文字列検索関数をそのまま使ってStringListに格納したキーでファイルを検索すれば良いのですぐに出来たのですが、ファイルの探索やディレクトリ指定のGUI(ダイアログ)、さらに検索結果のドラッグ&ドロップで結構悩みました。

ファイル探索

 まず、ファイル探索。これは、ディレクトリ内のファイルを次々に探索して見つかったファイルに検索をかける処理です。と書くと簡単そうですし、実際「ファイルだけの」探索なら単にFindFirst、FindNextでヒットしたファイルを検索関数に渡すだけなので、特に難しい点はありません。問題なのは、ファイルの探索ではサブディレクトリも探す必要がある事、しかもファイル名を*.*にして探索をかけるとなぜかFindFirst、FindNextが「指定したディレクトリ自身」さらには「親ディレクトリ」まで見つけてくれる事です。特に、2番目のFindFirst、FindNextの振る舞いには困りました。見つかったディレクトリで再帰呼び出しをかける場合、自分自身やその上位ディレクトリが含まれていると無限再帰に陥ってしまうからです。開発中もどうやら自分自身と親ディレクトリが「.」「..」で返される事を確認するまでに、何度か無限再帰に陥り強制終了させました。

 ファイル探索の関数では、まず見つかったファイルが自分自身、あるいは親ディレクトリか確認し、もしそうなら無視します。次に、ディレクトリかどうかをTSearchRecAttrプロパティで検査。ディレクトリでサブディレクトリ検索フラグsubDirが立っていれば、そのサブディレクト内を再帰的に探索するためにディレクトリを引数にして自分自身(探索関数)を再帰呼び出しします。以上の検査に引っかからなかったものは、ファイルと判断して検索にかけるようにしました。検索処理は、ファイルパスを引数に検索関数を呼び出す形で行います。

 ファイルの探索が終わったら、ProcessMessageでアプリケーションに送られたメッセージを処理します。今回は、検索処理用のスレッドは用意しなかったのですが、これにより検索中でもアプリケーションを操作する事が出来るようになりました(ただし、大きなファイルの検索中はレスポンスが遅れます)。
 以下のリストが実際のファイル探索関数です。引数にディレクトリを指定すると、その中のファイルを検索関数に渡し、サブディレクトリは自分自身に再帰的に渡します。

  procedure TForm1.searchFile(sDir:String); // ディレクトリ内のファイル取得
  var
    sRec:TSearchRec;
    lItem:TListItem;
  begin

    if not(IsPathDelimiter(sDir,Length(sDir))) then // 末尾に¥を付加
      sDir:=sDir+'\';

    if not(FindFirst(sDir+csFn,faAnyFile,sRec)=0) then
    begin

      FindClose(sRec);

      Exit;

    end;

    if (sRec.Attr=faDirectory) and (sRec.name<>'.') and (sRec.name<>'..')
      and (subDir) then
      searchFile(sDir+sRec.Name) // サブディレクトリ内ファイルを再帰的に取得
    else if (sRec.name<>'.') and (sRec.name<>'..') and (sRec.Size<csSize) and
      (sRec.Attr<>faDirectory) then
    begin

      if searchStr(sDir+sRec.Name) then // ファイル内に文字列発見
      begin

        lItem:=ListView.Items.Add; // リストビューにファイルを追加
        lItem.Caption:=sRec.Name;
        lItem.subItems.add(sDir+sRec.Name);
        ListView.Repaint;

        csHits:=csHits+1; // ヒット数更新

      end;

    end;

    while FindNext(sRec)=0 do // ディレクトリ内の全ファイルを検査
    begin

      if (sRec.Attr=faDirectory) and (sRec.name<>'.') and (sRec.name<>'..')
        and (subDir) then
        searchFile(sDir+sRec.Name) // サブディレクトリ内ファイルを再帰的に取得
      else if (sRec.name<>'.') and (sRec.name<>'..') and (sRec.Size<csSize) and
        (sRec.Attr<>faDirectory) then
      begin

        if searchStr(sDir+sRec.Name) then
        begin

          lItem:=ListView.Items.Add;
          lItem.Caption:=sRec.Name;
          lItem.subItems.add(sDir+sRec.Name);
          ListView.Repaint;

          csHits:=csHits+1;

        end;

      end;

      Application.ProcessMessages; // メッセージ処理

      if cbBreak then // 中止ボタンがクリックされたら処理を中止
        Break;

    end;

    FindClose(sRec);

  end;

 この関数は、サブディレクトリに対して自分自身を呼び出す再帰関数なので、一度の関数呼び出しで指定したディレクトリ内とそのサブディレクトリにある全ファイルに対して処理を実行します。本ソフトでは、キーやディレクトリを設定した後で検索対象ディレクトリを引数にこの関数を呼び出し、関数の終了をもって検索終了としました。

ドラッグ&ドロップ

 一般的なドラッグ&ドロップ受け入れ処理は、アプリケーションに送られてくるWM_DROPFILESメッセージとlParamHDROPで行います。この処理に関しては、ここでも紹介していますが、WM_DROPFILESメッセージでドロップを検知し、lParamHDROPからファイル名を取得する、という処理です。逆に言えば、アプリケーションにWM_DROPFILESメッセージとファイル名を入れたHDROPを渡せばそのファイルをドロップ出来るわけですね。
 幸いDELPHIのListViewコンポーネントでは、コンポーネント内のアイテムのドラッグ&ドロップを検出する機能が用意されているので、ドロップの検出に関しては何もする必要がありません。ドロップの処理は、EndDropイベントハンドラにそのまま記述できます。

 今回悩んだのは、HDROPの作成です。ネットワーク上で調べまくった結果、メモリブロックのハンドルを渡せば良いらしい事とHDROPの形式らしきものを発見(後にVC++ヘルプのCF_HDROPでも同様の型定義を見つけたので、多分定義自体はこれで良いはず)したのですが、DelphiでやるとHDROPの形式がおかしいのか、上手く行きません。
 散々悩んだ末に、「ドット絵でぃた」のドロップ処理部分にドロップファイル名を出力する処理を入れて試してみると、ファイル名先頭の3バイトが飛んでいました。で、オフセットに本来の数値より3少ない数値を入れると解決(したのか?)。1バイトなら終端のヌル文字の関係かな、という気もしますけど、3バイトですからねえ。原因が良くわからないのでちょっと不安ですが、とりあえずファイル名を渡せるようになったので良しとしますか。

 ところがこのドラッグ&ドロップ、しばらく試すうちに「InternetExplorerにドロップできない」事に気づきました。さらに調べてみると、IEにドロップするにはOLEを使ってドラッグ&ドロップ処理を行う必要があるとか。しかも、その処理はクリップボードを使うなどかなり面倒なようです。資料に関しても、OLEのドラッグ&ドロップはMFC/VBの機能を利用するものが中心でAPIレベルの処理内容を具体的に示す資料が見つかりません。こうなったら、「サーチ縁尋」をネットワークに対応させて...とかやっている暇はないので、今回のドラッグ&ドロップ機能はエディタなどに対してしか使えないものになってしまいました。

 それから、今回のプログラムではWM_DROPFILESHDROPを送る時に相手がWM_DROPFILESを処理するか調べず無条件に送りつけています(^^;。ドロップ出来ない(WM_DROPFILESを処理しない)アプリケーションにドロップすると本来ドロップされたアプリケーションで解放すべきHDROPのメモリが解放されないかもしれないので、ちょっとまずいんですけどね。

ディレクトリ選択ダイアログ

 ディレクトリを選ぶダイアログは、意外に楽でした。さすがにOpenDialogはないよなあ、と思って調べてみるとSHBrowseForFolderというAPIでディレクトリ選択のダイアログを使えるようです。ネットワークで検索すると、大量のサンプルも見つかりました。ポインタの設定でやや悩んだものの、最終的には無事ディレクトリを返す関数を作成でき、「参照」ボタンクリックのイベントハンドラから呼び出しています。

その他のポイント

 検索処理は、searchStr関数の中でファイルをバッファ(pFile)に読み込み、Delphiの文字列検索関数AnsiPosで検索キー用文字列リスト(csKey)を参照しながら検索しています。大文字小文字を区別しない(Capitalフラグが偽)の時は、検索キー・ファイル内文字をAnsiUpperCase()ですべて大文字に変換してから検索するようにしました。検索関数を自分で定義したり他のライブラリの関数を使う時は、AnsiPosの関数名を独自の関数名に変えてください。

  i:=0;

  // ファイル内をキー文字列リストで検索
  while (i<csKey.Count) and (AnsiPos(csKey.Strings[i],String(pFile))>0) do
      i:=i+1;

  if i=csKey.Count then // キー文字列がすべてあれば真
    res:=True
  else
    res:=False;

 リストのファイルがダブルクリックされたら、ダブルクリックされたファイルをShellExecuteで開く処理を行います。

 結果をファイルに書き出す処理は、リストの「パス」項目を指定されたファイルに書き出します。

  if ListView.Items.Count=0 then
  begin

    ShowMessage('ヒットしたファイルがありません');
    Exit;

  end;

  if not(SaveDialog1.Execute) then
    Exit;

  for i:=0 to ListView.Items.Count-1 do
  begin

    str:=str+ListView.Items.item[i].subItems[0]+#13#10;

  end;

  AssignFile(f, SaveDialog1.FileName);
  Rewrite(f);
  Write(f,str);
  CloseFile(f);

今後の改良など

 まず、IEにもドロップ出来るようにしたいですね。それから、検索関数も自前で実装しておきたいところ。アルゴリズムの勉強というだけでなく、条件を指定した検索が出来るようになりますからね。

「サーチ縁尋」ダウンロード
戻る