ファイルのソート
ファイルとフォルダをTStringListに加えてソートさせると例えばファイルとサブフォルダが混在して並んでしまうことがあります。
それをファイルの後にサブフォルダがまとまって並ぶようにしたいと。
参考サイト
CustomSort
TStringListのCustomSortメソッドを使います。
CustomSortというのはつまるところ、
function FuncDescendingOrderSort(slList: TStringList; iIndex1, iIndex2: Integer):
Integer;
begin
//降順にソート
Result := CompareText(slList[iIndex2], slList[iIndex1]);
end;
というように比較の結果を数字の大小で返す関数を作りなさいと。
そしてそれを
var
lsl_List: TStrings;
begin
lsl_List := TStringList.Create;
try
...
//lsl_Listは降順にソートされる
lsl_List.CustomSort(FuncDescendingOrderSort);
...
finally
lsl_List.Free;
end;
end;
のようにして使いなさいと。
そうすると FuncDescendingOrderSort の戻り値を使って並べ替えを行ってくれると。
リストの並び順の大小を返す関数を作りさえすればソートそのものはTStringListがやってくれると。
そういうことですね。
さてここからが本題です。
ファイルリストを取得してTStringListに保持してSortメソッドを呼びました。
ファイルとサブフォルダ中のファイルは分けてソートされるであろうと期待していました。
でも結果はファイルのリストの途中にサブフォルダ中のファイルが並んでソートされてしまうケースがありました。
上がFindFirstFileで取得したもの。
下がそれをSortしたもの。
例えば上の例のようにファイル名よりもサブフォルダのフォルダ名の方がASCII順で前の位置にある場合などです。
半角アルファベットのファイル名と日本語のファイル名があって半角アルファベットのサブフォルダがある場合なども日本語のファイル名の前に半角アルファベットのサブフォルダのファイルがきてしまいます。
まぁ考えてみれば当然なんですが。
StringListのソートはあくまでも文字列同士を比較して並べ替えているわけでファイルなのかフォルダなのかなんてことまではいちいち面倒見てくれません。
ならば自分で面倒みるしかないということでCustomSortの出番となります。
お手軽版
function FuncFileSort(List: TStringList; iIndex1, iIndex2: Integer): Integer;
begin
//まずフォルダーだけで比較
Result := CompareText(ExtractFilePath(List[iIndex1]), ExtractFilePath(slList[iIndex2]));
if (Result = 0) then begin
//フォルダーが同じならファイル名で比較
Result := CompareText(ExtractFileName(List[iIndex1]), ExtractFileName(slList[iIndex2]));
end;
Application.ProcessMessages;
end;
ファイル名のソートなのでCaseSensitiveの値をみてCompareTextとCompareStrの使い分けをする必要はありません。
CompareTextだけでOKです。
この関数をCustomSortに与えてあげればフォルダとファイルとをきちんと分けてソートしてくれるようになります。
//lsl_Listはファイルとフォルダを分けてソートされる
lsl_List.CustomSort(FuncFileSort);
エクスプローラに似せる
たいていの場合は上記のやり方で充分だろうと思います。
ただしこの関数でソートされたリストの結果はエクスプローラの並びとは違っています。
エクスプローラでは数値文字列を数値として扱っています。
file_1.txt
file_2.txt
file_10.txt
という三つのファイルがあると、単純にソートした場合
file_1.txt
file_10.txt
file_2.txt
となります。単純にソートする場合、1や2や10は数値としての大小ではなくあくまで文字列として比較されるのでこうなります。
けれどもエクスプローラでは
file_1.txt
file_2.txt
file_10.txt
という並びになります。
これは1や2や10を文字列ではなく数値として扱いその大小で比較しているためと思われます。
ということでその動作を真似てみました。
procedure gpcFileDirNameDiv(sFile:
WideString;
out sPath, sName:
WideString);
//sPathにパスをsNameにファイル名をセットする。
//パスの末尾の \ は返らない。
var
i, li_Pos, li_Start: Integer;
begin
li_Pos := -1;
li_Start := 0;
for i := Length(sFile)
downto 1
do begin
if (sFile[i] = '\')
or (sFile[i] = '/')
then begin
li_Pos := i - 1;
li_Start := i + 1;
Break;
end else if (sFile[i] = ':')
then begin
li_Pos := i;
li_Start := i + 1;
Break;
end;
end;
sPath := Copy(sFile, 1, li_Pos);
sName := Copy(sFile, li_Start, Length(sFile));
end;
function gfniCompareFileList(sStr1, sStr2:
WideString): Integer;
procedure lpc_StrNumDiv(sSrc:
WideString;
out sHeadStr, sNum, sEndStr:
WideString);
//sSrcを文字列と数値に分解する
var
i, k, li_Len: Integer;
lb_Num: Boolean;
begin
sHeadStr := sSrc;
sNum := '';
sEndStr := '';
lb_Num := False;
li_Len := Length(sSrc);
for i := 1
to li_Len
do begin
if (sSrc[i]
in [WideChar('0')..WideChar('9')])
then begin
sHeadStr := Copy(sSrc, 1, i -1);
for k := i+1
to li_Len
do begin
if not(sSrc[k]
in [WideChar('0')..WideChar('9')])
then begin
sNum := Copy(sSrc, i, k - i);
sEndStr := Copy(sSrc, k, MAXINT);
lb_Num := True;
//最後まで数値文字の場合この中に入ってこないことへの対策
Break;
end;
end;
if not(lb_Num)
then begin
sNum := Copy(sSrc, i, MAXINT);
end;
Break;
end;
end;
end;
function lfni_CompareFile(sStr1, sStr2:
WideString): Integer;
var
ls_Head1, ls_Num1, ls_End1,
ls_Head2, ls_Num2, ls_End2:
WideString;
li_Re: Integer;
li_Num1, li_Num2: Int64;
begin
if (sStr1 = '')
and (sStr2 = '')
then begin
Result := 0;
end else if (sStr2 = '')
then begin
Result := 1;
end else if (sStr1 = '')
then begin
Result := -1;
end else begin
Result := WideCompareText(sStr1, sStr2);
if (Result = 0)
then begin
//sStr1とsStr2は同じ並び順
if (WideUpperCase(sStr1) <> WideUpperCase(sStr2))
then begin
//比較した場合は同じだが文字列としては同じではない場合への対処。
//「が」とUnicodeの「か゛」の合成文字など。
if (Length(sStr1) > Length(sStr2))
then begin
//「が」より「か゛」の合成文字を前に持っていくため(簡単に済ませている)
Result := -1;
end else begin
Result := 1;
end;
end;
end else begin
//sStr1とsStr2は違う並び順
//数値の比較でエラーになった時ように退避しておく
li_Re := Result;
//文字列と数値文字列に分解'abc_12-A'->'abc_', '12', '-A'
lpc_StrNumDiv(sStr1, ls_Head1, ls_Num1, ls_End1);
lpc_StrNumDiv(sStr2, ls_Head2, ls_Num2, ls_End2);
//数値文字列以前の文字列で比較
if ((ls_Head1 = '')
and (ls_Head2 <> ''))
or ((ls_Head2 = '')
and (ls_Head1 <> ''))
then begin
//どちらかが空文字だった。
//'[.txt'と'1.txt'だと'[.txt'の方が先に並べられることへの対応。
end else begin
Result := gfniStringCompareText(ls_Head1, ls_Head2);
if (Result = 0)
then begin
//数値文字列以前は同じだった。
if (WideUpperCase(ls_Head1) <> (WideUpperCase(ls_Head2)))
then begin
//比較した場合は同じだが文字列としては同じではない場合への対処。
//「が」とUnicodeの「か゛」の合成文字など。
if (Length(ls_Head1) > Length(ls_Head2))
then begin
//「が」より「か゛」の合成文字を前に持っていくため(簡単に済ませている)
Result := 1;
end else begin
Result := -1;
end;
end else begin
//数値以前の文字列が同じだったので数値文字列で比較
if (ls_Num1 = '')
and (ls_Num2 = '')
then begin
Result := 0;
end else if (ls_Num2 = '')
then begin
Result := 1;
end else if (ls_Num1 = '')
then begin
Result := -1;
end else begin
try
li_Num1 := StrToInt64(ls_Num1);
li_Num2 := StrToInt64(ls_Num2);
if (li_Num1 = li_Num2)
then begin
Result := 0;
end else if (li_Num1 > li_Num2)
then begin
Result
:= -1;
end else if (li_Num1 < li_Num2)
then begin
Result
:= 1;
end;
if (Result = 0)
then begin
Result := lfni_CompareFile(ls_End1, ls_End2);
end;
except
//エラーが起きたら数値の比較は無効にしてしまう
Result :=
li_Re;
end;
end;
end;
end;
end;
end;
end;
end;
var
ls_Path1, ls_Name1,
ls_Path2, ls_Name2:
WideString;
begin
if (sStr1 = sStr2)
then begin
Result := 0;
Exit;
end;
gpcFileDirNameDiv(sStr1, ls_Path1, ls_Name1);
gpcFileDirNameDiv(sStr2, ls_Path2, ls_Name2);
//まずフォルダーだけで比較
Result := lfni_CompareFile(ls_Path1, ls_Path2);
if (Result = 0)
then begin
//フォルダーが同じならファイル名で比較
Result := lfni_CompareFile(ls_Name1, ls_Name2);
end;
end;
function FuncFileSortEx(List: TStringList; iIndex1, iIndex2: Integer): Integer;
//CustomSort用下請け関数
begin
Result := gfniCompareFileList(List[iIndex1], List[iIndex2]);
end;
var
lsl_List: TStrings;
begin
lsl_List := TStringList.Create;
try
...
//lsl_Listはファイル名順にソートされる
lsl_List.CustomSort(FuncFileSortEx);
...
finally
lsl_List.Free;
end;
end;
これでファイル名に連番をつけている場合も期待した順番どおりソートされるようになります。
このほかにもエクスプローラの並びと合わせるためには全角の記号の扱いを調整する必要があるようです。
そこまでやろうとすると大変なのでパスということで。
2009-02-12:「が」より「か゛」の合成文字を前に持っていくように修正。
2009-01-21: