|
| A simple spelling corrector |
| In this tutorial, we will build a more extensive program that performs very very simple spelling correction on a text file. It illustrates a number of important features of Delphi, and some of the power of the Delphi Run Time Library. | |
| Our program asks the user to load a text file by pressing a 'Load file' button on the main program form. When the user has selected a file from the file selection dialog that appears, the contents are displayed in a TMemo box on the form, and a ghosted button - 'Correct' - is then enabled. The file line count is shown. | |
| Pressing 'Correct' will correct mis-spellings of the word 'the' (something the author is often guilty of). Stats about the changes will be shown. When done, the 'Save file' button is enabled, allowing the file to be saved. | |
| More than one file can be loaded in a session, and none have to be saved (no warnings are given when saving or when failing to save). | |
| Designing the form |
| It is important when writing programs (applications) to design for ease of use by the user, and not ease of use for you, the coder. This is actually a very difficult thing to do, but it will vastly improve the acceptance of your program. The traditional, and still sensible approach is to design the external appearance and actions of your program, and then work down to code from there. Fortunately, Delphi, like other object oriented programs makes the transition easy. | |
|  |
| In the screen shot above, our form is shown built with 1 Memo box (TMemo), 4 labels (TLabel), and 3 buttons (TButton). The Memo box will be used to show the file text. The red arrows show where under the Standard graphical item tab to select these 'widgets' (a term used to describe a graphical control item). You click on the widget you want, and then drag its size on the form. The final label is shown with the drag corners. | |
| Notice that each widget has a name shown on it. For the Memo box, this is the initial contents that will be shown (our code will reset this to blank). For the labels and buttons, these are called the 'captions'. | |
| The first thing we will do is to change the button captions. Click on Button1, and you will see its attributes in the Object Inspector window. Change the caption from 'Button1' to 'Load file'. Likewise change the Button2 caption to 'Save file' and Button3 to 'Correct'. | |
| As well as changing captions, we will change the names from the default values Delphi used to something more useful. Memo1 becomes MemoBox. And the buttons are now named LoadButton, SaveButton and CorrectButton. The labels can stay as they are. | |
| Our form is now designed! | |
| Linking code to the form |
| Object oriented programming revolves around objects and how they interact together. We have form, memo box, label and button objects. If we double click on any of them, Delphi will create code for the clicked widget. When you run the program, clicking the widget will run the code. As simple as that (except that you can do a whole lot more - see the Events tab of the Object Inspector. When you have added an action, a link to your code will appear there). | |
| Acting upon form creation |
| So we will first double click on an empty part of the form. This will create the following code in your program unit, and position your cursor within it: | |
| procedure TForm1.FormCreate(Sender: TObject); begin end;
|
|
| This code, as its name implies, will be executed when your form is created (do not worry in this tutorial about the Sender parameter). This is very useful. It is effectively the start of your application. More importantly, it is the earliest part of your application when you can access the widgets on the form. We need this access because we will be working with them - initialising them, as in the code here: | |
| procedure TForm1.FormCreate(Sender: TObject); begin // Set the title of the form - our application title Form1.Caption := 'Very simple spell corrector'; // Disable all except the load file button SaveButton.Enabled := false; CorrectButton.Enabled := false; // Clear the file display box MemoBox.Clear; // Enable scroll bars for this memo box - this allows us to scroll up // and down and left and right to see all the text MemoBox.ScrollBars := ssBoth; // do not allow the user to directly type into the displayed file text MemoBox.ReadOnly := true; // Set the font of the memo box to a mono-spaced one to ease reading MemoBox.Font.Name := 'Courier New'; // Set all of the labels to blank Label1.Caption := ''; Label2.Caption := ''; Label3.Caption := ''; Label4.Caption := ''; // Create the open dialog object - used by the GetTextFile routine openDialog := TOpenDialog.Create(self); // Ask for only files that exist openDialog.Options := [ofFileMustExist]; // Ask only for text files openDialog.Filter := 'Text files|*.txt'; // Create the string list object that holds the file contents fileData := TStringList.Create; end;
|
|
| For now, do not worry about the openDialog statements. They'll be covered below. | |
| The last thing we have done is to create a string list (click on it in the code above to learn more). fileData refers to a TStringList variable we have defined earlier (before the implementation section of the unit): | |
| var // Global definitions in our unit Form1: TForm1; fileName : String; fileData : TStringList; openDialog : TOpenDialog;
|
|
| A string list is literally that - an object variable that can hold a variable number of strings. We will be using this to hold the contents of our text file. Before we can do that, we must create (instantiate) a TStringList object. This creation process allocates storage for the object and initialises it internally. It is defined as global so that all of the widget action code can work with it. Be careful with global data - you should keep it to a minimum. | |
| So when our program now starts, the form is cleared of extraneous labelling, and other bits and pieces are set in place, such as scroll bars for the memo box (note that you can do this at the form design stage - take a look at the properties for the memo box). | |
| Acting upon 'Load file' button clicking |
| Double click the 'Load file' button and more code will be inserted into your unit: | |
| procedure TForm1.LoadButtonClick(Sender: TObject); begin end;
|
|
| We want to load up a user selected file. So we need to let the user select using a file selection dialog: | |
| procedure TForm1.LoadButtonClick(Sender: TObject); begin // Display the file selection dialog if openDialog.Execute then // Did the user select a file? begin ...
|
|
| Now we can see that we have used the openDialog defined in the global variable section, and as initialised in the form creation routine. Let's look again at that initialisation: | |
| // Create the open dialog object - used by the GetTextFile routine openDialog := TOpenDialog.Create(self); // Ask for only files that exist openDialog.Options := [ofFileMustExist]; // Ask only for text files openDialog.Filter := 'Text files|*.txt';
|
|
| The file selection (open) dialog object must first be created. It can then be used as many times as we wish. We then configure the dialog to display only existing text files. Click on the TOpenDialog keyword above to learn more. | |
| Returning to the LoadButton code, we now load the file specified by the user: | |
| if openDialog.Execute then // Did the user select a file? begin // Save the file name fileName := openDialog.FileName; // Now that we have a file loaded, enable the text correction button CorrectButton.Enabled := true; // Load the file into our string list fileData.LoadFromFile(fileName); end;
|
|
| We save the name of the file (which includes its full path) so that we can save to it later. We enable the correct text button, and then do the elegant bit - load the file into a string list (the one we defined earlier) using just one statement! We can now access any line of the file by index number! | |
| Finally we do the following: | |
| // Display the file in the file display box MemoBox.Text := fileData.Text; // Clear the changed lines information Label1.Caption := ''; Label2.Caption := ''; Label3.Caption := ''; // Display the number of lines in the file Label4.Caption := fileName+' has '+IntToStr(fileData.Count)+ ' lines of text';
|
|
| we display this complete file in another very elegant statement - fileData.Text converts the string list into one big string which is then written to the memo box for display. | |
| We clear the labels except the fourth. Here we use another feature of the TStringList class - the Count property. It gives the count of lines in the loaded file. | |
| Acting upon the 'Correct' button clicking |
| Before we look at the Correct Button code, let us look at a new data type, an enumerated type, introduced in this unit: | |
| type TheMisSpelled = (TEH, ETH, EHT); // Enumeration of 'the' miss-spellings
|
|
| This defines the 3 mis-spellings of the word 'the' that our code will correct in loaded files. See the Sets and enumerations tutorial for more on enumerations. Here we limit our spell corrector to a tiny range of correction values, as enumerated here. These values, TEH, ETH and EHT are simply placeholders. We use strings to do the checking and corrections. | |
| Double click on the 'Correct' button, and enter the following code: | |
| procedure TForm1.CorrectButtonClick(Sender: TObject); var text : String; line : Integer; changeCounts : array[TEH..EHT] of Integer; begin // Set the changed line counts changeCounts[TEH] := 0; changeCounts[ETH] := 0; changeCounts[EHT] := 0; // Process each line of the file one at a time for line := 0 to fileData.Count-1 do begin // Store the current line in a single variable text := fileData[line]; // Change the 3 chosen basic ways of mis-spelling 'the' if ChangeText(text, TEH) then Inc(changeCounts[TEH]); if ChangeText(text, ETH) then Inc(changeCounts[ETH]); if ChangeText(text, EHT) then Inc(changeCounts[EHT]); // And store this padded string back into the string list fileData[line] := text; end; // And redisplay the file MemoBox.Text := fileData.Text; // Display the changed line totals if changeCounts[TEH] = 1 then Label1.Caption := 'Teh/teh changed on 1 line' else Label1.Caption := 'Teh/teh changed on '+ IntToStr(changeCounts[TEH])+' lines'; if changeCounts[ETH] = 1 then Label2.Caption := 'eth changed on 1 line' else Label2.Caption := 'eth changed on '+ IntToStr(changeCounts[ETH])+' lines'; if changeCounts[EHT] = 1 then Label3.Caption := 'eht changed on 1 line' else Label3.Caption := 'eht changed on '+ IntToStr(changeCounts[EHT])+' lines'; // Finally, indicate that the file is now eligible for saving SaveButton.Enabled := true; // And that no more corrections are necessary CorrectButton.Enabled := false; end;
|
|
| As we make changes, we keep count of the changed lines in the changeCounts array. Notice that it has bounds defined by the enumeration type we defined. We call a new routine ChangeText to do the changes on every string in the string list (the for loop indexes the string list from the first string (index 0) to the last (one less than the number of strings)). We increment counts if the called routine says that it made changes (it returns a Boolean value). Here is that routine: | |
| function TForm1.ChangeText(var Text: String; theType: TheMisSpelled): Boolean; var changed : Boolean; begin // Indicate no changes yet changed := false; // First see if the string contains the desired string case theType of TEH : if AnsiContainsStr(Text, 'teh') or AnsiContainsStr(Text, 'Teh') then begin Text := AnsiReplaceStr(Text, 'teh', 'the'); // Starts lower case Text := AnsiReplaceStr(Text, 'Teh', 'The'); // Starts upper case changed := true; end; ETH : if AnsiContainsStr(Text, 'eth') then begin Text := AnsiReplaceStr(Text, 'eth', 'the'); // Lower case only changed := true; end; EHT : if AnsiContainsStr(Text, 'eht') then begin Text := AnsiReplaceStr(Text, 'eht', 'the'); // Lower case only changed := true; end; end; // Return the changed status Result := changed; end;
|
|
| Click on any blue keywords to learn more. The routine is called 3 times per line - each time it is passed one of the mis-spelling enumeration types. The Case statement routes to the right code to perform according to the type. We set the returned value - Result - to true if we have changed the line. Notice that the Ansi commands are case sensitive. | |
| Returning to the LoadButton code above, the last things we do are to redisplay the changed string list in the memo box and display changed line stats. We also disable the CorrectButton and enable the SaveButton. | |
| Phew! | |
| Acting on the 'Save file' button clicking |
| Finally, double clicking the save button allows us to write code to save the now changed file: | |
| procedure TForm1.SaveButtonClick(Sender: TObject); begin // Simply save the contents of the file string list if fileName <> '' then fileData.SaveToFile(fileName); // And disable the file save button SaveButton.Enabled := false; end;
|
|
| This is a lot simpler - if we have a valid file name (this is a double check that we have something to save), then we use the string list SaveToFile method. Easy! Then we disable the save button. | |
| Putting it all together |
| Below is the full code, along with a sample before and after text file contents : | |
| // Full Unit code. // ----------------------------------------------------------- unit Unit1; interface uses SysUtils, StrUtils, Forms, Dialogs, Classes, Controls, StdCtrls; type TheMisSpelled = (TEH, ETH, EHT); // Enumeration of 'the' mis-spellings TForm1 = class(TForm) // Visual objects inserted by Delphi LoadButton : TButton; SaveButton : TButton; CorrectButton : TButton; MemoBox : TMemo; Label1 : TLabel; Label2 : TLabel; Label3 : TLabel; Label4 : TLabel; // Methods added by Delphi procedure LoadButtonClick(Sender: TObject); procedure SaveButtonClick(Sender: TObject); procedure CorrectButtonClick(Sender: TObject); private // Method added by the author function ChangeText(var Text : String; theType : TheMisSpelled) : Boolean; published // Constructor added by Delphi procedure FormCreate(Sender: TObject); end; var // Global definitions in our unit Form1: TForm1; fileName : String; fileData : TStringList; openDialog : TOpenDialog; implementation {$R *.dfm} // Include form definitions // Procedure called when the main program Form is created procedure TForm1.FormCreate(Sender: TObject); begin // Set the title of the form - our application title Form1.Caption := 'Very simple spell corrector'; // Disable all except the load file button SaveButton.Enabled := false; CorrectButton.Enabled := false; // Clear the file display box MemoBox.Clear; // Enable scroll bars for this memo box MemoBox.ScrollBars := ssBoth; // do not allow the user to directly type into the displayed file text MemoBox.ReadOnly := true; // Set the font of the memo box to a mono-spaced one to ease reading MemoBox.Font.Name := 'Courier New'; // Set all of the labels to blank Label1.Caption := ''; Label2.Caption := ''; Label3.Caption := ''; Label4.Caption := ''; // Create the open dialog object - used by the GetTextFile routine openDialog := TOpenDialog.Create(self); // Ask for only files that exist openDialog.Options := [ofFileMustExist]; // Ask only for text files openDialog.Filter := 'Text files|*.txt'; // Create the string list object that holds the file contents fileData := TStringList.Create; end; // Procedure called when the file load button is pressed procedure TForm1.LoadButtonClick(Sender: TObject); begin // Display the file selection dialog if openDialog.Execute then // Did the user select a file? begin // Save the file name fileName := openDialog.FileName; // Now that we have a file loaded, enable the text correction button CorrectButton.Enabled := true; // Load the file into our string list fileData.LoadFromFile(fileName); end; // And display the file in the file display box MemoBox.Text := fileData.Text; // Clear the changed lines information Label1.Caption := ''; Label2.Caption := ''; Label3.Caption := ''; // Display the number of lines in the file Label4.Caption := fileName+' has '+IntToStr(fileData.Count)+' lines of text'; end; // Procedure called when the file save button is pressed procedure TForm1.SaveButtonClick(Sender: TObject); begin // Simply save the contents of the file string list if fileName <> '' then fileData.SaveToFile(fileName); // And disable the file save button SaveButton.Enabled := false; end; // Procedure called when the correct text button is pressed procedure TForm1.CorrectButtonClick(Sender: TObject); var text : String; line : Integer; changeCounts : array[TEH..EHT] of Integer; begin // Set the changed line counts changeCounts[TEH] := 0; changeCounts[ETH] := 0; changeCounts[EHT] := 0; // Process each line of the file one at a time for line := 0 to fileData.Count-1 do begin // Store the current line in a single variable text := fileData[line]; // Change the 3 chosen basic ways of mis-spelling 'the' if ChangeText(text, TEH) then Inc(changeCounts[TEH]); if ChangeText(text, ETH) then Inc(changeCounts[ETH]); if ChangeText(text, EHT) then Inc(changeCounts[EHT]); // And store this padded string back into the string list fileData[line] := text; end; // And redisplay the file MemoBox.Text := fileData.Text; // Display the changed line totals if changeCounts[TEH] = 1 then Label1.Caption := 'Teh/teh changed on 1 line' else Label1.Caption := 'Teh/teh changed on '+ IntToStr(changeCounts[TEH])+' lines'; if changeCounts[ETH] = 1 then Label2.Caption := 'eth changed on 1 line' else Label2.Caption := 'eth changed on '+ IntToStr(changeCounts[ETH])+' lines'; if changeCounts[EHT] = 1 then Label3.Caption := 'eht changed on 1 line' else Label3.Caption := 'eht changed on '+ IntToStr(changeCounts[EHT])+' lines'; // Finally, indicate that the file is now eligible for saving SaveButton.Enabled := true; // And that no more corrections are necessary CorrectButton.Enabled := false; end; // Function to change a type of 'the' mis-spelling in a string // Returns true if the string was changed function TForm1.ChangeText(var Text: String; theType: TheMisSpelled): Boolean; var changed : Boolean; begin // Indicate no changes yet changed := false; // First see if the string contains the desired string case theType of TEH : if AnsiContainsStr(Text, 'teh') or AnsiContainsStr(Text, 'Teh') then begin Text := AnsiReplaceStr(Text, 'teh', 'the'); // Starts lower case Text := AnsiReplaceStr(Text, 'Teh', 'The'); // Starts upper case changed := true; end; ETH : if AnsiContainsStr(Text, 'eth') then begin Text := AnsiReplaceStr(Text, 'eth', 'the'); // Lower case only changed := true; end; EHT : if AnsiContainsStr(Text, 'eht') then begin Text := AnsiReplaceStr(Text, 'eht', 'the'); // Lower case only changed := true; end; end; // Return the changed status Result := changed; end; end.
|
|
| The displayed file before correction |
| Teh cat sat on eth mat The cat did not sit on eth mat or teh floor Teh teh teh eth eth eht eht Final line of 5.
|
|
| The displayed file after correction |
| The cat sat on the mat The cat did not sit on the mat or the floor The the the the the the the Final line of 5.
|
|
| The displayed statistics |
| Teh/teh changed on 5 lines eth changed on 4 lines eht changed on 2 lines The file has 5 lines
|
|
| | | | |