TClientDataSet And Deleting Files In A Collection - delphi

I have an experimental app that I am developing that adds image filenames to a collection. I am attempting to find the most efficient way to delete all the files in a collection except
for files that exist in another collection. Files can exist in any collection.
I have a TClientDataSet with the following fields:
ClientDataSet1.FieldDefs.Add('Index', ftInteger);
ClientDataSet1.FieldDefs.Add('Collection', ftString, 50);
ClientDataSet1.FieldDefs.Add('Filename', ftString, 254);
I came up with this which seems to work but seems inefficient:
var
i: Integer;
j: Integer;
iCollectionToDelete: string;
iCollection: string;
iFilename: string;
iFilenameInOtherCollection: string;
iFilesInOtherCollectionsStringList: TStringList;
begin
iCollectionToDelete := ListBox1.Items[ListBox1.ItemIndex];
{ Set filtered to false to see all the records in the database }
ClientDataSet1.Filtered := False;
{ Create a list of files not in the collection to be deleted }
iFilesInOtherCollectionsStringList := TStringList.Create;
try
for i := 0 to ClientDataSet1.RecordCount - 1 do
begin
iCollection := ClientDataSet1.FieldByName('Collection').AsString;
iFilename := ClientDataSet1.FieldByName('Filename').AsString;
if iCollection <> iCollectionToDelete then
begin
iFilenameInOtherCollection := ClientDataSet1.FieldByName('Filename').AsString;
iFilesInOtherCollectionsStringList.Add(iFilename);
end;
ClientDataSet1.Next;
end;
{ Compare the iFilenameInOtherCollection with all the filenames in the
dataset and if the iFilename is not in the other collection then
erase the file }
ClientDataSet1.First;
for i := 0 to ClientDataSet1.RecordCount - 1 do
begin
iFilename := ClientDataSet1.FieldByName('Filename').AsString;
ClientDataSet1.Next;
for j := 0 to iFilesInOtherCollectionsStringList.Count - 1 do
begin
iFilenameInOtherCollection := iFilesInOtherCollectionsStringList[j];
if iFilesInOtherCollectionsStringList.IndexOf(iFilename) = -1 then
if FileExists(iFilename) then
WindowsErase(handle, iFilename, False, False, False, False);
end;
end;
finally
iFilesInOtherCollectionsStringList.Free;
end;
end;
My question is can this be made more efficient or is there a way to do the same thing
using just TClientDataset methods?

Just for amusement, I thought I'd try doing this without using Stringlists at all, and instead use a couple of features of ClientDataSets, namely Filtering and the ability to copy data from one CDS to another in a single statement. It's quite a bit shorter than using stringlists and probably easier to maintain/modify/refactor as a result.
I've not benchmarked it against the Stringlist version but would be surprised if it were any faster,
because it depends on TClientDataSet.Locate, which is not a particularly efficient operation even when working against an indexed field.
The code is below. Hopefully the comments will explain how it works.
procedure TForm1.SetUp;
begin
ClientDataSet1.FieldDefs.Add('Index', ftInteger);
ClientDataSet1.FieldDefs.Add('Collection', ftString, 50);
ClientDataSet1.FieldDefs.Add('Filename', ftString, 254);
ClientDataSet1.CreateDataSet;
// Create some test data
ClientDataSet1.InsertRecord([1, 'C1', 'F1']);
ClientDataSet1.InsertRecord([2, 'C2', 'F1']);
ClientDataSet1.InsertRecord([3, 'C3', 'F1']);
ClientDataSet1.InsertRecord([4, 'C1', 'F2']);
ClientDataSet1.InsertRecord([5, 'C3', 'F3']);
end;
procedure Tform1.ApplyCDSFilter(CDS : TClientDataSet; FilterExpr : String);
// utility routine to filter/unfilter a dataset
begin
CDS.Filtered := False;
CDS.Filter := FilterExpr;
if FilterExpr <> '' then
CDS.Filtered := True;
end;
procedure TForm1.RemoveFilesOnlyInCollection(CollectionName : String);
var
CDS : TClientDataSet;
FilterExpr : String;
AFileName : String;
begin
// In the following, I'm just going to add the names of the files which belong to the
// specified collection as well as to another one
// to a listbox so as to be able to check the results by inspection
Listbox1.Items.Clear;
// next create a temporary CDS
CDS := TClientDataSet.Create(Nil);
// Index it by Filename
CDS.IndexFieldNames := 'Filename';
// Copy the data from ClientDataSet1 into it
CDS.Data := ClientDataSet1.Data;
// Construct a filter expression to select the collection whose members are to be
// retained. NOTE : the QuotedStr is to handle quotes embedded in the collection name.
FilterExpr := '(Collection =' + QuotedStr(CollectionName) + ')';
// Apply the filter to ClientDataSet1, so that only records that contain the CollectionName
// are "visible", temporarily
ApplyCDSFilter(ClientDataSet1, FilterExpr);
// Next, negate the filter expression and apply it to the temporary CDS
FilterExpr := 'not ' + FilterExpr;
ApplyCDSFilter(CDS, FilterExpr);
// Now, we can loop through ClientDataSet1 and test whether the Filename is present
// in the temporary CDS. If it is, that means that the Filename belongs to another
// collection too.
try
ClientDataSet1.DisableControls;
ClientDataSet1.First;
while not ClientDataSet1.Eof do begin
AFileName := ClientDataSet1.FieldByName('Filename').AsString;
if not CDS.Locate('Filename', AFileName, [loCaseInsensitive]) then
Listbox1.Items.Add(AFileName);
ClientDataSet1.Next;
end;
finally
CDS.Free;
ClientDataSet1.EnableControls;
ApplyCDSFilter(ClientDataSet1, '');
end;
end;

