2007. február 18., vasárnap
Copying Files in Delphi
Problem/Question/Abstract:
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:
{=============================================================================
CopyFile procedure found in the FMXUTILS.PAS file in Delphi\Demos\Doc\Filmanex
This is an example of copying a file using a buffer.
=============================================================================}
procedure CopyFile(const FileName, DestName: TFileName);
var
CopyBuffer: Pointer; { buffer for copying }
TimeStamp, BytesCopied: Longint;
Source, Dest: Integer; { handles }
Destination: TFileName; { holder for expanded destination name }
const
ChunkSize: Longint = 8192; { copy in 8K chunks }
begin
Destination := ExpandFileName(DestName); { expand the destination path }
if HasAttr(Destination, faDirectory) then { if destination is a directory... }
Destination := Destination + '\' + ExtractFileName(FileName);
{ ...clone file name }
TimeStamp := FileAge(FileName); { get source's time stamp }
GetMem(CopyBuffer, ChunkSize); { allocate the buffer }
try
Source := FileOpen(FileName, fmShareDenyWrite); { open source file }
if Source < 0 then
raise EFOpenError.Create(FmtLoadStr(SFOpenError, [FileName]));
try
Dest := FileCreate(Destination); { create output file; overwrite existing }
if Dest < 0 then
raise EFCreateError.Create(FmtLoadStr(SFCreateError, [Destination]));
try
repeat
BytesCopied := FileRead(Source, CopyBuffer^, ChunkSize); { read chunk }
if BytesCopied > 0 then { if we read anything... }
FileWrite(Dest, CopyBuffer^, BytesCopied); { ...write chunk }
until BytesCopied < ChunkSize; { until we run out of chunks }
finally
FileClose(Dest); { close the destination file }
end;
finally
FileClose(Source); { close the source file }
end;
finally
FreeMem(CopyBuffer, ChunkSize); { free the buffer }
end;
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:
{=============================================================
Quick and dirty copying using the CopyFrom method of TStream.
=============================================================}
procedure FileCopy(const FSrc, FDst: string);
var
sStream,
dStream: TFileStream;
begin
sStream := TFileStream.Create(FSrc, fmOpenRead);
try
dStream := TFileStream.Create(FDst, fmCreate);
try
{Forget about block reads and writes, just copy
the whole darn thing.}
dStream.CopyFrom(sStream, 0);
finally
dStream.Free;
end;
finally
sStream.Free;
end;
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:
unit copyprg;
interface
uses
Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs,
StdCtrls, Gauges;
type
TFileProg = class(TForm)
Gauge1: TGauge;
Button1: TButton;
procedure Button1Click(Sender: TObject);
procedure FormCreate(Sender: TObject);
private
{ Private declarations }
fCancel: Boolean;
public
property CancelIt: Boolean read fCancel;
end;
var
FileProg: TFileProg;
implementation
{$R *.DFM}
procedure TFileProg.Button1Click(Sender: TObject);
begin
fCancel := True;
end;
procedure TFileProg.FormCreate(Sender: TObject);
begin
fCancel := False;
end;
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:
unit FileCopy;
interface
uses
Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs;
type
TFileCopy = class(TComponent)
private
FSource,
FDest: string;
procedure DoCopyFile(const SrcFile, DstFile: string);
public
procedure CopyFile; virtual;
published
property FileSource: string read FSource write FSource;
property FileDestination: string read FDest write FDest;
end;
procedure Register;
implementation
uses copyprg;
procedure TFileCopy.CopyFile;
begin
DoCopyFile(FileSource, FileDestination);
end;
procedure TFileCopy.DoCopyFile(const SrcFile, DstFile: string);
const
bufSize = 16384; {Use a 16K buffer. You can use whatever size suits you, though.}
var
sStream,
dStream: TFileStream;
pBuf: Pointer;
cnt: Integer;
prgFrm: TFileProg;
totCnt,
X,
strmSize: LongInt;
begin
totCnt := 0;
{Open up the Source File to read it}
sStream := TFileStream.Create(SrcFile, fmOpenRead or fmShareDenyWrite);
{Create the copying progress form and set property values}
prgFrm := TFileProg.Create(Application);
with prgFrm.Gauge1 do
begin
MinValue := 0;
MaxValue := 100;
Progress := 0;
end;
prgFrm.Show;
{Get the size of the entire stream to use for the progress gauge. Note
we have to call FileSeek first because it will place the pointer
at the end of the file when we get the file first return value.}
strmSize := sStream.size;
try
{ Create the destination file. If it already exists,
overwrite it. }
dStream := TFileStream.Create(DstFile, fmCreate or fmShareExclusive);
try
GetMem(pBuf, bufSize);
try
{Read and write first bufSize bytes from source into the buffer
If the file size is smaller than the default buffer size, then
all the user will see is a quick flash of the progress form.}
cnt := sStream.Read(pBuf^, bufSize);
cnt := dStream.Write(pBuf^, cnt);
totCnt := totCnt + cnt;
{Loop the process of reading and writing}
while (cnt > 0) do
begin
{Let things in the background proceed while loop is processing}
Application.ProcessMessages;
{Read bufSize bytes from source into the buffer}
cnt := sStream.Read(pBuf^, bufSize);
{Now write those bytes into destination}
cnt := dStream.Write(pBuf^, cnt);
{Increment totCnt for progress and do arithmetic to update the
gauge}
totcnt := totcnt + cnt;
if not prgFrm.CancelIt then
with prgFrm.Gauge1 do
begin
Progress := Round((totCnt / strmSize) * 100);
Update;
end
else
Break; {If user presses cancel button, then break out of loop}
{which will make program go to finally blocks}
end;
finally
FreeMem(pBuf, bufSize);
end;
finally
dStream.Free;
if prgFrm.CancelIt then {If copying was cancelled, delete the destination file}
DeleteFile(DstFile); {after stream has been freed, which will close the file.}
end;
finally
sStream.Free;
prgFrm.Close;
end;
end;
procedure Register;
begin
{You can change the palette entry to something of your choice}
RegisterComponents('BD', [TFileCopy]);
end;
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.
Feliratkozás:
Megjegyzések küldése (Atom)
Nincsenek megjegyzések:
Megjegyzés küldése