Author: Lou Adler
How do I copy a file in Delphi?
Answer:
Reminiscing on Days Gone By...
Back in the old DOS days, we took for granted copying a file from one place to
another with the copy command. But with Windows, everything changed. We now use
File Manager or Explorer to copy files from one place to another, which is a huge
improvement over typing in the fully qualified path for both source and destination
files.
But at the programming level, performing the copying of one file to another is not
as apparent as one would think. In fact, there are no native Delphi calls for
copying a file whatsoever. So what do you do if you want to copy a file? You have
to write the routine yourself.
Interestingly enough, there is a pretty good example of copying a file that is in
the FMXUTILS.PAS file in the Delphi\Demos\Doc\Filmanex directory that will perform
a file copy using native Delphi file-related commands. While this method works just
fine, I decided to go another route; that is, to use a file stream to copy a file
from one place to another. Streams are interesting animals. They're used internally
in Delphi to read and write components, forms and data, and they're pretty handy.
Unfortunately, they aren't well-documented so they can be a bit tricky to use. I
went through a lot of trial and error to get them to work, and referenced several
sources outside of the online help (which is just about the only place you'll find
anything on streams in Delphi) before I got a handle on streams. But once I figured
them out, they became what I use for reading and writing files almost exclusively.
There's Almost Always More Than One Way of Doing Things...
Once you've programmed for a while, you realize that it's possible to solve a
particular problem in a variety of ways; which way is valid is dependent upon your
knowledge and experience (one way may be more optimized than another) or, at times,
even the situation will dictate that one methodology is better suited for a task
than another.
For instance, with file copying, there are times you just want to copy a file in
the background using a quick and dirty method, and you don't care if the user knows
what's going on at all. But there are other times, such as when file utilities are
part of an interface, when you want the user to be aware of the copying progress.
What I'm going to show you here are two ways to perform file copying: one quick and
dirty; the other, a more snazzy, graphical way of copying a file, though it uses a
few more resources and is a bit slower.
Quick and Dirty Copying
Traditionally, copying a file involves using a loop to move a series of blocks from
one file into a temporary buffer, then copying the contents of the buffer into
another file. Let's look at the CopyFile function found in the FMXUTILS.PAS:
1
2 {=============================================================================
3 CopyFile procedure found in the FMXUTILS.PAS file in Delphi\Demos\Doc\Filmanex
4 This is an example of copying a file using a buffer.
5 =============================================================================}
6
7 procedure CopyFile(const FileName, DestName: TFileName);
8 var
9 CopyBuffer: Pointer; { buffer for copying }
10 TimeStamp, BytesCopied: Longint;
11 Source, Dest: Integer; { handles }
12 Destination: TFileName; { holder for expanded destination name }
13 const
14 ChunkSize: Longint = 8192; { copy in 8K chunks }
15 begin
16 Destination := ExpandFileName(DestName); { expand the destination path }
17 if HasAttr(Destination, faDirectory) then { if destination is a directory... }
18 Destination := Destination + '\' + ExtractFileName(FileName);
19 { ...clone file name }
20 TimeStamp := FileAge(FileName); { get source's time stamp }
21 GetMem(CopyBuffer, ChunkSize); { allocate the buffer }
22 try
23 Source := FileOpen(FileName, fmShareDenyWrite); { open source file }
24 if Source < 0 then
25 raise EFOpenError.Create(FmtLoadStr(SFOpenError, [FileName]));
26 try
27 Dest := FileCreate(Destination); { create output file; overwrite existing }
28 if Dest < 0 then
29 raise EFCreateError.Create(FmtLoadStr(SFCreateError, [Destination]));
30 try
31 repeat
32 BytesCopied := FileRead(Source, CopyBuffer^, ChunkSize); { read chunk }
33 if BytesCopied > 0 then { if we read anything... }
34 FileWrite(Dest, CopyBuffer^, BytesCopied); { ...write chunk }
35 until BytesCopied < ChunkSize; { until we run out of chunks }
36 finally
37 FileClose(Dest); { close the destination file }
38 end;
39 finally
40 FileClose(Source); { close the source file }
41 end;
42 finally
43 FreeMem(CopyBuffer, ChunkSize); { free the buffer }
44 end;
45 end;
But Delphi implements a method of TStream called CopyFrom that allows you to copy
the entire contents of one stream into another in one fell swoop. Here's an
implementation of copying a file using the CopyFrom method:
46
47 {=============================================================
48 Quick and dirty copying using the CopyFrom method of TStream.
49 =============================================================}
50
51 procedure FileCopy(const FSrc, FDst: string);
52 var
53 sStream,
54 dStream: TFileStream;
55 begin
56 sStream := TFileStream.Create(FSrc, fmOpenRead);
57 try
58 dStream := TFileStream.Create(FDst, fmCreate);
59 try
60 {Forget about block reads and writes, just copy
61 the whole darn thing.}
62 dStream.CopyFrom(sStream, 0);
63 finally
64 dStream.Free;
65 end;
66 finally
67 sStream.Free;
68 end;
69 end;
The declaration of the CopyFrom method is as follows:
function CopyFrom(Source: TStream; Count: LongInt): LongInt;
Source is the TStream you're going to copy from, and Count is the number of bytes
to copy from the stream. If Count is zero (0), the entire contents of the source
stream is copied over. This makes for a quick one-liner copying.
Notice that in both the examples above, all the functionality is enclosed in nested
try..finally blocks. This is extremely important because just in case something
goes wrong, all resources and pointers that are created are freed. You don't want
to have stray pointers or unreleased memory in your system, so providing at least
this level of exception handling is key to ensuring that you don't.
A Sexier File Copy
If you write robust user interfaces, practically everything that you do involves
interacting with the user by providing visual cues to let the user know what's
going on. File copying is one of those types of operations that when performed
within the context of a user interface must provide some status as to the progress
of the copy operation. Therefore, a quick and dirty copy like the one I just
described above won't do. What we need then is something with a bit more pizazz.
In order to get status, we need to copy the file in chunks. That way, as we copy
each chunk from one file to another, we can let the user know how far we've
proceeded. What this implies is that we need two pieces. The first is the unit that
performs the copying; the other a status window used for notification. For me, the
best way to get both pieces to work in concert was to build a custom component
which encapsulates the file copy operation and uses another unit to perform the
notification.
The notification unit is just a simple form with a TGauge and a TButton placed on
it. The unit code is as follows:
70 unit copyprg;
71
72 interface
73
74 uses
75 Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs,
76 StdCtrls, Gauges;
77
78 type
79 TFileProg = class(TForm)
80 Gauge1: TGauge;
81 Button1: TButton;
82 procedure Button1Click(Sender: TObject);
83 procedure FormCreate(Sender: TObject);
84 private
85 { Private declarations }
86 fCancel: Boolean;
87 public
88 property CancelIt: Boolean read fCancel;
89 end;
90
91 var
92 FileProg: TFileProg;
93
94 implementation
95
96 {$R *.DFM}
97
98 procedure TFileProg.Button1Click(Sender: TObject);
99 begin
100 fCancel := True;
101 end;
102
103 procedure TFileProg.FormCreate(Sender: TObject);
104 begin
105 fCancel := False;
106 end;
107
108 end.
Nothing odd here. I simply added a custom property to the form called CancelIt,
which is a simple Boolean flag used to cancel the copying operation midstream
should the user desire to do so. The real work happens in the custom component
itself. Let's look at its code, then discuss it:
109 unit FileCopy;
110
111 interface
112
113 uses
114 Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs;
115
116 type
117 TFileCopy = class(TComponent)
118 private
119 FSource,
120 FDest: string;
121 procedure DoCopyFile(const SrcFile, DstFile: string);
122 public
123 procedure CopyFile; virtual;
124 published
125 property FileSource: string read FSource write FSource;
126 property FileDestination: string read FDest write FDest;
127 end;
128
129 procedure register;
130
131 implementation
132
133 uses copyprg;
134
135 procedure TFileCopy.CopyFile;
136 begin
137 DoCopyFile(FileSource, FileDestination);
138 end;
139
140 procedure TFileCopy.DoCopyFile(const SrcFile, DstFile: string);
141 const
142 bufSize = 16384; {Use a 16K buffer. You can use whatever size suits you, though.}
143 var
144 sStream,
145 dStream: TFileStream;
146 pBuf: Pointer;
147 cnt: Integer;
148 prgFrm: TFileProg;
149 totCnt,
150 X,
151 strmSize: LongInt;
152 begin
153 totCnt := 0;
154 {Open up the Source File to read it}
155 sStream := TFileStream.Create(SrcFile, fmOpenRead or fmShareDenyWrite);
156
157 {Create the copying progress form and set property values}
158 prgFrm := TFileProg.Create(Application);
159 with prgFrm.Gauge1 do
160 begin
161 MinValue := 0;
162 MaxValue := 100;
163 Progress := 0;
164 end;
165 prgFrm.Show;
166
167 {Get the size of the entire stream to use for the progress gauge. Note
168 we have to call FileSeek first because it will place the pointer
169 at the end of the file when we get the file first return value.}
170 strmSize := sStream.size;
171
172 try
173 { Create the destination file. If it already exists,
174 overwrite it. }
175 dStream := TFileStream.Create(DstFile, fmCreate or fmShareExclusive);
176 try
177 GetMem(pBuf, bufSize);
178 try
179 {Read and write first bufSize bytes from source into the buffer
180 If the file size is smaller than the default buffer size, then
181 all the user will see is a quick flash of the progress form.}
182 cnt := sStream.read(pBuf^, bufSize);
183 cnt := dStream.write(pBuf^, cnt);
184
185 totCnt := totCnt + cnt;
186 {Loop the process of reading and writing}
187 while (cnt > 0) do
188 begin
189 {Let things in the background proceed while loop is processing}
190 Application.ProcessMessages;
191
192 {Read bufSize bytes from source into the buffer}
193 cnt := sStream.read(pBuf^, bufSize);
194
195 {Now write those bytes into destination}
196 cnt := dStream.write(pBuf^, cnt);
197
198 {Increment totCnt for progress and do arithmetic to update the
199 gauge}
200 totcnt := totcnt + cnt;
201 if not prgFrm.CancelIt then
202 with prgFrm.Gauge1 do
203 begin
204 Progress := Round((totCnt / strmSize) * 100);
205 Update;
206 end
207 else
208 Break; {If user presses cancel button, then break out of loop}
209 {which will make program go to finally blocks}
210 end;
211
212 finally
213 FreeMem(pBuf, bufSize);
214 end;
215 finally
216 dStream.Free;
217 if prgFrm.CancelIt then {If copying was cancelled, delete the destination
218 file}
219 DeleteFile(DstFile); {after stream has been freed, which will close the
220 file.}
221 end;
222 finally
223 sStream.Free;
224 prgFrm.Close;
225 end;
226 end;
227
228 procedure register;
229 begin
230 {You can change the palette entry to something of your choice}
231 RegisterComponents('BD', [TFileCopy]);
232 end;
233
234 end.
Like the CopyFile routine in FMXUTILS.PAS, the concept behind copying for this
component is the same: Grab a chunk of the source file, then dump it into the
destination file. Repeat this process until all possible data has been copied over.
Notice that I used a TFileStream once again. But this time, I didn't copy the
entire file over in one fell swoop. That would've defeated the whole purpose of
providing user status.
I've commented the code extensively, so I won't go into real detail here. I'll
leave it up to you to study the code to learn about what's going on in it.
Notice the method declaration for CopyFile is declared as a virtual method. I've
done this on purpose so that this class can be used a template class for
specialized copy operations. The CopyFile method is actually rather trivial at this
level -- all it does is call the DoCopyFile method and pass the FileSource and
FileDestination property values.
However, it is the only public interface for actually performing the copying
operation. This is an important point for all you component designers out there.
Providing limited method visibility ensures that the core features of your
components remain intact. Remember, you want other users of your component to see
only what is absolutely necessary.
How is this useful? It allows you to have a bit of control over how the hierarchy
develops. By hiding the basic functionality from descendant classes, you can ensure
that the basic functionality of your class is retained throughout the inheritance
tree. Granted, users can completely override the behavior of the CopyFile method,
but that doesn't mean that the original capability will be lost. It will still be
there, just not implemented.
Obviously, the meat of the work is performed by the DoCopyFile method. Study the
code to see what happens from point to point. Note that I used a Pointer for the
buffer. You can use just about any type as a buffer, but a pointer makes sense
because its a simple 4-byte value. If you are copying a text file and want to treat
the pointer like a string, you can cast it as a PChar, so long as you append a #0
byte to the end of the buffer. Neat stuff, huh?
A Little Note About TFileStream
TFileStream is not a direct assignment of TStream. In fact, it's a descendant of
THandleStream which, when created, fills a property value called Handle which is
the handle to an external file. TFileStream inherits the Handle property. The
significance of this is really neat: File operations that take a handle as input
can be applied to a TFileStream. That has interesting implications in that you can
do file operations on a TFileStream object before you write it to another place.
Try experimenting with this.
Okay, we've come a long way. And no, I haven't delved into the depths of Stream classes. That's probably best left to another article or series of articles. In any case, play around with the TCopyFile class. It could prove to be a useful addition to your applications.
|