This tutorial follows on from the Level Three tutorial about making arrays of controls. (Originally (and probably still!) called DT3a). There is a search button at the bottom of the page. It will show you how you can customise a standard Delphi component to give it the features you wish Borland had incorporated in the first place. This is an early draft... apologies if there are mistakes. Please send me an email if you encounter any... or to say that you worked through successfully? Tom Boyd [email protected] As you work through it, you might feel that you could accomplish what is here by more direct means. There is a penalty to pay, too, in that the component we are going to develop cannot be placed on the design-time form that is a major aid to the 'visual' aspect of Delphi programming. (I suspect that there are ways you COULD add your component to the Components Palette... but they are not covered here.) If you find those thoughts a serious problem, consider this: How else would you produce AN ARRAY of edit boxes, even without customisations? All the completed program will do is provide an inches to centimeters converter. It won't even work both ways! However, by keeping the program simple, the essentials of customising a component should be kept clear. I am endebted to Mr. Alan Lloyd of comp.lang.pascal.delphi.misc newsgroup for showing me how to customising a component. There is also a welth of information in the Delphi help file (of course) if you can find it (not always easy)... look under components, overviewof component creation. Start a new project. I called my Demo, with my usual suffixes to distinguish forms, units, etc. In particular, to make the following work, name the form DemoF1. Add StdCtrls to the Uses section. The TEdit control is defined in StdCtrls, so the compiler needs StdCtrls to create our customised TEdit. Put a label on the form. Autosize=false. Top=50,Left=50,Height=50,Width=50. Select the label. Do ctrl-c. Do ctrl-v three times. You should now have 4 labels. Leaving the first where it was, drag the others so that you have two rows of two, neatly spaced. Now delete the first... our customised edit box will go there. Make the captions of the upper and lower right hand labels 'Inches' and 'Cms'. Make the name of the lower left label lOutput. Just to get everything else working, put an ordinary edit box where the first label was. Save what you've got so far. Run it and debug if necessary.. it shouldn't do anything yet, but it should run! Program the edit box's OnChange event as follows: As a first step, make it just lOutput.caption:=FloatToStrF(4.5,ffFixed,4,2); With that, any change in the edit box should put 4.50 in lOutput. Next make edit box's text 0 and make the OnChange event procedure TDemoF1.Edit1Change(Sender: TObject); var rInches:real; begin try rInches:=StrToFloat(Edit1.text); except on EConvertError do rInches:=0 end; lOutput.caption:=FloatToStrF(rInches*100/39.37,ffFixed,4,2); end; Delphi will report the EConvertError if you make the contents of the edit box invalid for conversion to a number, but just click OK and then Run again. Also, the insertion point indicator in the edit box disappears... but you can edit the edit box contents normally. (If someone could tell me how to avoid losing the IPI, I'd be grateful.) When you've got this working, save. Next we will replace the standard edit box control with one of our own devising. In the unit's Type section, add: TBoydEdit = class(TEdit) public constructor Create(AOwner:TComponent;iTop,iLeft,iSize:integer); destructor Destroy; end;(*Declare type TBoydEdit*) As you can guess, the name TBoydEdit is up to you, though convention suggests starting it with a T. The names AOwner,iTop and iLeft are also up to you. (I would have used something like compOwner, but Borland used AOwner) The first parameter of the Create method will always (?... comment form any expert who knows otherwise will be welcomed!) be there, and be of type TComponent. The other parameters are up to you... include whatever is needed for your customisation requirements. In this example, we are going to be able to say where the edit box will be and how big. It will always be square.... but you should be able to re-define it for some other rule, once you've studied this tutorial. (You can't run the program at this stage) Add the following to the implementation section of the program: constructor TBoydEdit.create(AOwner:TComponent;iTop,iLeft,iSize:integer); begin end; destructor TBoydEdit.destroy; begin end; Your program should run again, though it won't yet do anything it didn't do before. revise the constructor (in the implementation section) as follows: constructor TBoydEdit.create(AOwner:TComponent;iTop,iLeft,iSize:integer); begin inherited create(AOwner); parent:=TWinControl(AOwner); with Self do begin top:=iTop; left:=iLeft; width:=iSize; height:=iSize; visible:=true; enabled:=true; color:=clGreen; end;(*...with self*) end; Again, your program should run, though it still won't yet do anything it didn't do before. What properties can you set? What names do you use? Just look in the Delphi help file. If you are making a customised TEdit control, then the TEdit properties are available to you... and you must use the names defined by the TEdit. E.g., in the example above, top, left, width, height, visible, enabled, color. Drag the edit box you gave your form to some out of the way place. In the Var section, add BoydEdit1:TBoydEdit; Add a OnCreate event to your form, and program it as follows: BoydEdit1:=TBoydEdit.create(DemoF1,50,50,50); If you run the program now, you should see a green edit box where the old one used to be. Entering data won't do anything yet. (Why green? Just to show it's not an ordinary edit box!) Now we come to a further problem connected with the fact that the TBoydEdit version of the edit box is not registered and so it cannot be 'seen' by the object inspector. How do we get the program to process events arising from BoydEdit1? In particular, we need to write an event handler to react to changes in BoydEdit1.text. With the standard edit box, we just turned to the object inspector and clicked the OnChange event, and a skelton for a handler appeared. First: What events can you attach code to? The answer is similar to the answer to the question above about what properties are available. If you are customising TEdit, then use the object inspector to look at TEdit's events. You'll see OnChange, OnClick, OnDblClick, etc. The first step is to choose a name for the procedure that will handle the event from your customised control. For this example, I'm using the name BoydOnChange. Now, in the With Self block, add a line to the control's create constructor saying OnChange:=BoydOnChange; It will look as if you are merely declaring another property default. In a loose sense you are... you are declaring what procedure to execute when OnChange events arise from the control. Note that you have no choice over the name 'OnChange', though of course you could specify handlers for other events. The next thing you must do is put private procedure BoydOnChange(Sender:TObject); in the declaration of the TBoydEdit type near the top of the program. It can go just before the word 'public'. (Sorry... at this point my understanding of public/private/published and implementation is too hazy to explain or defend details... but what I've aasid to do does work!) And lastly, copy the old TDemoF1.Edit1Change, and make changes in the (**)'d lines to make it procedure TBoydEdit.BoydOnChange(Sender: TObject); (**) var rInches:real; begin try rInches:=StrToFloat(BoydEdit1.text); (**) except on EConvertError do rInches:=0 end; DemoF1.lOutput.caption:=FloatToStrF(rInches*100/39.37,ffFixed,4,2); (**) end; That should do it! Of course, the ordinary edit box put on the form early in the tutorial could now be deleted. If you decide to do this, you'll want to turn things like procedure TDemof1.Edit1Change(Sender: TObject);... will have to be taken out as well. Dt4b: A Little File Massaging program. This is a level 4 tutorial not because it is very complicated, but because I want to go quickly, skim many details. The tutorial describes the start of creating a program I wrote for converting Notepad files into crude HTML pages, the pages used in these tutorials! The program.... Asks the user what file is to be massaged. (Called the source program from here on, and a file named Source.txt is going to be the example.) Renames it from, e.g., Source.txt to Source.bak (If there was already a Source.bak, the user can either delete the old Source.bak, or give a different name for Source.txt to be renamed.) Copies Source.Bak to Source.txt, but changes any lower case 'e's to upper case 'E's. This is a simple thing, useful only as an exercise for the muscles of this program. (In the version I use, the program adds [br] before each isolated CR/LF, and two [br]s before a double cr/lf.) So.. Broad outline: Open old file Rename it Copy from it, inserting [br]s Close output file Close input file Details of Open/ Rename: Set TestFlag false Repeat If TestFlag=true then explain why file unsatisfactory Get name of source file Set TestFlag true Until file named is more than 8 bytes long Set NameToGiveIt to [previous].bak Set TestFlag false Repeat If FileExists(NameToGiveIt)=true then ....Ask 'Delete the old file called NameToGiveIt'? ....If Yes then begin .........delete NameToGiveIt .........Set TestFlag true .......else (may not delete) .........Get new NameToGiveIt from user ...else (did not exist) .....Set TestFlag true Until TestFlag true Rename as NameToGiveIt So... that's the plan for how we do the filename shuffling. Now... the code!.... Start a project as explained in Tutorial 1. Call it DD06. Make the form about 300 high, 400 wide. Put a label called lMsgs in the bottom part of the form; caption:='Welcome to file massager'; wordwrap:=true; Add a button captioned 'Select and process file' From the dialogs tab, put an OpenDialog component on DD06's form. Set option ofFileMustExist=true In the unit's var declaration add... dfin,dfout:file of byte;(*DataFiles for INput, OUTput*) dfinName, dfoutName:string; Use the object inspector to make DD06f1.Button1Click and fill it as follows... procedure TDD06f1.Button1Click(Sender: TObject); var TestFlag,DidNotCancel:boolean; begin TestFlag:=true;(*to suppress message on first pass*) repeat if TestFlag=false then showmessage('The file you specified is too short for this program to '+ 'process. Name another.'); TestFlag:=false;(*will stay false til good file spec'd*) opendialog1.filter:= 'Text files (*.TXT)|*.txt|All files (*.*)|*.*|'; DidNotCancel:=opendialog1.execute; AssignFile(dfin,opendialog1.FileName); dfinName:=opendialog1.FileName; reset(dfin); if Filesize(dfin)>5 then TestFlag:=true; closefile(dfin); application.processmessages; until (not DidNotCancel) or (TestFlag=true); end; ...and get that much working... it should provide for finding a suitable file, even though it does nothing with it yet. Next, we need to save and then strip off any extension the filename may have had, e.g. .txt, so to the unit's vars add... sExtn:string and to the ButtonClick handler's vars add ... c1:byte and after ... until (not DidNotCancel) or (TestFlag=true); ... add ... If DidNotCancel then begin sExtn:='';(*in case no extension*) c1:=pos('.',dfinName); if c1>0 then begin sExtn:=copy(dfinName,c1,1+length(dfinName)-c1); dfinName:=copy(dfinName,1,c1-1); lTmp.caption:=dfinName; (*note that the '.' of, e.g., '.txt' saved in sExtn, and that dfinName is now extensionless*) end; end;(*DidNotCancel*) Now the selection of a new name for the source can begin. In a simple case, something like Source.txt simply becomes Source.bak, but we are providing for the case where Source.bak already exists. To ButtonClick's vars add ... ...add sTmp:string t and just before the... end;(*DidNotCancel*) ... add ... TestFlag:=false; sTmp:=dfinName+'.bak'; repeat if FileExists(sTmp) then begin if MessageDlg('File called '+sTmp+' already exists. Delete it?', mtConfirmation, [mbYes, mbNo], 0) = mrYes then begin if DeleteFile(sTmp) then TestFlag:=true (*no ; here*) else (*handle failed deletes*); end(*No. ; here. This ends do a delete*) else (*not permitted to delete old .bak, so a new name will have to be provided, which will then be tested.*) sTmp:= InputBox('Make Change', 'Give a different extension', '.bak'); if copy(sTmp,1,1)<>'.' then sTmp:='.'+sTmp; if length(sTmp)>4 then sTmp:='.bak';{already known to be invalid} sTmp:=dfinName+sTmp; end(* no ; here. File Existed*) else TestFlag:=true;(*File didn't exist*) until TestFlag; RenameFile(dfinName+sExtn,sTmp); dfinName:=sTmp; dfoutName:=dfinName+sExtn; (*dfin/outNames now hold current name of source file and name of file output to go to, with their extensions*) That completes the renaming of the source file, probably as Source.bak. N.B. The frequent changes of what is in dfinName, which reflect the progress of the processing, give opportunities for confusion. Now we start on the code for reading through it, writing it out to a new file, making changes along the way. Just before... end;(*DidNotCancel*) ... add ... ReadMassageWriteToNew; lMsgs.caption:='Done!'; ... and just after ... procedure TDD06f1.Button1Click(Sender: TObject); var TestFlag,DidNotCancel:boolean; ... add ... procedure ReadMassageWriteToNew; (*On entry, dfinName and dfoutName should hold the full path+name+exten specification of the file to be read from, and the file to be written to. The first should exist and the second shouldn't*) var wBytesDone:word; bTmp:byte; begin wBytesDone:=0; assignfile(dfin,dfinName); lTmp.caption:=dfoutName; reset(dfin); assignfile(dfout,dfoutName); rewrite(dfout); repeat inc(wBytesDone); if wBytesDone mod 32=0 then application.processmessages; read(dfin,bTmp); if bTmp=ord('e') then bTmp:=ord('E'); (*Change lowercase es to Es, as a simple test of prgm*) write(dfout,bTmp); until eof(dfin); closefile(dfout); closefile(dfin); end; Obviously, a program to change es to Es is of limited use, but it illustrates a number of points, and will, I hope, be useful as a starting point for your own needs. N.B. the application.processmessages which appears within the Repeat..until loop. Without this, your computer can be trapped within the loop if for some reason (typo?) the 'until' condition is never met. Even if it IS met, without the processmessages, this program will take over the computer for its exclusive use until the loop finishes, which is not a good situation to create. Delphi tutorial: A Code Solver... and competition. (Dt4c) This is a link to another of my pages. (The page you are reading is not typical of my "Tutorial" pages.) On the linked page you will find a proposal for a programming competition which would be fun for a class or club. Included in that, however, are two programs, one with its source code in downloadable form. (That program was written to be functional as it is, but extendable for the ambitious) They derive from the simple file manipulation program developed in the related tutorial on this site. The programs are not useful as encryption software, only as a fun exercise for programmers Dt4d: A start towards a Windows Explorer-like program The ideas in this are not especially arcane. Some of the matters raised are relatively central to Delphi programming. Even so, I've put it in Level 4. We are going to see some inter-action between objects, which might be a little alarming for people who are just getting started. This tutorial was written using Delphi 2. SOme of the components used may not be available to Delphi 1 programmers. Some unusual good news for you: The source code for this will be posted in a zip file. It is in two parts. The first shows you how to build Windows Explorer-like access to your backing store, and how to pick a single file from it. Part Two shows a way display the file's contents. It also shows the use of multiple windows. Part One: Navigating Your Backing Store I use the classic Compuserve email software. (Too) many people send their emails twice, once as plain text, and a second time in an HTML version. My primitive email browser doesn't cope well with this, and I don't often open attachments. Thus I end up with dozens of mystery files. I drag them into Notepad, see if I see anything important, and then delete 99% of them. This is tedious! I wanted a better way of weeding my disc. What follows is not a complete solution to that problem, but maybe if you know where I was going, it will help you follow the tutorial. The program's main form is similar to Windows Explorer, but not as good. It lets you see what is on a floppy or hard disc. You can then select files an open them up to see their contents. (In the tutorial, the examination of the contents is not very sophisticated, but the basic idea is covered.) After you've looked at a file, you can add it to a list of files to be deleted later, or simply go on to check another file. When you're done for the day, you can delete the files whose names you've been accumulating. Start a new application in the usual way. Name the form DD13f1 (Delphi Demo). From the System tab of the component pallette, add to your form one each of the following: DirectoryListBox FileListBox (You can leave them with the names assigned by Delphi). Save (in it's own folder) what you have so far. Call the current unit DD13u1, and the project DD13. Create an OnDirectoryListBox1Change event handler consisting simply of: FileListBox1.directory:=DirectoryListBox1.directory; .... and try running the embryonic application. If you click in the DirectoryListBox, you should be able to change which directory you are in, and when you do, the list of files in the FileListBox should change. (Don't worry if folders (directories) are not showing in the FileListBox). This is pretty cool. If you don't believe me, try programming the functionality we have so far without using either of the components I've specified. The more I work with Delphi, the more I find that it is really easy (that's to good news) IF (that's the bad news) you can find (amongst the hundreds... thousands?... of available bits) the thing you need. I'm also learning not to fight Redmond. Don't try to do things YOUR way. Try to figure out how Bill wants it done, and then find the tools to do it that way.... and when you've managed those two hard things, you'll find you've reached the place where things are easy!! Now we're going to more or less repeat the above. First put a formatted floppy in your A: drive. Now, also from the System tab, add a DriveComboBox. To make it work, give it an OnDriveComboBox1Change handler consisting of... DirectoryListBox1.drive:=DriveComboBox1.drive; FileListBox1.drive:=DriveComboBox1.drive; The above works fine, and is not "too" clever. If you like being clever, there is a property of the DirectoryListBox you may want to use: FileList. If you set that to FileListBox1, then you can leave out the FileListBox1.drive... line in the OnDriveComboBox1Change code above. No doubt there are other such tricks available if you sek them out. An aside: The program, as developed so far, doesn't cope well if you click on a drive which doesn't have a disc in it. I'm afraid I haven't sorted out what you'd need to add to a commercial application. Also from the System tab, add a FilterComboBox1, and, surprise, surprise an event handler: FileListBox1.mask:=FilterComboBox1.mask; The Filter property of the FilterComboBox determines what filters are available to your user. Doubleclick on the cell beside "Filter" in the Object Inspector to access an editor for the Filter property. As an exercise: Try to add to the list an entry with name: "Text files (*.txt)", filter: "*.txt" So far so good? This would be a good place to save the project, by the way. "Yes, but it doesn't DO anything", I hear you cry! ("Yes, yes, but you don't go"... who said that?) Add a label to the form. Then add an OnFileListBox1Click event handler label1.caption:=FileListBox1.filename; Now when you click on, or use the cursor keys to move between, file names, the name of the file appears in the label. You do what you like with the file! I've given you access to your backing store, and showed you how to pick a file from it. The rest of this tutorial addresses just one of many things you might do with what you have so far. Part Two: Multiple Windows, Reading Files This application doesn't NEED multiple Windows, but I wanted to have them. We're going to add one window for the display of files, and another to hold a list of files which we have decided to delete in a minute. The "in a minute" approach allows the user to be put through an "Are you sure?" check, but only one, not one for each file to be deleted. So: create two new windows for the application by.... ....using File|New Form. Name the new form FileViewer. Save the project, naming the new unit DD13FVu Repeat the process, naming the third form ToDelete. Adjust sizes and positions to taste. Resave the project, naming the third unit DD13TDu. As we now have three forms (windows), and each has their own code (unit), the screen can get a bit cluttered! The code is in a nice tabbed window. If you can get to the code for one, you can switch to another just by clicking on the relevant tab. If you are looking at the code for a form you want to access, just press F12. If you are looking at a form and need its code: F12 will work for that, too. Put a label across the top of the FileView window. "Label1" will do for a name. (In general, using meaningful names is wise, but where you know there are going to be few of any particular type of component, you can be lazy.) Go back to the already existing (DD13f1) OnFileListBox1Click event handler. Add FileViewer.show; FileViewer.label1.caption:=FileListBox1.filename; You also need to add DD13FVu to the Uses clause near the top of DD13u1. It is inefficient to do a FileViewer.show every time you change the file selected, but I couldn't find a better place to put it! To FileViewer add a button. Call it buExamine. Also add a large memo. Add the following as buExamine's OnClick handler: procedure TFileViewer.buExamineClick(Sender: TObject); var dfFile:file of byte; c1,c2:integer; bTmp:byte; sTmp:string; begin assignfile(dfFile,Label1.caption); reset(dfFile); memo1.clear; for c1:=0 to 15 do begin(*c1*) sTmp:=''; for c2:=0 to 60 do begin if not(eof(dfFile)) then begin read(dfFile,bTmp); if (bTmp<>31) and (bTmp<127) then sTmp:=sTmp+chr(bTmp); end; end; memo1.lines.add(sTmp); end;(*c1*) closefile(dfFile); end; That should "work", but adding FileViewer.memo1.clear; to the OnFileListBox1Click handler in DD13f1 is a good idea. Don't, by the way, attempt to use this program to view the contents of DD13.exe while it is running. It will raise an error. The file viewer is nothing special!! It is just something basic you can refine to meet your needs! Add another button to DD13FileViewer. Call it buAddToDeleteList. In minute, we'll give it an OnClick handler, but first add a memo to ToDelete. Make it quite wide. Set its Wrd Wrap property to false. (Explained later.) Go to the new memo's Lines peoperty via the Object Inspector. Double click on the cell beside "Lines" to bring up the String List Editor. Remove everything from the memo... be sure to achieve "0 lines" in the upper left of the window. Also, add ToDelete.show to DD13f1.FileListBox1Click. It can go just before the FileViewer.show; which is there already. Add DD13TDu to DD13f1's Uses clause. An aside. (You can skip this if you already make a lot of use of CtrlC/ Ctrl-V.) Use Copy/ Paste a lot. It avoids typos, takes the pain out of long names, etc. To add DeleteList.show to DD13f1.FileListBox1Click, all you need to do is... Put your cursor just in front of FileViewer.show Press and keep down the shift key Press the down arrow key once (The effect of the previous two lines can also be accomplished by a drag of the mouse.) Release the shift key Do Ctrl-C, Ctrl-V, Ctrl-V Select the first FileViewer Type DeleteTo Odd at first, but eventually your fingers will fly! (End of aside) Add DD13TDu to FileViewer's Uses clause. Now you can build FileViewer.buAddToDeleteListClick, which is simply... ToDelete.memo1.lines.add(Label1.caption); That takes care of almost everything! All that is left is a way to delete whatever files have been listed on ToDelete. Put a buDeleteThese button on ToDelete. The following is suitable for the OnClick: begin if MessageDlg('Last chance! Abort delete?', mtInformation, [mbNo, mbYes], 0) = mrNo (*Reversing mbNo/Yes does not resquence them in message box*) then begin (*Delete the files*) while memo1.lines.count>0 do begin deletefile(memo1.lines[0]); memo1.lines.delete(0); end;(*while..*) end; (*Delete the files*) end; BE CAREFUL!!!! You now have a "live" file delete capability. I used Notepad to create a bunch of files I could safely examine and/or delete. That should do it! One little chuckle for you. The final part of this, the buDeleteTheseClick handler, was simple copied from a working elaboration of the program in this tutorial. I was late for getting away to something, but just wanted to get the job DONE! Pasted the code in, ran the program, it "worked"... but the files weren't deleted! No error messages. The memo was cleared properly. So why no file deletes? Eventually, I noticed that my memo was narrow and I had not set WordWrap to false. In the original, the memo was wide. Thus, before I fixed things, the program was trying to delete things like... C:\Program Fil es\Borland\aTmpFi le.txt ... which the memo had split into three lines, and, not surprisingly, attempting Deletefile('C:\Program Fil') resulted in no deletion! Oh, the joys of programming.... There is a crudeness in this: After you've deleted the files, they are still listed in the FileListBox. I'm sure there's some way to overcome that, but I leave that to you.... :-) Anyway... I hope that was useful, and that you won't hesitate to bring errors to my attention.
Hosted by www.Geocities.ws

1