The TImageCache Component


The TImageCache component is a TComponent descendant that can save and load multiple images to a single file. It's a pretty interesting component because it shows how to use the TFileStream and TMemoryStream classes to work with files and memory, and how to use the TList class to maintain a list of objects in memory.

The TList Class

The TList class is a very powerful and very addictive class that is declared in the Classes unit and enables you to manage a list of pointers in memory. Its addictiveness arises from the fact that it can be used to store anything you want: objects, records, integers, strings, and even methods. For instance, the following code adds a pointer to the ShowMessage procedure to a TList and then calls the ShowMessage procedure through the pointer stored in the list:

procedure TForm1.Button1Click(Sender: TObject); var   Lst: TList; type   TStringProc = procedure(const S: string); begin   Lst := TList.Create;   try     Lst.Add(@ShowMessage);     TStringProc(Lst[0])('An awkward Hello!');   finally     Lst.Free;   end;       // try..finally end;

Creating the TImageCache Component

Now, open your component package and run the New VCL Component wizard to derive the TImageCache component from TComponent. Save the component as image from book ImageCache.pas and add it to the opened package.

To give the TImageCache component the ability to manage a list of bitmaps, we have to use a private TList instance that will hold the bitmap images and we have to surface these images through an array property. The component should also have the following methods:

  • One or more methods for adding images to the list

  • A Delete method for removing a single image from the list

  • A Clear method for releasing all images from memory once they are no longer needed

First, we have to override the component's constructor and destructor to create and destroy the TList instance:

unit ImageCache; interface uses   SysUtils, Classes; type   TImageCache = class(TComponent)   private     { Private declarations }     FImages: TList;   protected     { Protected declarations }   public     { Public declarations }     constructor Create(AOwner: TComponent); override;     destructor Destroy; override;   published     { Published declarations }   end; procedure Register; implementation procedure Register; begin   RegisterComponents('My Components', [TImageCache]); end; constructor TImageCache.Create(AOwner: TComponent); begin   inherited Create(AOwner);   FImages := TList.Create; end; destructor TImageCache.Destroy; begin   FImages.Free;   inherited Destroy; end; end. 

To surface the images stored in the FImages list, we have to create a read-only array property called Images. Since this property has to return a TBitmap, add the Graphics unit to the uses list.

The read method of this property should check whether the Index parameter is in the valid range (0 -> FImages.Count – 1) and typecast the stored pointer to a TBitmap when returning the image:

type   TImageCache = class(TComponent)   protected     { Protected declarations }     function GetImage(Index: Integer): TBitmap; virtual;   public     { Public declarations }    property Images[Index: Integer]: TBitmap read GetImage; default;   published     { Published declarations }   end; ... function TImageCache.GetImage(Index: Integer): TBitmap; begin   if (Index >= 0) and (Index <= Pred(FImages.Count)) then     Result := TBitmap(FImages[Index])   else     Result := nil; end; 

To make the component more user-friendly, let's create the following three overloaded Add methods:

  • One method should accept a TBitmap parameter to enable the programmer to add an already loaded bitmap to the list.

  • The second method should accept a string parameter to enable the programmer to load a bitmap from file and add it to the list.

  • The final method should accept a TStrings parameter to allow the programmer to add a larger number of images to the list.

Here are the three overloaded public Add methods:

type   TImageCache = class(TComponent)   public     { Public declarations }     procedure Add(ABitmap: TBitmap); overload;     procedure Add(const AFileName: string); overload;     procedure Add(AStrings: TStrings); overload; end; ... procedure TImageCache.Add(ABitmap: TBitmap); begin   FImages.Add(ABitmap); end; procedure TImageCache.Add(const AFileName: string); var   B: TBitmap; begin   B := TBitmap.Create;   B.LoadFromFile(AFileName);   FImages.Add(B); end; procedure TImageCache.Add(AStrings: TStrings); var   item: string; begin   for item in AStrings do     Add(item); end;

Now that we have the ability to add images to the list, we should also create the methods that remove these images from memory. To delete an image from memory, you cannot just call the TList's Delete method because that would only delete the pointer to the TBitmap in memory. You should first typecast the pointer to a bitmap and call Free to delete the image, and only then call the TList's Delete method to remove the pointer from the list. For the Clear method, which needs to remove all bitmaps from memory, you don't have to call the TList's Delete method to remove every bitmap's pointer from the list. You should first delete all images in a loop and then call the TList's Clear method to remove all pointers from the list.

Here are the Clear and Delete methods:

type   TImageCache = class(TComponent)   protected     { Protected declarations }     procedure Clear;     procedure Delete(Index: Integer); virtual; end; ... procedure TImageCache.Delete(Index: Integer); begin   if (Index >= 0) and (Index <= Pred(FImages.Count)) then   begin     TBitmap(FImages[Index]).Free;     FImages.Delete(Index);   end;       // if Index end; procedure TImageCache.Clear; var   cnt: Integer;   bmp: TBitmap; begin   { first delete all TBitmaps }   for cnt := 0 to Pred(FImages.Count) do   begin     bmp := TBitmap(FImages[cnt]);     bmp.Free;   end;   { remove all pointers from the list since they now point     to invalid memory locations }   FImages.Clear; end;