Just add iFilesInOtherCollectionsStringList.Sorted := True after you fill it. IndexOf will then use fast binary search instead of extremely slow one-by-one looping. Probably that will be enough for your purposes.
Another option is to prepare list-to-delete first and then launch a worker thread which will execute removal in the background. This will likely help because usually file operations much more slow than memory comparisons. You may check if it is the deletion that slows down your process by commenting out the WindowsErase line.

Related

Drag and drop from VirtualTreeView to shell (Ole drag and drop)

I am trying to drag and drop from VirtualTreeView to create a file in shell (drag and drop from VirtualTreeView to a folder in File Explorer or desktop folder).
I only found example of doing the opposite (shell to VirtualTreeView), but I cannot find any example for doing that. Help?
Doing any drag-drop operations in Windows involves creating an IDataObject, and giving that object to Windows.
The Virtual Treeview handles a lot of that grunt-work for you, creating an object that implements IDataObject for you. The tree then raises events when you need to help populate it.
When passing "file-like" things through a copy-paste or a drag-drop, you are require to add two clipboard formats to the IDataObject:
CF_FILEDESCRIPTOR, and
CF_FILECONTENTS
In addition to support for formats that the virtualtree itself will add, you can choose to indicate support for more clipboard format.
OnGetUserClipboardFormats Event
This is the event where you are given a chance to add additional clipboard formats to the IDataObject that the tree will be creating:
procedure TForm1.lvAttachmentsGetUserClipboardFormats(Sender: TBaseVirtualTree;
var Formats: TFormatEtcArray);
var
i: Integer;
begin
//Add formats for CF_FILEDESCRIPTOR and CF_FILECONTENTS
i := Length(Formats);
SetLength(Formats, i + 1);
Formats[i].cfFormat := CF_FILEDESCRIPTOR;
Formats[i].ptd := nil;
Formats[i].dwAspect := DVASPECT_CONTENT;
Formats[i].lindex := -1;
Formats[i].tymed := TYMED_HGLOBAL;
i := Length(Formats);
SetLength(Formats, i + 1);
Formats[i].cfFormat := CF_FILECONTENTS;
Formats[i].ptd := nil;
Formats[i].dwAspect := DVASPECT_CONTENT;
Formats[i].lindex := 0;
Formats[i].tymed := TYMED_ISTREAM;
end;
The tree will then given the IDataObject to the shell as part of the drag-drop operation.
Later, an application that the user dropped items onto will enumerate all formats in the IDataObject, e.g.:
CF_HTML ("HTML Format")
CFSTR_FILEDESCRIPTOR ("FileGroupDescriptorW")
CFSTR_FILECONTENTS ("FileContents")
CF_ENHMETAFILE
And it will see that the IDataObject contains FileDescriptor and FileContents.
The receiving application will then ask the IDataObject to actually cough up data. (This "delayed-rendering" is a good thing, it means your source application doesn't actually have to read any content unless it actually gets requested).
OnRenderOleData Event
This is the event where the virtual tree realizes its IDataObject has been asked to render something, and it needs you to finally render that actual content.
The general idea with these two clipboard formats is:
CF_FILEDESCRIPTOR lets you return a record that describes the file-like thing (e.g. filename, file size, created date, last modified date, last accessed date)
CF_FILECONTENTS lets you return an IStream that contains the actual file contents
procedure TForm1.lvAttachmentsRenderOLEData(Sender: TBaseVirtualTree; const FormatEtcIn: tagFORMATETC;
out Medium: tagSTGMEDIUM; ForClipboard: Boolean; var Result: HRESULT);
var
global: HGLOBAL;
stm: IStream;
begin
if FormatEtcIn.cfFormat = CF_FILEDESCRIPTOR then
begin
global := GetAttachmentFileDescriptorsFromListView(lvAttachments, ForClipboard);
if global = 0 then
Exit;
ZeroMemory(#Medium, SizeOf(Medium));
Medium.tymed := TYMED_HGLOBAL;
Medium.hGlobal := global;
Result := S_OK;
end
else if FormatEtcIn.cfFormat = CF_FILECONTENTS then
begin
ZeroMemory(#Medium, SizeOf(Medium));
Medium.tymed := TYMED_ISTREAM;
Result := GetAttachmentStreamFromListView(lvAttachments, ForClipboard, FormatEtcIn.lindex, stm);
if Failed(Result) then
Exit;
Medium.stm := Pointer(stm);
IUnknown(Medium.stm)._AddRef;
Result := S_OK;
end;
end;
The first helper function creates an array of FILE_DESCRIPTOR objects, and copies them to a HGLOBAL allocated memory:
function GetAttachmentFileDescriptorsFromListView(Source: TVirtualStringTree; ForClipboard: Boolean): HGLOBAL;
var
i: Integer;
nCount: Integer;
nodes: TNodeArray;
descriptors: TFileDescriptorDynArray;
data: TAttachment;
begin
Result := 0;
if ForClipboard then
nodes := Source.GetSortedCutCopySet(False)
else
nodes := Source.GetSortedSelection(False);
if Length(nodes) = 0 then
Exit;
nCount := 0;
for i := 0 to Length(nodes) - 1 do
begin
//Get the file thing from this node
data := GetNodeDataFromNode(nodes[i]);
if not Assigned(data) then
Continue;
//Increase the size of our descriptors array by one
Inc(nCount);
SetLength(Descriptors, nCount);
//Fill in the next descriptor
descriptors[nCount-1] := data.ToWindowsFileDescriptor;
end;
Result := FileDescriptorsToHGLOBAL(descriptors);
end;
The second helper copies your file-like thing's binary contents to an IStream:
function GetAttachmentStreamFromListView(Source: TVirtualStringTree; ForClipboard: Boolean; lindex: Integer; var stm: IStream): HResult;
var
nodes: TNodeArray;
data: TAttachment;
begin
Result := E_FAIL;
if ForClipboard then
nodes := Source.GetSortedCutCopySet(False)
else
nodes := Source.GetSortedSelection(False);
if Length(nodes) = 0 then
Exit;
if (lIndex < Low(Nodes)) or (lIndex > High(Nodes)) then
begin
Result := DV_E_LINDEX;
Exit;
end;
//Get the file thing from this node
data := GetNodeDataFromNode(nodes[i]);
if not Assigned(data) then
Continue;
//Fetch the content into a IStream wrapped memory stream
stm := data.GetStream(nil);
Result := S_OK;
end;
Your attachment object, whatever it is has to know:
how to represent itself as a TFileDescriptor
how to return the contents as an IStream

Delete Files With progressbar

I'm trying to make progressbar while deleting files here is my code:
procedure TForm1.Timer1Timer(Sender: TObject);
var
i:Integer;
begin
i:=i+1;
ProgressBar.Max:=DeleteList.Count - i ; //Files = 8192
DeleteFile(GetIniString('List', 'File' + IntToStr(i),'','FileLists.ini'));
ProgressBar.Position:=ProgressBar.Position+1;
end;
Using threads or IFileOperation both involve fairly steep learning curves. Here are a couple of possibilities:
TDirectory method
At Jerry Dodge's prompting I decided to add an example of using TDirectory to
get a list of files and process it in some way, e.g. delete files in the list.
It displays a periodic progress message - see the if i mod 100 = 0 then statement
in the ProcessFiles method. Unfortunately I couldn't find a way to show
a periodic message during the list-building stage because AFAIC TDirectory
doesn't expose the necessary hook to do so.
procedure TForm2.ProcessFileList(FileList : TStringList);
var
i : Integer;
S : String;
begin
for i := 0 to FileList.Count - 1 do begin
// do something with FileList[i], e.g. delete it
S := FileList[i];
DeleteFile(S);
// Display progress
if i mod 100 = 0 then // do something to show progress
Caption := Format('Files processed: %d ', [i]);
// OR, you could use i and FileList.Count to set a trackbar % complete
end;
Caption := Format('Processed: %d files', [FileList.Count]);
end;
procedure TForm2.GetFileList(const Path : String; FileList : TStringList);
var
Files : Types.TStringDynArray;
i : Integer;
begin
Files := TDirectory.GetFiles('C:\Temp');
FileList.BeginUpdate;
try
for i:= 0 to Length(Files) - 1 do
FileList.Add(Files[i]);
finally
FileList.EndUpdate;
end;
end;
procedure TForm2.Button1Click(Sender: TObject);
var
FileList : TStringList;
begin
FileList := TStringList.Create;
try
GetFileList('C:\Temp', FileList);
ProcessFileList(FileList);
Memo1.Lines.Text := FileList.Text;
finally
FileList.Free;
end;
end;
It should be evident that this way of doing it is a lot simpler than using the
traditional, Windows-specific method below, at the expense of loss of some flexibility,
and has the advantage of being cross-platform.
IFileOperation method (Windows-specific)
The Windows API has functionality to retrieve and process a list of files e.g. in a directory and there used to be a trivially-simple-to-use wrapper around this, including a progress animation, in the (antique) v.3 of SysTools library from TurboPower S/Ware, but I'm not sure this wrapper ever made it into the later public domain version. On the face if it, it could also be done using the IFileOperation interface but google has yet to conjure a simple example. Note that an SO answer about this contains the comment "this is a very complex API and you do need to read the documentation carefully".
I attempted to do this myself but soon got out of my depth. Remy Lebeau's answer here to the q I posted when I got stuck shows how to do it, but the TDirectory method above seems vastly easier at my skill level.
Traditional (D7) method (Windows-specific)
In my experience, if you are only looking to process a few hundred thousand files, you should be able to do it, displaying progress as you go, by adding the files to a TStringList and then processing that, with code along the following lines:
procedure GetFileList(const Path : String; Recurse : Boolean; FileList : TStringList);
// Beware that the following code is Windows-specific
var
FileCount : Integer;
procedure GetFilesInner(sPath : String);
var
Path,
AFileName,
Ext: String;
Rec: TSearchRec;
Done: Boolean;
begin
Path := IncludeTrailingBackslash(sPath);
if FindFirst(Path + '*.*', faAnyFile, Rec) = 0 then begin
Done := False;
while not Done do begin
if (Rec.Name <> '.') and (Rec.Name <> '..') then begin
AFileName := Path + Rec.Name;
Ext := LowerCase(ExtractFileExt(AFileName));
if not ((Rec.Attr and faDirectory) = faDirectory) then begin
inc(FileCount);
if FileCount mod 100 = 0 then
//show progress in GUI
;
FileList.Add(AFileName)
end
else begin
if Recurse then
GetFilesInner(AFileName);
end;
end;
Done := FindNext(Rec) <> 0;
end;
FindClose(Rec);
end;
end;
begin
FileCount := 0;
FileList.BeginUpdate;
FileList.Sorted := True;
FileList.Duplicates := dupIgnore; // don't add duplicate filenames to the list
GetFilesInner(Path);
FileList.EndUpdate;
end;
procedure TForm1.Button2Click(Sender: TObject);
var
FileList : TStringList;
FileName : String;
i : Integer;
begin
FileList := TStringList.Create;
try
GetFileList('d:\aaad7', True, FileList);
for i := 0 to FileList.Count - 1 do begin
FileName := FileList[i];
// do something with FileName, e.g. delete the file
if i mod 100 = 0 then
// display progess e.g. by
Caption := IntToStr(i);
end;
Memo1.Lines := FileList;
finally
FileList.Free;
end;
end;
The if [...] mod [...] = 0 then statements are where you can show the two phases' progress howver you want.
Btw, this code was olny intended to get you started. I'm obliged to Jerry Dodge for reminding me that in recent versions of Delphi, there is similar functionality built-in, by way of the TDirectory.GetFiles method so if you are interested in cross-platform and/or accommodate Unicode, you would do better to study the ins and outs of TDirectory and non-Windows-specific routines like TrailingPathDelim.
When you really want to show some progress in a UI when deleting files, you should use threads:
create a thread, which deletes the files
then poll the progress of the deletion thread from the UI
Be careful when using threads, not to access UI parts (like the progressbar) from within the deletion thread. Such things should at least be synchronized.

Load database field of all records into ListView Item Detail Object

Using Delphi XE8 I'm currently testing functionality with Firemonkey TListViews.
One thing I'm trying to do is to load a field of all records from a TFDMemtable component into a Listview Item, specifically into the DetailObject of the ListView Item.
For example, I have 3 records in a table (db field is called 'Name'):
Record 1 = Name 1
Record 2 = Name 2
Record 3 = Name 3
There is only 1 DetailObject property per ListView Item so my question is, would I be able to add all of the fields (Name 1, Name 2, Name 3) into that one DetailObject?
Below is what I've attempted so far but no luck. Not 100% sure what I need to do.
procedure MainForm.BuildList;
var LItem : TListViewItem;
begin
ListView1.BeginUpdate;
try
ListView1.CLearItems;
LItem := ListView1.Items.Add;
LItem.Objects.DetailObject.Visible := True;
with memtable do
begin
while not eof do
begin
LItem.Detail := FieldByName('Name').AsString;
end;
end;
finally
ListView1.EndUpdate;
end;
end;
I'm sorry if this isn't clear enough, please let me know.
Any help would be great.
I think I should warn you that before seeing your q, I'd never done anything with FMX ListViews and Master/Detail datasets. The Following is a little rough around the edges, and the layout isn't ideal, but it shows one way to populate a ListView from Master + Detail datasets. I have no idea whether there are better ways. Personally, I would see if I could use Live Bindings to do the job.
procedure TMasterDetailForm.BuildList;
var
LItem : TListViewItem;
DetailItem : TListViewItem;
ListItemText : TListItemText;
DetailIndex : Integer;
begin
ListView1.BeginUpdate;
ListView1.ItemAppearanceObjects.ItemEditObjects.Text.TextVertAlign := TTextAlign.Leading; // The default
// seems to be `Center`, whereas we want the Master field name to be at the top of the item
try
ListView1.Items.Clear; //Items;
Master.First;
while not Master.eof do begin
LItem := ListView1.Items.Add;
LItem.Text := Master.FieldByName('Name').AsString;
LItem.Height := 25;
Detail.First;
DetailIndex := 0;
while not Detail.Eof do begin
Inc(DetailIndex);
ListItemText := TListItemText.Create(LItem);
ListItemText.PlaceOffset.X := 100;
ListItemText.PlaceOffset.Y := 25 * (DetailIndex - 1);
ListItemText.TextAlign := TTextAlign.Leading;
ListItemText.Name := 'Name' + IntToStr(DetailIndex); //Detail.FieldByName('Name').AsString;
LItem.Data['Name' + IntToStr(DetailIndex)] := Detail.FieldByName('Name').AsString;
Detail.Next;
end;
LItem.Height := LItem.Height * (1 + DetailIndex);
Master.Next;
end;
finally
ListView1.EndUpdate;
end;
end;
TListItemText is one of a number of "drawable" FMX objects that can be added to do the TListViewItem. They seem to need unique names so that they can be accessed via the Names property.
FWIW, I used 2 TClientDataSets as the Master and Detail in my code.
Also FWIW, for FMX newbies like me, populating an FMX TreeView is a lot more like what you'd do in a VCL project:
procedure TMasterDetailForm.BuildTree;
var
PNode,
ChildNode : TTreeViewItem;
begin
TreeView1.BeginUpdate;
try
TreeView1.Clear;
Master.First;
while not Master.eof do begin
PNode := TTreeViewItem.Create(TreeView1);
TreeView1.AddObject(PNode);
PNode.Text := Master.FieldByName('Name').AsString;
Detail.First;
while not Detail.Eof do begin
ChildNode := TTreeViewItem.Create(TreeView1);
ChildNode.Text := Detail.FieldByName('Name').AsString;
PNode.AddObject(ChildNode);
Detail.Next;
end;
Master.Next;
end;
finally
TreeView1.EndUpdate;
end;
end;
Btw, in your code you should have been calling
memtable.Next;
in your while not eof loop, and memtable.First immediately before the loop.

how do i prevent adding duplicates items in listview in delphi

i have some list inside a list view i want to prevent Adding items that already exist and allow only items that's not exist i search about that before i post my question i find some codes that removes the duplicated items but thats not my point , a little example of what aim trying to achieve , example
listview1.Items.Add.caption := 'item1'
listview1.Items.Add.subitems.add:= 'content'
listview1.Items.Add.caption := 'item2'
listview1.Items.Add.subitems.add:= 'content2'
listview1.Items.Add.caption := 'item3'
listview1.Items.Add.subitems.add:= 'content3'
//duplicated line
listview1.Items.Add.caption := 'item1'// here what i want to ignore if exist and add any other items comes below
listview1.Items.Add.subitems.add:= 'content'
listview1.Items.Add.caption := 'item4'
listview1.Items.Add.subitems.add:= 'content4'
any idea on how to achieve that ignore exist items and add what ever other items ?
Current code
if Command = 'CallThis' then
begin
if Assigned(MS) then
begin
SL := TStringList.Create;
try
SL.LoadFromStream(MS);
for I := 0 to SL.Count -1 do
begin
Line := SL.Strings[I];
ExplodeLine(Line, item, content, num);
with vieform.list.Items.Add do
begin
Caption := StripHTML(item);
Subitems.Add(content);
Subitems.Add(num)
end;
end;
finally
SL.Free;
end;
MS.Free;
end;
end;
You should not use the visual controls to store and manage your data. Have a list for all the data and present the data in the listview or any other control you like.
// class to store data (shortend)
TMyData = class
constructor Create( const Item, Content : string );
property Item : string;
property Content : string;
end;
// list to organize the data
MyList := TObjectList<TMyData>.Create(
// comparer, tell the list, when are items equal
TComparer<TMyData>.Construct(
function ( const L, R : TMyData ) : integer
begin
Result := CompareStr( L.Item, R.Item );
end ) );
// create an item
MyData := TMyData.Create( 'item1', 'content1' );
// check for duplicate in list
if not MyList.Contains( MyData ) then
MyList.Add( MyData )
else
MyData.Free;
// present the list in a ListView
ListView1.Clear;
for MyData in MyList do
begin
ListItem := ListView1.Items.Add;
ListItem.Data := MyData; // store a reference to the data item
ListItem.Caption := MyData.Item;
ListItem.SubItems.Add( MyData.Content );
end;
Thats all
Just write your own procedure which does all the work for you. Also helps with your sub items, except I'm not sure what you were trying to do in your code (This is what I'm assuming you were attempting to do)...
procedure TForm1.Add(const Caption, Sub: String);
var
I: TListItem;
X: Integer;
begin
for X := 0 to ListView1.Items.Count-1 do
if SameText(ListView1.Items[X].Caption, Caption) then Exit;
I:= ListView1.Items.Add;
I.Caption:= Caption;
I.SubItems.Add(Sub);
end;
Then, you simply call it like this:
Add('Item1', 'Content');
Add('Item2', 'Content2');
Add('Item3', 'Content3');
Add('Item1', 'Content1');
That would result in 3 items in the list, because the 4th already exists.
Please note however that this may not actually solve your real underlying issue. If you feel the need to perform this check, then it's probably a good time to re-think your design. The approach you're using makes me believe you're using the TListView to store data. UI controls should never be the container of actual data, it should only provide the interface to the user.

Why doesn't my TStringList gets sorted

I Have a TStringList I create on FormCreate
ScriptList := TStringList.Create;
In another function in my program after I have loaded strings into the list I have the following code
ScriptList.Sorted := True;
ScriptList.Sort;
for i := 0 to ScriptList.Count - 1 do
ShowMessage(ScriptList[i]);
But the list is not sorted
Why is that?
Edited:
Filling the list is done by the following code
function TfrmMain.ScriptsLocate(const aComputer: boolean = False): integer;
var
ScriptPath: string;
TempList: TStringList;
begin
TempList := TStringList.Create;
try
if aComputer = True then
begin
ScriptPath := Folders.DirScripts;
Files.Search(TempList, ScriptPath, '*.logon', False);
ScriptList.AddStrings(TempList);
end
else
begin
if ServerCheck then
begin
ScriptPath := ServerPath + 'scripts_' + Network.ComputerName + '\';
Folders.Validate(ScriptPath);
TempList.Clear;
Files.Search(TempList, ScriptPath, '*.logon', False);
ScriptList.AddStrings(TempList);
Application.ProcessMessages;
ScriptPath := ServerPath + 'scripts_' + 'SHARED\';
Folders.Validate(ScriptPath);
TempList.Clear;
Files.Search(TempList, ScriptPath, '*.logon', False);
ScriptList.AddStrings(TempList);
end;
end;
finally
TempList.Free;
end;
ScriptList.Sort;
Result := ScriptList.Count;
end;
The filesearch function:
function TFiles.Search(aList: TstringList; aPathname: string; const aFile: string = '*.*'; const aSubdirs: boolean = True): integer;
var
Rec: TSearchRec;
begin
Folders.Validate(aPathName, False);
if FindFirst(aPathname + aFile, faAnyFile - faDirectory, Rec) = 0 then
try
repeat
aList.Add(aPathname + Rec.Name);
until FindNext(Rec) <> 0;
finally
FindClose(Rec);
end;
Result := aList.Count;
if not aSubdirs then Exit;
if FindFirst(aPathname + '*.*', faDirectory, Rec) = 0 then
try
repeat
if ((Rec.Attr and faDirectory) <> 0) and (Rec.Name<>'.') and (Rec.Name<>'..') then
Files.Search(aList, aPathname + Rec.Name, aFile, True);
until FindNext(Rec) <> 0;
finally
FindClose(Rec);
end;
Result := aList.Count;
end;
The main problem is that the list is filled OK with the items I want, but it never gets sorted.
When you set Sorted to True you are saying that you want the list to be maintained in order. When new items are added, they will be inserted in order. When Sorted is True, the Sort method does nothing because the code is built on the assumption that the list is already order.
So, in your code calling Sort does nothing and could be removed. However, I would take the alternative approach, remove the setting of Sorted and call Sort explicitly:
ScriptList.LoadFromFile(...);
ScriptList.Sort;
for i := 0 to ScriptList.Count - 1 do
...
Now, in fact I think that your code is not quite as you have claimed. You claim that you load the file, and then set Sorted to True. That cannot be the case. Here is the SetSorted implementation:
procedure TStringList.SetSorted(Value: Boolean);
begin
if FSorted <> Value then
begin
if Value then Sort;
FSorted := Value;
end;
end;
So, if Sorted is False when you set it to True, the list will be sorted.
But even that does not explain what you report. Because if Sorted is True when you call LoadFromFile, each new line will be inserted in order. So, what you report in the question cannot be the whole story.
Unless you are making subsequent additions to the list, it is cleaner, in my view, to ignore the Sorted property. Leave Sorted as its default value of False. And call Sort when you want to enforce an ordering to the list. All the same, it might be worth digging a bit deeper to understand why your assertions in the question don't tally with the implementation of TStringList.

Resources