1

I am using Delphi XE3 and want to implement Windows Thumbnail style to show a list of images via TListView control.

What I need is like below:

enter image description here

The images are displayed as thumbnail style, there is a caption below each image. And when I click the image, the image together with the caption will be shown as selected...

To improve the performance, I do not want to load all the images into an image list beforehand, instead, I want to load the image when it is to be displayed. Therefore, I am thinking of using OnCustomDrawItem and OnAdvancedCustomDrawItem.

Below is a very simple version of my plan(I set the style of the list view to vsIcon):

    procedure TForm1.FormCreate(Sender: TObject);
    var
      ListItem1: TListItem;
    begin
      ListItem1 := ListView1.Items.Add;

      ListItem1.Caption := 'Chrysanthemum';
    end;

    procedure TForm1.ListView1AdvancedCustomDrawItem(Sender: TCustomListView;
      Item: TListItem; State: TCustomDrawState; Stage: TCustomDrawStage;
      var DefaultDraw: Boolean);
    var
      JPEG: TJPEGImage;
      R: TRect;
    begin
    {
      R := Item.DisplayRect(drBounds);

      JPEG := TJPEGImage.Create;

      JPEG.LoadFromFile('C:\Users\Public\Pictures\Sample Pictures\Chrysanthemum (2).jpg');

      Sender.Canvas.StretchDraw(R, JPEG);
    }
    end;

    procedure TForm1.ListView1CustomDrawItem(Sender: TCustomListView;
      Item: TListItem; State: TCustomDrawState; var DefaultDraw: Boolean);
    var
      JPEG: TJPEGImage;
      R: TRect;
    begin
      R := Item.DisplayRect(drBounds);

      JPEG := TJPEGImage.Create;

      JPEG.LoadFromFile('C:\Users\Public\Pictures\Sample Pictures\Chrysanthemum (2).jpg');

      Sender.Canvas.StretchDraw(R, JPEG);
    end;

But the result is not satisfactory, as follows:

  1. I cannot find a way to set the size of each icon. (All icon will have the same size).

  2. I try to put the codes in OnCustomDrawItem and OnAdvancedCustomDrawItem. I cannot figure out much differences between these twos. The only main difference that in Advancedxxx version, the caption is editable. I cannot understand why.

  3. The caption is not displayed under the image, instead, it is in the middle of the image, that is not desired. How to fix that?

Thanks