Now that we have the Clear method, we must update the component's destructor and first call the Clear method to remove the images from memory when the component is destroyed:

destructor TImageCache.Destroy; begin   Clear;   FImages.Free;   inherited Destroy; end;

Saving and Loading Images

The most important methods of the TImageCache class are the SaveToFile and LoadFromFile methods that enable us to store images to and load images from a cache file. Let's start by creating the SaveToFile method.

Here's how the TImageCache component stores multiple images to a single file:

  • First, the SaveToFile method needs to instantiate the TFileStream class to create the destination file.

  • The first piece of data that is written to the file is the number of images that exist in the list (this helps us read the images from the file more easily).

  • To save the image from the list to the file and to read the image's size, the SaveToFile method copies the bitmap to a TMemoryStream.

  • For every image, the SaveToFile method stores two values: the bitmap's size and the bitmap itself.

The following listing shows the TImageCache component's SaveToFile method.

Listing 26-4: The SaveToFile method

image from book
type   TImageCache = class(TComponent)   public     { Public declarations }     procedure SaveToFile(const AFileName: string); end; ... procedure TImageCache.SaveToFile(const AFileName: string); var   cnt: Integer;   fs: TFileStream;   buffer: TMemoryStream;   imgSize: Integer;   imgCount: Integer; begin   fs := TFileStream.Create(AFileName, fmCreate);   try     { create the TMemoryStream before the loop because there's no need       to create a new instance of the TMemoryStream for every image }     buffer := TMemoryStream.Create;     try       { write image count }       imgCount := FImages.Count;       fs.Write(imgCount, SizeOf(Integer));       for cnt := 0 to Pred(FImages.Count) do       begin         { save the image to the memory stream, but first call Clear           to remove the old image and to reset the stream's position }         buffer.Clear;         TBitmap(FImages[cnt]).SaveToStream(buffer);         { write the bitmap's size to the file }         imgSize := buffer.Size;         fs.Write(imgSize, SizeOf(Integer));         { finally, write the image to the file }         fs.CopyFrom(buffer, 0);       end;        // for cnt     finally       buffer.Free;     end;         // try..finally (TMemoryStream.Create)   finally     fs.Free;   end;          // try..finally (TFileStream.Create) end;
image from book

To implement the LoadFromFile method, you have to do the opposite of what you did in the SaveToFile method (and a bit more):

  • First, you have to call the Clear method to remove the existing images from memory.

  • Instantiate the TFileStream to access the file.

  • Read the number of images stored in the file.

  • Use the read image count in a loop to read all images.

When loading the image from the file, you have to do the following:

  • Read the size of the bitmap stored in the file by the SaveToFile method.

  • Call the Clear method of the memory stream to remove the old data and to restore the stream's Position to 0.

  • Load the image to the memory stream.

  • To read the entire image using the TBitmap.LoadFromStream method, you must set the stream's Position to 0 (after you load the image into the memory stream, the Position property points to the end of the stream, and this is not good because the TBitmap's LoadFromStream method reads the stream's data from Position to the stream's end).

  • After resetting the stream's Position, create a new TBitmap instance and call its LoadFromStream method to load the image from the memory stream into the TBitmap.

  • Finally, call the TImageCache's Add method and pass it the new TBitmap to add it to the list.

The following listing shows the TImageCache component's LoadFromFile method.

Listing 26-5: The LoadFromFile method

