Pascal Newsletter #2
The full source code examples of this issue are available for download.
![]() |
![]() |
Pascal Newsletter #2 INDEX 1. A FEW WORDS FROM THE EDITOR 2. FILE FINDER Alternative to not subclassing and replacing a component Reading and saving a file A simple tokenizer Putting the pieces together Sorting a TListView 3. IS PASCAL DYING? 4. A SIMPLE COMMAND-LINE APPLICATION ________________________________________________________________________ 1. A FEW WORDS FROM THE EDITOR We are very grateful for the support, encouragement and words of appreciation we have received from our subscribers. We are open to hearing about your programming needs to see if we can write articles to cover them in this newsletter. Please remember you can also join our mailing lists to discuss the articles of this newsletter and other programming issues. You can find the source code in our web site. Regards, Ernesto De Spirito eds2004 @ latiumsoftware.com ________________________________________________________________________ JfControls Library. Multi-language. Multi-appearance. Skins. Privileges. More than 40 integrated and customizable components. Impressive GUI. Centralized resources administration. Multiple programming problems solved. For Delphi 3-7 and C++ Builder 3-6. http://www.jfactivesoft.com/ ________________________________________________________________________ 2. FILE FINDER Alternative to not subclassing and replacing a component ======================================================== In the article "Subclassing components" of the previous issue, we saw an example where we derived a new class from TListView to add a new method to it (GetItemAtX). Then we installed this new component and changed the class of ListView1 in the form of our file finder application. When all we do is just add a few methods to a class, all this procedure of having to install a new component could be avoided by just adding the unit of the new class and casting the object when we need to call one of the methods we implemented in this new class. For example, to our FileFind project of the second issue of the Delphi Newsletter now we added (Shift+F11) the ListViewX unit we wrote in the last issue, and we only changed the double click event of ListView1: procedure TForm1.ListView1DblClick(Sender: TObject); var Col: Integer; ListItem: TListItem; begin ListItem := TListViewX(ListView1).GetItemAtX(Last.X, Last.Y, Col); if ListItem <> nil then begin if Col = 0 then begin if ShellExecute(Self.Handle, nil, PChar(ListItem.SubItems.Strings[0] + ListItem.Caption), nil, nil, SW_SHOWMAXIMIZED) <= 32 then begin Application.MessageBox(cstrCouldNotExecApp, 'Error', MB_ICONEXCLAMATION); end; // if end else if Col = 1 then begin if ShellExecute(Self.Handle, 'explore', PChar(ListItem.SubItems.Strings[0]), nil, nil, SW_SHOWMAXIMIZED) <= 32 then begin Application.MessageBox(cstrCouldNotExecApp, 'Error', MB_ICONEXCLAMATION); end; // if end; // if end; // if end; It is almost the same as the one we saw in the previous issue, except for the first executable line. We cannot call ListView1.GetItemAtX(...) since GetItemAtX is not a method of TListView (the class of ListView1) or one of its ancestors, but a method of a derived class of TListView: TListViewX. Since this derived class does not add data fields (just one method), then both classes are assignment compatible and we can cast a TListView object to a TListViewX object using this expression: TListViewX(ListView1) which is of type TListViewX, so we can use it to call the GetItemAtX method: TListViewX(ListView1).GetItemAtX(...) Reading and saving a file ========================= In the file finder application, we loaded the contents of a file in a TStringList object using the LoadFromFile method, and then we looked for the keywords in its Text property. This way of doing things is not really efficient, since LoadFromFile loads the file and parses it to separate the lines, and then when we use the Text property, an internal method is called to produce a string joining the lines, so this double job is done for nothing, plus we use more than the double of the memory storage you need. A better approach could be using a TFileStream object (or the old Assign, Reset, Read and Close procedures) to directly read the contents of a file into a string. Here is a function using TFileStream that returns the content of the file whose name is passed as parameter: function LoadFile(const FileName: string): string; var Stream: TFileStream; begin Stream := nil; Result := ''; try Stream := TFileStream.Create(FileName, fmOpenRead or fmShareDenyWrite); SetLength(Result, Stream.Size); Stream.Read(Pointer(Result)^, Stream.Size); except Result := ''; Stream.Free; raise; end; // try Stream.Free; end; If we needed to save the contents of string back to disk, we can use the following procedure: procedure SaveFile(const FileName: string; content: string); var Stream: TFileStream; begin Stream := nil; try Stream := TFileStream.Create(FileName, fmCreate); Stream.Write(Pointer(content)^, Length(content)); except Stream.Free; raise; end; // try Stream.Free; end; A simple tokenizer ================== As it is now, our application looks for files containing the phrase we enter in the corresponding text box of the form. Our intention actually is that it can look for keywords, like an Internet search engine. For example, if we enter Hello World in the Containing text box, we would like to see all the files that contain the word "Hello" and/or the word "World" (either together, separated, near, far, in that order or the second before the first), not all the files that contain the exact phrase "Hello World", although sometimes we might want to perform a phrase search... So, what we do is define a simple syntax for the keyword search. For example we assume that spaces separate keywords, except for text enclosed by quotes: Hello World ==> Two keywords/phrases: 'Hello' and 'World' 'Hello' World ==> Two keywords/phrases: 'Hello' and 'World' Hello "World" ==> Two keywords/phrases: 'Hello' and 'World' "Hello" 'World' ==> Two keywords/phrases: 'Hello' and 'World' "Hello World" ==> One keyword/phrase: 'Hello World' 'Hello ' World ==> Two keywords/phrases: 'Hello ' and 'World' Hello " World" ==> Two keywords/phrases: 'Hello' and ' World' Sometimes we need to search for special characters like tabs, carriage returns, line feeds, etc. We normally can't insert some of these characters in a textbox (or write them in the command-line), but we can do like the C language preprocessor and use the backslash to indicate that the next character has a special meaning (For example \t stands for the tab character and \n stands for the CR+LF pair). Now, first thing we are going to do is write a simple string tokenizer that separates the keywords of a string and interprets some special characters: procedure GetKeywords(StringList: TStringList; s: string); const blanks = [' ', #9, #13, #10]; var i, n: integer; c, quote: char; status: (tsBlank, tsToken); token: string; backslash: boolean; begin StringList.Clear; s := s + #0; // Add end-of-string mark n := Length(s); quote := #0; // Just to avoid a compiler warning backslash := false; // Just to avoid a compiler warning status := tsBlank; for i := 1 to n do begin c := s[i]; case status of tsBlank: if c = #0 then begin break; // End of string end else if c in blanks then begin // Ignore blanks end else if c in ['"', ''''] then begin status := tsToken; quote := c; backslash := false; token := ''; // New token end else if c = '\' then begin status := tsToken; quote := #0; backslash := true; token := ''; // New token end else begin status := tsToken; quote := #0; backslash := false; token := c; // New token end; tsToken: if c = #0 then begin StringList.Add(Token); // End of token break; // End of string end else if (c in blanks) and (quote = #0) then begin StringList.Add(Token); // End of token status := tsBlank; end else if backslash then begin case c of 'n': token := token + #13#10; 'r': token := token + #13; 'a': token := token + #10; 't': token := token + #9; 'q': token := token + '"'; '@'..'Z': token := token + Chr(Ord(c)-Ord('@')); 'e': token := token + #27; '/': token := token + #28; '*': token := token + #29; '-': token := token + #30; '+': token := token + #31; '^': token := token + #127; else token := token + c; end; // case backslash := false; end else if c = '\' then begin backslash := true; end else if c = quote then begin StringList.Add(Token); // End of token status := tsBlank; end else begin token := token + c; end; end; // case status of end; // for end; This procedure implements a (two-)state machine to do the job of parsing the string it receives as a parameter to separate the keywords (that are placed in a string list). For example, if we had a TStringList object named SL1, after the sentence GetKeywords(SL1, '"\"Hello " World"'), SL1 will have two strings: SL1.Strings[0] (or just SL1[0]) being '"Hello ' SL1.Strings[1] (or just SL1[1]) being 'World"' Think of it as a vending machine to which you can insert characters as if they were coins. Actually, in this case the characters will be automatically taken from a string inside the for..do loop. The machine will process each character and change its internal state as needed. For example, when its state is tsBlank, if it receives more white spaces (spaces, tabs, CRs and LFs) it keeps being in that state, until it receives a different character (the first of a keyword) that will make it shift to the tsToken state, and it starts "recording" the next characters that arrive until it receives a white space signaling the end of the token. It is time then to deliver the product (add the token to the string list) and go back to the tsBlank state. Well, actually it's a bit more complex because we considered quoted tokens, backslashes, and an end-of-input character (#0), but all in all it should be easy to understand if you are familiar with this kind of algorithms... Putting the pieces together =========================== Now we are ready to rewrite the Execute procedure of the search thread: procedure TThread1.Execute; var Keywords: TStringList; Content: string; i, n: integer; // ------------------------------------- procedure ScanFolder(const folder: string); var SearchRec: TSearchRec; i: integer; begin if FindFirst(folder + OwnerForm.Edit1.Text, faReadOnly Or faHidden Or faSysFile Or faArchive, SearchRec) = 0 then begin Location := folder; repeat try FileName := SearchRec.Name; Content := AnsiUpperCase(LoadFile(folder + FileName)); Score := 0; for i := 0 to n do if AnsiPos(Keywords[i], Content) <> 0 then inc(Score); if Score > 0 then begin inc(Count); Time := FileDateToDateTime(SearchRec.Time); Synchronize(AddFileName); end; // if except end; // try until Terminated Or (FindNext(SearchRec) <> 0); end; // if FindClose(SearchRec); if (not Terminated) and OwnerForm.Checkbox1.Checked then begin if FindFirst(folder + '*', faReadOnly Or faHidden Or faSysFile Or faArchive Or faDirectory, SearchRec) = 0 then begin repeat try if ((SearchRec.Attr and faDirectory) <> 0) and (SearchRec.Name <> '.') and (SearchRec.Name <> '..') then ScanFolder(folder + SearchRec.Name + PathSeparator); except end; // try until Terminated Or (FindNext(SearchRec) <> 0); end; // if FindClose(SearchRec); end; // if end; // ------------------------------------- begin // procedure TThread1.Execute; Count := 0; Synchronize(Initialize); Keywords := TStringList.Create; GetKeywords(Keywords, OwnerForm.Edit2.Text); n := Keywords.Count - 1; for i := 0 to n do Keywords[i] := AnsiUpperCase(Keywords[i]); ScanFolder(OwnerForm.Edit3.Text); Keywords.Free; Synchronize(Finalize); end; Sorting a TListView =================== We decided to add two more columns to ListView1: one for the time stamp of the files and another one to hold the "score" of the files, being the number of keywords found in the files. To be able to fill those columns, we added two fields to the TThread1 class to hold their corresponding values: Time: TDateTime; // Time stamp of the file Score: cardinal; // Number of hits And we also had to add these two lines at the end of the AddFileName method of TThread1: ListItem.SubItems.Add(DateTimeToStr(Time)); ListItem.SubItems.Add(IntToStr(Score)); After the search is done, we would like to sort the files based on their score. Sorting a TListView on the first column is easy: setting the SortType to stText is like setting Sorted to True in TListBox object, and setting SortType to stNone is like setting Sorted to False in a TListBox object. To have a TListView sorted on another column (or arbitrary data stored or referenced in TListItem objects), we should either write an OnCompare event or an ordering function to be used with the CustomSort method. If you want to keep a list sorted while adding, modifying and deleting items, then you should use an OnCompare event. The parameter "Compare" should be set to 1, -1 or 0 depending on whether the first item is greater than (or should be placed after) the second item, the first item is lower than (or should be placed before) the second item, or if the two items are equal, respectively. For example: procedure TForm1.ListView1Compare(Sender: TObject; Item1, Item2: TListItem; Data: Integer; var Compare: Integer); var n1, n2: cardinal; begin n1 := StrToInt(Item1.SubItems[2]); n2 := StrToInt(Item2.SubItems[2]); if n1 > n2 then Compare := -1 else if n1 < n2 then Compare := 1 else Compare := 0; end; If you want to keep the list sorted, set its SortType property to stBoth. Setting SortType to stNone afterwards won't undo the sorting, but future additions and changes on the list won't be sorted, so if you just want to perform a temporal sort, you can do the following: ListView1.SortType := stBoth; ListView1.SortType := stNone; or else: ListView1.CustomSort(nil, 0); If you need a faster sort, then you should write an ordering function. This function should return 1, -1 or 0 (like the Compare parameter of the OnCompare event discussed above). For example: function ByScore(Item1, Item2: TListItem; Data: integer): integer; stdcall; var n1, n2: cardinal; begin n1 := StrToInt(Item1.SubItems[2]); n2 := StrToInt(Item2.SubItems[2]); if n1 > n2 then Result := -1 else if n1 < n2 then Result := 1 else Result := 0; end; Then, every time you want to sort the list, you call CustomSort passing the address of the ordering function. For example: ListView1.CustomSort(@ByScore, 0); The Data parameter of the OnCompare event is 0 if the event is called automatically when SortType is stData or stBoth, but if it is generated because of a call to CustomSort, then its value is the second parameter to this method. The same happens with the Data parameter of the ordering function, so the Data parameter is normally used to specify a column to sort (we didn't use it in our example since we always order by the fourth column). In our example we added the ByScore function as presented above and at the end of the Finalize method we added this sentence: OwnerForm.ListView1.CustomSort(@ByScore, 0); This is it for now. You can find the full source code in our web site: http://www.latiumsoftware.com/download/p0002.zip ________________________________________________________________________ 3. IS PASCAL DYING? A discussion line under this subject took place about a week ago in the fpc-pascal mailing list of Free Pascal. It is true that C++ is still the top language in almost all operating systems, that Visual Basic has a prominent position in the Windows platform, and that other languages like Java, Perl, etc. are taking over the Internet, but that doesn't necessarily mean Pascal is dying. Delphi gives programmers a performance comparable to any C compiler with the ease of the Pascal language syntax, sometimes it compiles faster (an easier syntax help and also the packaging of units instead of libraries and heather files), it comes with an important library of visual and nonvisual components you can readily use in your applications (to which you can add hundreds of components you can download from the net, from freeware to commercial, including many open source ones) and it has one of the strongest, most solid and most helpful programmers communities. Once you start getting a bit used to Delphi and you compare it with other tools you begin understanding what RAD means! Not for nothing Delphi has saved Borland from bankruptcy and is still selling copies. Also a new version is currently under development for Windows along with its counterpart for Linux, thus becoming one of the first commercial RAD tool for this OS as we said in the first issue of our Kylix Newsletter. There are already other commercial RAD tools for Linux, so strictly speaking Delphi won't be the first, but many forecast it will be the first one to become widely accepted (at least it has all the potential). It wouldn't be crazy to think that Kylix could help Linux grow in quantity and quality of applications, allowing it to keep gaining grounds not only in the internet-servers market where it already achieved an important position below Windows NT, but also in the business-servers market where it has a very poor position and proved to be incapable of changing much that situation so far (although it is growing anyway, but at a very slow pace). At the same time, ceteris paribus (specially considering no other big players enter the game), the growth of Linux in this market could feedback the growth of Kylix in a sort of symbiotic relationship. But all this is just speculation... And Delphi is not the only Pascal language implementation going around. Among other pascal interpreters and compilers, Free Pascal is an interesting open source project that ambitions to take Pascal to other operating systems and microprocessors. The free software movement is becoming more important every day, evangelization continues and we don't know its limits, but we certainly know it hasn't reached them yet... Do you agree? Do you disagree? Do you have comments? Opinions? Would you like to share them with us? Please write! ________________________________________________________________________ 4. A SIMPLE COMMAND-LINE APPLICATION We've been asked for more examples of TStringList and for a sample command-line application, so here they go two for the price of one! :) We decided to write an application capable of replacing multiples occurrences of one string with another string, in one file or a group of files. For example: Replacer "e-mail" "email" c:\web\index.htm c:\web\content\*.html @c:\web\filelist.txt This will search index.htm and all the files that match the file specification c:\web\content\*.html, and all the files listed in the lines of the file c:\web\filelist.txt (wildcards accepted) looking for text "e-mail" and replacing all found occurrences with "email". Replacer @searchandreplace.txt c:\web\index.htm c:\web\content\*.html @c:\web\filelist.txt This will search the same files stated above, but a file named searchandreplace.txt may contain multiple search and replace strings (one pair per line), for example: "</tr>" "" "</font></td>" "</td>" "\t" " " This would remove all occurrences of "</tr>" and all occurrences of "</font>" preceding a "</td>". Also, all tab characters will be replaced by spaces. To avoid reinventing the wheel, we will reuse GetKeywords, LoadFile and SaveFile that we implemented above, so we put them in a unit named Common. Here is the rest of the application: program Replacer; {$APPTYPE CONSOLE} {$DEFINE Debug} uses SysUtils, Classes, Common; var Search, Replace, Files, Temp: TStringList; ff, i, j, m, n: integer; SearchRec: TSearchRec; s: string; FilePath, FileName: string; begin WriteLn('Replacer v1.0 - Copyright (c) 2000 Ernesto De Spirito'); if (ParamCount <= 1) or ((ParamCount = 2) and (Copy(ParamStr(1),1,1) <> '@')) then begin Write('Searches a file (or a group of files) for'); WriteLn(' the occurrences of one string (or a'); Write('group of strings) and replaces them for'); WriteLn(' another string (or a group of strings).'); WriteLn; WriteLn('Syntax:'); Write(#9'Replacer "search text"') WriteLn(' "replace text" "filename.ext"'); Write(#9'Replacer "search text"'); WriteLn( "replace text" "@filename.ext"'); WriteLn(#9'Replacer "@searchandreplace.ext" "filename.ext"'); WriteLn(#9'Replacer "@searchandreplace.ext" "@filename.ext"'); WriteLn; Write('filename.ext is the full path name of'); WriteLn(' the file(s) to process. Wildcards are'); Write('accepted. Multiple files can be specified'); WriteLn(' adding extra parameters.'); Write('The at sign ("@") before a filename to'); WriteLn(' process indicates the specified file'); Write('contains a list of filenames (one per'); WriteLn(' line, and can include wildcards).'); Write('The at sign ("@") before the first parameter'); WriteLn(' indicates a file containing'); Write('search and replace strings (should be enclosed'); WriteLn(' by single or double quotes).'); WriteLn; Write('WARNING: Contents of the files will'); WriteLn(' be changed. Use at your onw risk!'); end else begin // Initialize variables to avoid compiler warnings Search := nil; Replace := nil; Files := nil; Temp := nil; try // Create the objects Search := TStringList.Create; // List of search strings Replace := TStringList.Create; // List of replace strings Files := TStringList.Create; // List of files to process Temp := TStringList.Create; // Temporal string list // Get search and replace strings if Copy(ParamStr(1),1,1) <> '@' then begin ff := 3; // First filename // Easy case. Only one search and replace string Search.Add('"' + ParamStr(1) + '" "' + ParamStr(2) + '"'); end else begin ff := 2; // First filename // Read the file with the search and replace strings. Search.LoadFromFile(Copy(ParamStr(1),2,Length(ParamStr(1))-1)); end; // if // Separate and parse the strings m := Search.Count - 1; j := 0; while j <= m do begin if Trim(Search[j]) = '' then begin Search.Delete(j); dec(m); end else begin GetKeywords(Temp, Search[j]); Search[j] := Temp[0]; Replace.Add(Temp[1]); inc(j); end; end; // while // Ok, now lets get the files to process n := ParamCount; for i := ff to n do begin if Copy(ParamStr(i),1,1) <> '@' then begin Files.Add(ParamStr(i)); end else begin Temp.LoadFromFile(Copy(ParamStr(i),2,Length(ParamStr(i))-1)); Files.AddStrings(Temp); Temp.Clear; end; end; // Trim file names and delete empty lines n := Files.Count - 1; for i := n downto 0 do begin s := Trim(Files[i]); if s = '' then begin Files.Delete(i); end else begin Files[i] := s; end; end; // for // And now we process the files... n := Files.Count - 1; for i := 0 to n do begin // For each filespec if FindFirst(Files[i], faArchive, SearchRec) = 0 then begin FilePath := ExtractFilePath(Files[i]); repeat // Process each file in a filespec FileName := FilePath + SearchRec.Name; Write(FileName + '...'); try // Reads the file s := LoadFile(FileName); // Do the search and replace thing for j := 0 to m do begin s := StringReplace(s, Search[j], Replace[j], [rfReplaceAll, rfIgnoreCase]); end; // for // And writes the file to disk SaveFile(FileName, s); WriteLn(' Ok!'); except on e: exception do WriteLn(' Error: ' + e.Message); end; until FindNext(SearchRec) <> 0; FindClose(SearchRec); end; // if end; //for (processing files) except on e: Exception do WriteLn('Error: ', e.Message); end; // try Search.Free; Replace.Free; Files.Free; Temp.Free; end; WriteLn; {$IFDEF Debug} Write('Press <ENTER> to end...'); ReadLn; {$ENDIF Debug} end. The full source code can be found in our web site. If you need any help you can join the delphi-en mailing list (it's free), where other subscribers to the newsletter might be able to help you and answer your questions and doubts. Free Pascal programmers might have to adapt the code a little bit, but most of the things we used in this example are available in Free Pascal. We are looking for someone who can help us modify examples like this one to work with Free Pascal under Windows and Linux. If you know Free Pascal well enough to do it and have some time for this, please contact us. If you have even more time and want to write some little articles introducing Free Pascal (for Windows and Linux) to be included in this newsletter, PLEASE CONTACT US! :-) ________________________________________________________________________ YOU CAN HELP US We need your help to keep this newsletter going and growing. You can help by referring the newsletter to your colleagues: http://www.latiumsoftware.com/en/pascal/delphi-newsletter.php Or you can help by voting for us in some or all of these rankings to give more visibility to our web site and thus increase the number of subscriptions to this newsletter: http://www.sandbrooksoftware.com/cgi-bin/TopSite2/rankem.cgi?id=latium http://news.optimax.com/delphi/links/links.exe/click?id=70C517ECAE6E http://www.programmingpages.com/?r=latiumsoftwarecomenpascal http://www.top219.org/cgi-bin/vote.cgi?delphi&83 http://top100borland.com/in.php?who=20 http://top200.jazarsoft.com/delphi/rank.php3?id=latium http://213.65.224.200/cgi-bin/toplist.cgi/hits?Id=80 It's just a few seconds for you that REALLY mean a lot to us. ________________________________________________________________________ If you haven't received the full source code examples for this issue, you can get them from http://www.latiumsoftware.com/download/p0002.zip ________________________________________________________________________ This newsletter is provided "AS IS" without warranty of any kind. Its use implies the acceptance of our licensing terms and disclaimer of warranty you can read at http://www.latiumsoftware.com/en/legal.php where you will also find a note about legal trademarks. Articles are copyright of their respective authors and they are reproduced here with their permission. You can redistribute this newsletter as long as you do it in full (including copyright notices), without changes, and gratis. ________________________________________________________________________ Main page: http://www.latiumsoftware.com/en/pascal/delphi-newsletter.php Group home page: http://groups.yahoo.com/group/pascal-newsletter/ Subscribe/join: pascal-newsletter-subscribe@yahoogroups.com Unsubscribe/leave: pascal-newsletter-unsubscribe@yahoogroups.com Problems with your subscription? eds2004 @ latiumsoftware.com ________________________________________________________________________ Latium Software http://www.latiumsoftware.com/en/index.php Copyright (c) 2000 by Ernesto De Spirito. All rights reserved. ________________________________________________________________________ |
The full source code examples of this issue are available for download.
![]() |
Errors? Omissions? Comments? Please contact us!