alancc
  • 487
  • 2
  • 24
  • 68
  • Take a look at the documentation for `OnDrawItem()` event. There's a note that says: *Note: List views receive several other custom draw events, including OnCustomDraw, OnCustomDrawItem, OnCustomDrawSubItem, OnAdvancedCustomDraw, OnAdvancedCustomDrawItem, and OnAdvancedCustomDrawSubItem. These other events, unlike OnDrawItem, occur regardless of the OwnerDraw property's value. ...* So, use any of these other events. – Tom Brunberg Oct 28 '18 at 06:48
  • 1
    If you want to display lots of images in `TListView` consider use it in virtual mode setting `OwnerData` property to `true`. [Example of owner-drawing items in TListView](https://stackoverflow.com/a/52371393/5581243). – Josef Švejk Oct 28 '18 at 16:41
  • @Dima Is there any complete sample code on TListView owner drawing? The link you provided uses AdvancedCustomDrawItem, I check the document http://docs.embarcadero.com/products/rad_studio/delphiAndcpp2009/HelpUpdate2/EN/html/delphivclwin32/ComCtrls_TListView_OnCustomDrawItem.html and it said "To augment the default drawing process at other stages (such as after the list item is drawn), use the OnAdvancedCustomDrawItem event instead." So it seems that the example uses the wrong event handler? – alancc Oct 29 '18 at 09:43
  • No, example is correct. If you will use `OnCustomDrawItem` event you should draw item from scratch, setting `DefaultDraw` to `false`, otherwise you will have default drawing done over your drawing. `OnAdvancedCustomDrawItem` event allows you to have a control over paint stages `cdPrePaint` and `cdPostPaint`. In my opinion it is more elegant way to owner-draw `TListView` and for that simple example it was enough. But yes if you will do heavy-paint things in drawing events, it is better to use `OnCustomDrawItem` event as it occurs *prior* actual item drawing. What to use depends on your needs. – Josef Švejk Oct 29 '18 at 10:34
  • @Dima, I have just modified my question to include my test. I try to use OnCustomDrawItem and OnAdvancedCustomDrawItem and both keep DefaultDraw to true. The result is a bit different but I cannot understand why. Also there are some more problems with my test. Thank you very much. – alancc Oct 30 '18 at 09:22
  • Do you want to have *owner-drawn* (with custom colors of selected items, text etc.) or *themed* `TListView` (that will looks probably as the same as on your screenshot)? – Josef Švejk Oct 30 '18 at 10:02
  • @Dima At the minimum, I just want a thumbnail listview that will allow me to draw image when needed instead of using a image list, and put a caption under the image, as well as the function of selecting the image together with the caption. Of course if possible, I want to make the thumbnail more beautiful(with the same look and feel as my screenshot, which is just from Windows 7 explorer window). – alancc Oct 30 '18 at 11:18
  • I think the images should be stored somewhere. `TImageList` is possible storage for your task. Plus you should store names of files (to paint them under images). Thus, the only solution I can advise: create your *custom* storage that will act like `TImageList` with names of each image in it. Then you run `TListView` in virtual mode and assign to each item of `TLisView` related item from your storage. Perhaps, there is more elegant way to load image "on-the-fly", but I can't imagine which one. – Josef Švejk Oct 30 '18 at 11:30
  • @Dima, The main problem with TImageList is that I have 10 thousands of images, loading all of them into TImageList will consume a lot of memory and it unnecessary since the user will only see 10 to 20 images at a time. So I want to maintaince an internal list containing the full paths of all the images, and an internal cache to cache about 50 images at the most. When user browse to a page(I just divide all the images into several pages to simplify the description), I will determine whether the images in the page is in the cache, if yes, the just load it from cache, if not, then load from disk. – alancc Oct 30 '18 at 11:45
  • @Dima, or, if load-on-the-fly is fast enough and user does not care about the loading delay, then I will deliminate the cache and just store the image file names in memory. – alancc Oct 30 '18 at 11:47
  • 1
    I recommend you to look at [this component](https://stackoverflow.com/a/8954813/5581243). It can displays images as standard `TListView` (but it has not all capabilities of the latter). It caches images after you specify folder to search them in it. This thing is worth to try. Probably for your task it could be more useful then dances with standard `TListView`. – Josef Švejk Oct 30 '18 at 13:37
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/182830/discussion-between-alancc-and-dima). – alancc Oct 30 '18 at 23:14
  • @Dima, It seems virtual list view is the best appropriate for my case. Thank you very much. – alancc Nov 01 '18 at 09:57
  • In the past I have used [Mustangpeak VirtualShellTools](https://github.com/pyscripter/MustangpeakVirtualShellTools) and they even had previews for office and pdf documents. This is acchived by implementing [IPreviewHandlerFrame](https://learn.microsoft.com/en-us/windows/desktop/shell/preview-handlers) interface and doing other things, that windows explorer does. – Torbins Oct 30 '18 at 15:11

1 Answers1

5

The attached code loads the images (in this case icons) into the TImageList that is assigned to the LargeImages property of the TListView only when the associated icon actually gets displayed in the listview. The main thing is to set the OwnerData property of the listview to TRUE and to create an event handler for OnData events. Parallel to the items in the listview the program maintains a list of the items in the listview that is in sync with the actual list in the listview, in this case a TStringList. In its Objects property I store the index of the associated icon resource if that has already been loaded and added to the TImageList. If the icon resource has not been loaded, this happens in the LoadIconFromFile function and the index of the icon in the TImageList gets stored in the TStringList.

The actual drawing of the icons and the text in the TListView is fully handled by the control itself, the code does neither handle OnDraw nor any OnCustomDraw* events. Just set the image size in the TImageList to the size of the bitmaps you want to display and create them accordingly.

Older Delphi versions contain a sample project called "VirtualListView.dpr" that is quite helpful to understand when the various OnData* events get fired and how to properly use them.

unit MainFormU;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, ImgList, StdCtrls, ComCtrls;

type
  TForm1 = class(TForm)
    Icons_LV: TListView;
    Label1: TLabel;
    Large_IL: TImageList;
    procedure Icons_LVData(Sender: TObject; Item: TListItem);
    procedure FormCreate(Sender: TObject);
    procedure FormDestroy(Sender: TObject);
  private
    FileList : TStringList;

    procedure FillListView;
    function LoadIconFromFile (const sFileName: String;
                               out iIndex: Integer) : Boolean;
  end;

var Form1 : TForm1;

implementation

{$R *.dfm}

uses ShellApi;

const
  cWinSysDir = 'c:\windows\system32\';

procedure TForm1.FormCreate (Sender: TObject);
begin
  FileList := TStringList.Create;
  FillListView;
end;

procedure TForm1.FormDestroy (Sender: TObject);
begin
  FileList.Free;
end;

procedure TForm1.Icons_LVData (Sender: TObject; Item: TListItem);

var iIndex : Integer;

begin
  if (Item.Index >= FileList.Count) then
    exit;

  Item.Caption := FileList [Item.Index];

  if (FileList.Objects [Item.Index] = TObject (-1)) then
  begin
    if not (LoadIconFromFile (cWinSysDir + Item.Caption, iIndex)) then
      iIndex := 0;

    FileList.Objects [Item.Index] := TObject (iIndex);
  end { if }
  else iIndex := Integer (FileList.Objects [Item.Index]);

  Item.ImageIndex := iIndex
end;

procedure TForm1.FillListView;

var SR : TSearchRec;

begin
  FillChar (SR, SizeOf (TSearchRec), #0);

  if (FindFirst (cWinSysDir + '*.exe', faAnyFile, SR) = 0) then
    repeat
      FileList.AddObject (SR.Name, TObject ((-1)));
    until (FindNext (SR) <> 0);

  FindClose (SR);
  Icons_LV.Items.Count := FileList.Count;
end;

function TForm1.LoadIconFromFile (const sFileName: String;
                                  out iIndex: Integer) : Boolean;

var
  hIcon : Windows.HICON;
  Icon : TIcon;

begin
  Result := false;

  if (ExtractIcon (MainInstance, PChar (sFileName), UInt ((-1))) > 0) then
  begin
{$IFDEF DEBUG}
    OutputDebugString (PChar (Format ('LoadIconFromFile "%s"', [sFileName])));
{$ENDIF}
    hIcon := ExtractIcon (MainInstance, PChar (sFileName), 0);

    if (hIcon <> 0) then
    begin
      Icon := TIcon.Create;
      Icon.Handle := hIcon;
      iIndex := Large_IL.AddIcon (Icon);
      Icon.Free;
      Result := true;
    end; { if }
  end { if }
end;

end.

The full example is available for download here.

Olaf Hess
  • 1,453
  • 11
  • 18
  • OP wants to display *thumbnails of images* not the icons of `*.exe`-files. – Josef Švejk Nov 07 '18 at 10:00
  • 2
    The procedure would be the same, just replace the method "LoadIconFromFile" with one that loads the images into the TImageList. From the code posted along with the question I figured that creating the thumbnails was not the issue. – Olaf Hess Nov 07 '18 at 10:16
  • 1
    My previous comment was foolish as I incorrectly stated where memory leak occurs. Also, just loading image in `TImageList` will not create thumbnail. By the way, I've tested your code and there is a memory leak. It occurs because you do not free instance of `TIcon` in `LoadIconFromFile()` method. Change the code where `TIcon` is creating in this way: `Icon := TIcon.Create; try Icon.Handle := hIcon; iIndex := Large_IL.AddIcon (Icon); finally Icon.Free; end;`. – Josef Švejk Nov 07 '18 at 11:14
  • @Dima: I've fixed the memory leak. Thanks for pointing it out. – Olaf Hess Nov 07 '18 at 22:34