image from book
type   TImageCache = class(TComponent)   public     { Public declarations }     procedure LoadFromFile(const AFileName: string); end; ... procedure TImageCache.LoadFromFile(const AFileName: string); var   fs: TFileStream;   imgCount: Integer;   buffer: TMemoryStream;   imgSize: Integer;   cnt: Integer;   bmp: TBitmap; begin   Clear; { remove existing data from memory }   { pass fmOpenRead if you only want to open the file }   fs := TFileStream.Create(AFileName, fmOpenRead);   try     { create the TMemoryStream helper object that will load images from       the file to a memory stream that can then be used by the TBitmap }     buffer := TMemoryStream.Create;     try       { read the number of bitmaps stored in the file }       fs.Read(imgCount, SizeOf(Integer));       { now that you have the image count, read them from the file }       for cnt := 1 to imgCount do       begin         { first read the size of the bitmap }         fs.Read(imgSize, SizeOf(Integer));         { now read imgSize bytes to the memory stream }         buffer.Clear;         buffer.CopyFrom(fs, imgSize);         { reset the buffer's position before calling LoadFromStream }         buffer.Position := 0;         { finally, create the image }         bmp := TBitmap.Create;         bmp.LoadFromStream(buffer);         FImages.Add(bmp);       end;        // for cnt     finally       buffer.Free;     end;         // try..finally (TMemoryStream.Create)   finally     fs.Free;   end;          // try..finally (TFileStream.Create) end;
image from book

Final Touches

To complete the TImageCache component, we have to create two public properties and an event. In order to use the TImageCache component in an application, we need a Count property to tell us how many images there are in the list and an ImageIndex property to tell us which one is currently being used (displayed) by the client application.

To create the Count property, you only have to surface the Count property of the FImages list:

type   TImageCache = class(TComponent)   protected     { Protected declarations }     function GetImageCount: Integer; virtual;   public     { Public declarations }     property Count: Integer read GetImageCount; end; ... function TImageCache.GetImageCount: Integer; begin   Result := FImages.Count; end; 

The ImageIndex property is an integer property with a write method that makes sure we can't enter a value that's outside the 0 to FImages.Count – 1 range. Here's the implementation of the ImageIndex property:

type   TImageCache = class(TComponent)   private     { Private declarations }     FImageIndex: Integer;   protected     { Protected declarations }     procedure SetImageIndex(Value: Integer); virtual;   public     { Public declarations }     property ImageIndex: Integer read FImageIndex write SetImageIndex; end; ... procedure TImageCache.SetImageIndex(Value: Integer); begin   if Value < 0 then     FImageIndex := 0   else if Value > Pred(FImages.Count) then     FImageIndex := Pred(FImages.Count)   else     FImageIndex := Value; end;

Now that you have the ImageIndex property, you should update the LoadFromFile method to have it "select" the first image in the cache by setting the FImageIndex field to 0 at the end of the method:

procedure TImageCache.LoadFromFile(const AFileName: string); ...   finally     fs.Free;   end;       // try..finally (TFileStream.Create)   FImageIndex := 0; { "select" the first image } end;

Finally, to justify the creation of a component, we need to create the OnRead event, which should occur every time an image is accessed. To correctly implement the OnRead event, you have to update the implementation of the GetImage read method to have it fire the OnRead event if the user passes a valid bitmap index. The following code shows how to implement the OnRead event:

type   TImageCache = class(TComponent)   private     { Private declarations }     FOnRead: TNotifyEvent;   protected     { Protected declarations }     procedure DoRead; virtual;   published     { Published declarations }     property OnRead: TNotifyEvent read FOnRead write FOnRead;   end; ... function TImageCache.GetImage(Index: Integer): TBitmap; begin   if (Index >= 0) and (Index <= Pred(FImages.Count)) then   begin     Result := TBitmap(FImages[Index]);     DoRead; { fire the OnRead event }   end else     Result := nil; end; procedure TImageCache.DoRead; begin   if Assigned(FOnRead) then FOnRead(Self); end;

Testing the Component

The easiest way to test the TImageCache component is to create two simple applications: an editor application to create the cache files and a viewer application that can view the images stored in a cache file.

To create a simple editor application (see Figure 26-7), you only have to do the following:

  1. Use the TOpenDialog component to enable the user to add bitmaps to a TListBox.

  2. Pass the TListBox component's Items to the Add method of the TImageCache component to load the images.

  3. Call the TImageCache component's SaveToFile method to save the selected images to a cache file.

image from book
Figure 26-7: The Cache Editor

Here's the complete source code of the Cache Editor application:

procedure TMainForm.CreateCacheButtonClick(Sender: TObject); begin   if SaveDialog1.Execute then   begin     ImageCache.Add(ListBox1.Items);     ImageCache.SaveToFile(SaveDialog1.FileName);     MessageDlg('Cache created', mtInformation, [mbOK], 0);   end; end; procedure TMainForm.AddImagesButtonClick(Sender: TObject); begin   if OpenDialog1.Execute then     ListBox1.Items.AddStrings(OpenDialog1.Files); end;

To create a simple viewer application (see Figure 26-8), you need to drop three buttons on the Designer Surface, use the LoadFromFile method to load the images from the cache file, and use the Images and ImageIndex properties to browse the cache.

image from book
Figure 26-8: The Cache Viewer

Here's the complete source code of the Cache Viewer application:

procedure TMainForm.LoadButtonClick(Sender: TObject); begin   if OpenDialog1.Execute then   with ImageCache do   begin     LoadFromFile(OpenDialog1.FileName);     { the following line fires the OnRead event }     Image1.Picture.Bitmap := Images[ImageIndex];   end; end; procedure TMainForm.PrevButtonClick(Sender: TObject); begin   with ImageCache do   begin     ImageIndex := ImageIndex - 1;     { the following line fires the OnRead event }     Image1.Picture.Bitmap := Images[ImageIndex];   end; end; procedure TMainForm.NextButtonClick(Sender: TObject); begin   with ImageCache do   begin     ImageIndex := ImageIndex + 1;     { the following line fires the OnRead event }     Image1.Picture.Bitmap := Images[ImageIndex];   end; end; { this is the TImageCache component's OnRead event handler } procedure TMainForm.ImageCacheRead(Sender: TObject); const   STATUS = 'Viewing image %d of %d'; begin   with ImageCache do    StatusBar1.SimpleText := Format(STATUS, [Succ(ImageIndex), Count]); end;



Inside Delphi 2006
Inside Delphi 2006 (Wordware Delphi Developers Library)
ISBN: 1598220039
EAN: 2147483647
Year: 2004
Pages: 212
Authors: Ivan Hladni

Similar book on Amazon

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net