注意
この記事は、実験のため、全文を AI アシスタント dot に執筆してもらいました。コントロールの実装は Codex(KAI)、そのコードを解説する記事の執筆は dot が担当しています。
はじめに
今回作ってもらったのは、FireMonkey 用の画像一覧コントロール TImageListView です。画像をタイル状に並べ、URL からの読み込みや複数選択を扱います。使用環境は Delphi 13.2/Windows 11 です。
この記事では PK.GUI.ImageListView.pas を、表示部品、配置、画像取得、選択、後始末に分けて読んでいきます。最後に、フォームへ組み込む使用例を載せます。
以下はソースに基づく解説です。dot によるコンパイル・実行確認は行っていません。途中のコードは該当箇所の抜粋で、前後や内部を省略した箇所にはその旨を記しています。
1. 一覧と項目を別のクラスにする
中心になるのは、一覧全体の TImageListView と、一枚分の画像を持つ TImageListViewItem です。宣言から継承元だけを抜き出すと、次の関係になります。
// メンバー宣言は省略
TImageListViewItem = class(TLayout)
// 省略
end;
TImageListView = class(TVertScrollBox)
// 省略
end;
TImageListView は、縦スクロールの土台として TVertScrollBox を使います。FMX 標準の TImageList を継承したものではありません。
項目の側は TLayout を土台にして、背景、画像、選択枠、読み込み表示を組み合わせています。図中の「一覧」は TImageListView、「項目」は TImageListViewItem を表します。
表示部品は項目に閉じ込め、項目の配置や選択のルールは一覧が管理する、という分担です。
2. 一枚の画像を表示する部品を組み立てる
TImageListViewItem.Create の画像と選択枠を作る部分です。背景 FBackground は、この直前に TRectangle として生成されています。
// コンストラクタ前半を省略
FImage := TImage.Create(Self);
FImage.Parent := FBackground;
FImage.Align := TAlignLayout.Client;
FImage.Margins.Rect := TRectF.Create(8, 8, 8, 8);
FImage.WrapMode := TImageWrapMode.Fit;
FImage.HitTest := False;
FSelectionFrame := TRectangle.Create(Self);
FSelectionFrame.Parent := Self;
FSelectionFrame.Align := TAlignLayout.Client;
FSelectionFrame.Fill.Kind := TBrushKind.None;
FSelectionFrame.Stroke.Color := TAlphaColors.Dodgerblue;
FSelectionFrame.Stroke.Thickness := 3;
FSelectionFrame.XRadius := 8;
FSelectionFrame.YRadius := 8;
FSelectionFrame.Visible := False;
FSelectionFrame.HitTest := False;
// コンストラクタ後半を省略
画像の親は背景で、Align := TAlignLayout.Client により背景の内側を使います。さらに四辺に 8 の余白を取り、WrapMode := TImageWrapMode.Fit で縦横比を保って収めています。画像を枠いっぱいに切り抜く設定ではないので、縦長・横長の違いに応じて余白ができます。
選択枠は塗りを None にし、線だけを重ねています。Visible を切り替えれば、画像そのものを書き換えずに選択状態を表せます。画像や枠の HitTest は False にし、マウス操作は項目側の MouseDown で処理します。
読み込み表示の切り替えは、専用の小さなメソッドにまとめています。
procedure TImageListViewItem.SetLoading(const AValue: Boolean);
begin
FAniIndicator.Enabled := AValue;
FAniIndicator.Visible := AValue;
end;
TAniIndicator は項目の中央に置かれています。読み込み開始時には動作と表示をともに有効にし、終了時にはともに無効にする構成です。
3. Add で項目を作り、Index は一覧から求める
項目を追加する入口は Add です。
function TImageListView.Add: TImageListViewItem;
begin
Result := TImageListViewItem.Create(Self);
Result.Parent := Self;
Result.FreeNotification(Self);
Result.ApplyListViewTemplate(Self);
FItems.Add(Result);
ArrangeItems;
end;
Create(Self) の Self はコンポーネントの所有者、Parent := Self は表示上の親の指定です。続いて解放通知を登録し、一覧の外観設定を適用してから FItems に登録します。最後の ArrangeItems で、追加した項目を含めて並べ直します。
戻り値が項目自身なので、利用側では FImageListView.Add.LoadFromURL(URL) のように続けて呼び出せます。
各項目の Index は固定の番号を保存しているのではなく、問い合わせ時に一覧から検索します。
function TImageListViewItem.GetIndex: Integer;
begin
var LOwner := OwnerImageListView;
if LOwner = nil then
Exit(-1);
Result := LOwner.Items.IndexOf(Self);
end;
OwnerImageListView は、Parent を上へたどって所属する一覧を探すメソッドです。名前は Owner ですが、ここで調べているのは表示上の親子関係です。一覧が見つからなければ -1、見つかれば Items.IndexOf(Self) の結果を返します。
したがって、Index は画像の識別 ID ではなく、その時点の並び順です。途中の項目を削除すれば、後ろの項目の値も変わります。
4. 幅から列数を求めてタイル状に並べる
まず GetColumnCount で、横に何項目置けるかを計算します。
function TImageListView.GetColumnCount: Integer;
begin
var LAvailableWidth := Max(
FItemWidth,
Width - Padding.Left - Padding.Right - 16);
Result := Max(
1,
Trunc((LAvailableWidth + FItemSpacing) / (FItemWidth + FItemSpacing)));
end;
一覧の幅から左右の Padding と 16 を引き、項目幅と項目間隔で割っています。外側の Max(1, ...) により、列数は最低でも一列になります。
次に ArrangeItems が、項目番号を行と列へ変換します。以下はその配置ループです。
// ArrangeItems の前半を省略
for var LIndex := 0 to FItems.Count - 1 do
begin
var LColumn := LIndex mod LColumnCount;
var LRow := LIndex div LColumnCount;
var LLeft := Padding.Left + LColumn * (FItemWidth + FItemSpacing);
var LTop := Padding.Top + LRow * (FItemHeight + FItemSpacing);
FItems[LIndex].SetBounds(
LLeft,
LTop,
LActualItemWidth,
FItemHeight);
end;
// メソッド末尾の end; を省略
mod の余りが列番号、div の商が行番号です。たとえば三列なら、インデックス 0〜2 が一行目、3〜5 が二行目になります。座標は「余白+行または列の番号×項目サイズと間隔」で求め、SetBounds に渡しています。
LActualItemWidth は前半で FItemWidth から決めています。項目を一覧の横幅いっぱいに均等伸縮させる方式ではなく、指定した項目サイズを基本に、列数を変える配置です。Resize やサイズ関連プロパティの変更時にも ArrangeItems が呼ばれます。
ここでは追加済みの全項目を走査します。画面内にある項目だけを生成・再利用する仮想化は入っていません。
5. URL の読み込み要求をキューに積む
項目から一覧へ読み込みを依頼する
LoadFromURL は、その場で HTTP 通信を始めるメソッドではありません。URL と状態を記録し、一覧へ要求を渡します。
procedure TImageListViewItem.LoadFromURL(const AURL: string);
begin
var LOwner := OwnerImageListView;
if LOwner = nil then
raise EInvalidOperation.Create(SItemRequiresListView);
FURL := AURL;
FLocalFileName := '';
FLoadError := '';
SetLoading(True);
LOwner.QueueDownload(Self, AURL);
end;
一覧への所属を先に確認するため、項目だけを単独で作って URL 読み込みを始めることはできません。通常は Add を経由して作ります。
要求を受け取る QueueDownload は、項目と URL の組を TImageListViewDownloadRequest にしてキューへ入れます。
procedure TImageListView.QueueDownload(
const AItem: TImageListViewItem;
const AURL: string);
begin
TMonitor.Enter(FQueueLock);
try
FQueue.Enqueue(TImageListViewDownloadRequest.Create(AItem, AURL));
FQueueEvent.SetEvent;
finally
TMonitor.Exit(FQueueLock);
end;
end;
複数のワーカーが同じキューを読むので、追加処理は TMonitor で保護しています。try ... finally にして、処理途中で例外が発生してもロックを解放する形です。
FQueueEvent.SetEvent は、要求を待っているワーカーへの通知です。キューを守る役割は TMonitor、待機と通知の役割は TEvent が受け持ちます。
ワーカーが要求を取り出す
取り出し側の TryDequeue でも、同じ FQueueLock を使います。
function TImageListView.TryDequeue(
out ARequest: TImageListViewDownloadRequest): Boolean;
begin
TMonitor.Enter(FQueueLock);
try
Result := FQueue.Count > 0;
if Result then
ARequest := FQueue.Dequeue
else
FQueueEvent.ResetEvent;
finally
TMonitor.Exit(FQueueLock);
end;
end;
要求があれば一件取り出し、なければ通知をリセットして False を返します。ワーカー本体はこの結果に応じて、取得処理へ進むか、イベントを待ちます。
procedure TImageListViewDownloadWorker.Execute;
begin
while not Terminated do
begin
var LRequest: TImageListViewDownloadRequest;
if FListView.TryDequeue(LRequest) then
Download(LRequest)
else
FListView.FQueueEvent.WaitFor(250);
end;
end;
WaitFor(250) は、要求がないときの待機です。常にキューを回し続けるのではなく、通知または待機時間の経過後に再び確認します。
ワーカー数を決めるのは DownloadThreadCount で、既定値は 4 です。画像の件数に合わせて一件につき一本のスレッドを作るのではなく、この数のワーカーでキューを処理します。
HTTP で受け取るのはバイト列
Download の HTTP 分岐では、THTTPClient.Get の結果を TMemoryStream に受け取ります。以下は取得結果を確認してバイト列へ移す部分です。
// Download の前半と、クライアント・ストリームの生成処理を省略
var LResponse := LHTTPClient.Get(ARequest.URL, LStream);
if (LResponse.StatusCode < 200) or
(LResponse.StatusCode > 299) then
begin
raise ENetHTTPClientException.CreateFmt(
SHTTPError,
[LResponse.StatusCode, LResponse.StatusText]);
end;
SetLength(LBytes, LStream.Size);
LStream.Position := 0;
if LStream.Size > 0 then
LStream.ReadBuffer(LBytes[0], LStream.Size);
// 解放処理、例外処理、後半を省略
HTTP ステータスが 2xx 以外なら例外とし、そのメッセージを後段へ渡します。この段階では画像を描画せず、データをバイト列として扱っています。なお file:// で始まる URL は別分岐で、ワーカーがローカルファイルをバイト列として読みます。
6. UI スレッドへ渡して画像に反映する
取得が終わったら、Download の末尾で UI スレッドに処理を渡します。
// Download の取得処理と例外処理を省略
var LItem := ARequest.Item;
var LListView := FListView;
var LResultBytes := LBytes;
var LResultErrorMessage := LErrorMessage;
TThread.Queue(
nil,
procedure
begin
if not Terminated and LListView.ContainsItem(LItem) then
LItem.FinishLoading(LResultBytes, LResultErrorMessage);
end);
// メソッド末尾の end; を省略
TThread.Queue をワーカーから呼び、UI スレッドで FinishLoading を実行する構成です。コードでは停止状態と、項目が一覧に残っているかを確認しています。ただし、これだけで破棄時の寿命管理がすべて保証されるわけではなく、後述の確認点が残ります。
HTTP 読み込みの経路を図にすると、次のようになります。図中の「UIへ通知」が TThread.Queue、「画像を展開」が FinishLoading 内の処理です。
受け取った項目側の処理は次のとおりです。
procedure TImageListViewItem.FinishLoading(
const ABytes: TBytes;
const AErrorMessage: string);
begin
SetLoading(False);
FLoadError := AErrorMessage;
if AErrorMessage <> '' then
Exit;
var LStream := TBytesStream.Create(ABytes);
try
FImage.Bitmap.LoadFromStream(LStream);
FLocalFileName := '';
finally
LStream.Free;
end;
end;
最初にインジケータを止め、取得時のエラーを LoadError に記録します。エラーがなければ TBytesStream を作り、Bitmap.LoadFromStream で画像へ変換します。
重要なのは、通信はワーカー側、画像のデコードは UI スレッド側という分担です。小さな枠に表示していても、画像を縮小してから保持する処理はありません。高解像度画像を多く扱う場合は、メモリとデコード負荷を確認する必要があります。
また、このメソッドにはデコード例外を LoadError に入れる処理はありません。取得エラーを受け取れることと、画像形式の不正などを含めたすべての失敗を処理できることは、分けて考えます。
7. 選択は一覧で管理し、項目は枠を表示する
項目の MouseDown は、左クリックを受けたら一覧へ処理を渡します。その際に選択処理を行い、クリックかダブルクリックかに応じたイベントを呼びます。選択ルールの中心は HandleItemMouseDown です。
まず、複数選択時の Shift キーの分岐を見ます。
// HandleItemMouseDown の所属確認・単一選択分岐を省略
if ssShift in Shift then
begin
var LAnchor := FSelectionAnchor;
if not ContainsItem(LAnchor) then
LAnchor := AItem;
if not (ssCtrl in Shift) then
ClearSelection;
SelectRange(LAnchor.Index, AItem.Index);
FSelectionAnchor := LAnchor;
Exit;
end;
// Ctrl キーと通常クリックの分岐を省略
FSelectionAnchor が範囲選択の基点です。基点が有効でなければクリックした項目を基点にし、SelectRange で両端の Index の間を選択します。Ctrl を併用していなければ、先に既存の選択を解除します。
Ctrl キーの分岐は、クリックした一件の状態を反転させます。
// HandleItemMouseDown の前半を省略
if ssCtrl in Shift then
begin
SetItemSelected(AItem, not AItem.Selected);
if AItem.Selected then
FSelectionAnchor := AItem;
Exit;
end;
// 通常クリックの分岐を省略
修飾キーなしでは既存の選択を解除して一件を選びます。MultiSelect := False の場合も一件だけの選択です。矢印キーの処理では、左右はインデックスを一つ、上下は列数分だけ増減させるコードが入っています。
状態を変える SetItemSelected は、項目へ表示変更を伝え、一覧側の選択項目リストを更新します。以下は引数と状態の確認を終えた後の部分です。
// SetItemSelected の引数・状態確認を省略
AItem.SetSelected(AValue);
if AValue then
begin
if FSelectedItems.IndexOf(AItem) < 0 then
FSelectedItems.Add(AItem);
if Assigned(FOnSelected) then
FOnSelected(Self, AItem);
end
else
FSelectedItems.Remove(AItem);
// メソッド末尾の end; を省略
項目側の SetSelected は FSelected と選択枠の Visible を更新します。選択のルールと選択状態の描画を分けているわけです。
ここで OnSelected は、選択状態になった側だけで呼ばれます。選択解除を含む「選択集合が変わった」という通知ではありません。
利用側が単一の選択項目を読む Selected にも、押さえておきたい条件があります。
function TImageListView.GetSelected: TImageListViewItem;
begin
if FSelectedItems.Count = 1 then
Result := FSelectedItems[0]
else
Result := nil;
end;
選択数がちょうど一件の場合にだけ項目を返し、未選択でも複数選択でも nil です。最後に選択した項目を返すプロパティではありません。複数件を扱うときは、SelectedItems と SelectedCount を使います。SelectedItems は FItems の順に走査して作るため、クリック順ではなく一覧の並び順です。
8. プロパティ変更を配置と外観へ反映する
サイズを変えるプロパティは、値を設定して配置をやり直します。たとえば高さの setter は次のとおりです。
procedure TImageListView.SetItemHeight(const AValue: Single);
begin
FItemHeight := Max(MinItemSize, AValue);
ArrangeItems;
end;
MinItemSize は 48.0 です。ItemWidth にも同じ下限があり、サイズを変えたら ArrangeItems が呼ばれます。
一方、色や角丸などの変更は UpdateItemTemplates を通じて、各項目の ApplyListViewTemplate に伝わります。
procedure TImageListViewItem.ApplyListViewTemplate(
const AListView: TImageListView);
begin
FBackground.Fill.Color := AListView.ItemBackgroundColor;
FBackground.XRadius := AListView.ItemRadius;
FBackground.YRadius := AListView.ItemRadius;
FSelectionFrame.Stroke.Color := AListView.SelectionFrameColor;
FSelectionFrame.Stroke.Thickness := AListView.SelectionFrameThickness;
FSelectionFrame.XRadius := AListView.ItemRadius;
FSelectionFrame.YRadius := AListView.ItemRadius;
AListView.ApplyAniIndicatorTemplate(FAniIndicator);
end;
一覧側にある設定を、背景、選択枠、インジケータへ配る処理です。新しい項目を作るときだけでなく、追加済みの項目へ外観変更を反映するためにも使っています。
AniIndicatorTemplate は表示用インジケータそのものではなく、幅、高さ、StyleLookup などの設定元です。それぞれの項目は自分専用のインジケータを持ちます。
画像を取り出す入口には Bitmap、読み込み済みか調べる入口には HasImage があります。保存処理も短くまとまっています。
procedure TImageListViewItem.SaveToFile(const AFileName: string);
begin
if not HasImage then
raise EInvalidOperation.Create(SImageNotLoaded);
FImage.Bitmap.SaveToFile(AFileName);
end;
ここで保存するのは、取得した元ファイルのバイト列ではなく、読み込んだビットマップです。HasImage が False なら例外になります。
ローカルファイルを直接読む LoadFromFile も用意されていますが、こちらは呼び出し元で Bitmap.LoadFromFile を実行する同期処理です。URL 読み込みと同じキューを経由するわけではありません。
9. 項目の削除と所有権をそろえる
一覧が持つ項目リストは、コンストラクタで次のように作られています。
// TImageListView.Create の他の初期化は省略
FItems := TObjectList<TImageListViewItem>.Create(False);
Create(False) なので、FItems から削除するだけでは項目は解放されません。項目の生成時には一覧をコンポーネントの所有者に指定していますが、通常の削除では Delete が明示的に後始末します。
procedure TImageListView.Delete(const AIndex: Integer);
begin
CheckIndex(AIndex);
var LItem := FItems[AIndex];
RemoveQueuedDownloads(LItem);
SetItemSelected(LItem, False);
if FSelectionAnchor = LItem then
FSelectionAnchor := nil;
FItems.Delete(AIndex);
LItem.RemoveFreeNotification(Self);
LItem.Parent := nil;
LItem.Free;
ArrangeItems;
end;
未処理のダウンロード要求をキューから外し、選択状態と範囲選択の基点を整理してから、リストから除外して項目を解放します。最後に残った項目を並べ直します。
FreeNotification は、項目が別経路で解放された場合にも一覧側を整理するための仕組みです。Notification から DetachItem が呼ばれ、管理リストや選択リストから対象を外す処理が入っています。利用側が Items を直接書き換えるより、Add、Delete、Remove、Clear を使う方が、この後始末と足並みをそろえられます。
一覧自体の破棄は、ワーカーを止めてから項目と管理用オブジェクトを解放する順序になっています。
destructor TImageListView.Destroy;
begin
StopWorkers;
Clear;
FSelectedItems.Free;
FWorkers.Free;
FQueueLock.Free;
FQueueEvent.Free;
FQueue.Free;
FItems.Free;
inherited Destroy;
end;
StopWorkers では終了要求を出し、待機中のワーカーを起こして終了を待ちます。ただし、キューから要求を除去することは、すでに始まった HTTP 通信を即座に中断することとは異なります。
実際の利用前に確認する点
処理の構成が読み取れる一方、非同期処理の境界には確認点もあります。
-
破棄時のコールバック:完了処理は
TThread.Queue(nil, ...)で登録する一方、停止処理ではRemoveQueuedEvents(LWorker)を呼んでいます。関連付けが一致していないため、待機中のコールバックとワーカー・一覧の寿命は見直す必要があります。 - 同じ項目の再読み込み:要求の世代を識別する仕組みがないため、古い URL の結果が後から反映される可能性があります。
-
失敗時の状態:
LoadFromFileは例外を再送出する経路で末尾のSetLoading(False)に到達しません。画像デコード時の例外処理と併せ、失敗時の表示と後始末を確認する必要があります。
ここまでがコントロール本体の構造です。既存の FMX 部品を項目にまとめ、一覧がレイアウト・選択・取得キューを管理する形になっています。
使用例
最後に、フォームへ組み込むコードです。Load ボタンを押すと Picsum Photos の画像を 100 件追加し、読み込み済みの画像をダブルクリックすると別フォームで表示します。
フォームの準備
- 新しい FireMonkey アプリケーションを作り、フォームのクラス名を
TForm1、ユニット名をUnit1にします。 - プロジェクトへ
PK.GUI.ImageListView.pasを追加します。 - フォームへ
TButtonを一つ置き、NameをButton1、TextをLoadにします。 - フォームの
OnCreateをFormCreate、ボタンのOnClickをButton1Clickに割り当てます。 -
Unit1.pasを以下の内容にします。イベントは宣言と実装を両方含めています。フォームの.fmxは、手順3・4で作成したものを使います。
画像の取得にはインターネット接続が必要です。特別なスタイルの配置は不要です。
Unit1.pas
unit Unit1;
interface
uses
System.SysUtils, System.Types, System.UITypes, System.Classes,
System.Variants, FMX.Types, FMX.Controls, FMX.Forms, FMX.Graphics,
FMX.Dialogs, FMX.Objects,
PK.GUI.ImageListView, FMX.Controls.Presentation, FMX.StdCtrls;
type
TForm1 = class(TForm)
Button1: TButton;
procedure FormCreate(Sender: TObject);
procedure Button1Click(Sender: TObject);
private
FImageListView: TImageListView;
procedure ImageListViewDblClick(
const Sender: TObject;
const Item: TImageListViewItem);
public
{ public declarations }
end;
var
Form1: TForm1;
implementation
{$R *.fmx}
procedure TForm1.Button1Click(Sender: TObject);
begin
Button1.Visible := False;
for var LIndex := 1 to 100 do
begin
var LURL :=
Format(
'https://picsum.photos/seed/fmx-image-list-%d/640/360',
[LIndex]
);
FImageListView.Add.LoadFromURL(LURL);
end;
end;
procedure TForm1.FormCreate(Sender: TObject);
begin
FImageListView := TImageListView.Create(Self);
FImageListView.Parent := Self;
FImageListView.Align := TAlignLayout.Client;
FImageListView.ItemWidth := 200;
FImageListView.ItemHeight := 150;
FImageListView.DownloadThreadCount := 4;
FImageListView.OnDblClick := ImageListViewDblClick;
Button1.BringToFront;
end;
procedure TForm1.ImageListViewDblClick(
const Sender: TObject;
const Item: TImageListViewItem);
begin
if (Item = nil) or not Item.HasImage then
Exit;
var LForm := TForm.CreateNew(Self);
LForm.Caption := Item.FileName;
LForm.Width := 900;
LForm.Height := 700;
LForm.Position := TFormPosition.ScreenCenter;
var LImage := TImage.Create(LForm);
LImage.Parent := LForm;
LImage.Align := TAlignLayout.Client;
LImage.Margins.Rect := TRectF.Create(12, 12, 12, 12);
LImage.WrapMode := TImageWrapMode.Fit;
LImage.Bitmap.Assign(Item.Bitmap);
LForm.Show;
end;
end.
FormCreate で一覧をフォームいっぱいに配置し、項目の大きさを幅 200、高さ 150 に指定しています。コントロール本体の既定の高さは 160 ですが、この例では 150 に変更しています。
ボタン側で必要なのは、URL を作って Add.LoadFromURL を呼ぶことです。100 件を一度に要求しても、取得処理は設定した 4 本のワーカーが受け持ちます。URL で指定する画像サイズは 640×360 で、一覧の表示枠の大きさとは別です。
ダブルクリックのハンドラでは HasImage を確認し、別フォームの TImage に Bitmap.Assign で画像を渡しています。画像一覧の中に拡大表示画面まで抱え込ませず、アプリ側のイベントで用途に合わせた処理を付ける使い方です。
なお、ここではプレビュー用フォームをメインフォームの所有物として生成しています。繰り返し開く用途では、閉じたフォームをその都度解放するか、同じフォームを再利用するかをアプリ側で決めます。
最後に(ここから人間)
AI で作ったコントロールを AI に解説させたら楽なのでは?と思って、dot に書いて貰いましたが、いまいちわかりづらいですね…
dot は無料で使えるのでやらせてみましたが、dot より Codex に書かせた方が良いのかも知れません。
改めて書くと URL からの読込みが実装されたイメージを表示するリストビューコントロールです。
ウェブで良く見る奴です。
以下、ソース全文です。
unit PK.GUI.ImageListView;
interface
uses
System.Classes,
System.Generics.Collections,
System.IOUtils,
System.Math,
System.NetEncoding,
System.Net.HttpClient,
System.Net.URLClient,
System.SyncObjs,
System.SysUtils,
System.Types,
System.UITypes,
FMX.Controls,
FMX.Graphics,
FMX.Layouts,
FMX.Objects,
FMX.StdCtrls,
FMX.Types;
type
TImageListView = class;
TImageListViewItem = class;
TImageListViewItemEvent = procedure(
const Sender: TObject;
const Item: TImageListViewItem) of object;
TImageListViewItem = class(TLayout)
private
FAniIndicator: TAniIndicator;
FBackground: TRectangle;
FImage: TImage;
FLoadError: string;
FLocalFileName: string;
FSelected: Boolean;
FSelectionFrame: TRectangle;
FURL: string;
function GetBitmap: TBitmap;
function GetFileName: string;
function GetHasImage: Boolean;
function GetIndex: Integer;
function OwnerImageListView: TImageListView;
procedure ApplyListViewTemplate(const AListView: TImageListView);
procedure FinishLoading(
const ABytes: TBytes;
const AErrorMessage: string);
procedure SetLoading(const AValue: Boolean);
procedure SetSelected(const AValue: Boolean);
protected
procedure MouseDown(
Button: TMouseButton;
Shift: TShiftState;
X: Single;
Y: Single); override;
public
constructor Create(AOwner: TComponent); override;
procedure LoadFromFile(const AFileName: string);
procedure LoadFromURL(const AURL: string);
procedure SaveToFile(const AFileName: string);
property Bitmap: TBitmap read GetBitmap;
property FileName: string read GetFileName;
property HasImage: Boolean read GetHasImage;
property Index: Integer read GetIndex;
property LoadError: string read FLoadError;
property LocalFileName: string read FLocalFileName;
property Selected: Boolean read FSelected;
property URL: string read FURL;
end;
TImageListViewDownloadRequest = record
Item: TImageListViewItem;
URL: string;
constructor Create(
const AItem: TImageListViewItem;
const AURL: string);
end;
TImageListViewDownloadWorker = class(TThread)
private
FListView: TImageListView;
class function FileURLToFileName(const AURL: string): string; static;
procedure Download(const ARequest: TImageListViewDownloadRequest);
protected
procedure Execute; override;
public
constructor Create(const AListView: TImageListView);
end;
TImageListView = class(TVertScrollBox)
private
FAniIndicatorTemplate: TAniIndicator;
FDownloadThreadCount: Integer;
FItemBackgroundColor: TAlphaColor;
FItemHeight: Single;
FItemRadius: Single;
FItemSpacing: Single;
FItemWidth: Single;
FItems: TObjectList<TImageListViewItem>;
FMultiSelect: Boolean;
FOnClick: TImageListViewItemEvent;
FOnDblClick: TImageListViewItemEvent;
FOnSelected: TImageListViewItemEvent;
FQueue: TQueue<TImageListViewDownloadRequest>;
FQueueEvent: TEvent;
FQueueLock: TObject;
FSelectedItems: TList<TImageListViewItem>;
FSelectionAnchor: TImageListViewItem;
FSelectionFrameColor: TAlphaColor;
FSelectionFrameThickness: Single;
FWorkers: TObjectList<TImageListViewDownloadWorker>;
function ContainsItem(const AItem: TImageListViewItem): Boolean;
function GetColumnCount: Integer;
function GetSelected: TImageListViewItem;
function GetSelectedCount: Integer;
function GetSelectedItems: TArray<TImageListViewItem>;
procedure ApplyAniIndicatorTemplate(const AIndicator: TAniIndicator);
procedure ArrangeItems;
procedure CheckIndex(const AIndex: Integer);
procedure DetachItem(const AItem: TImageListViewItem);
procedure HandleItemMouseDown(
const AItem: TImageListViewItem;
const Shift: TShiftState);
procedure RemoveQueuedDownloads(const AItem: TImageListViewItem);
procedure SelectRange(
const AStartIndex: Integer;
const AEndIndex: Integer);
procedure SetAniIndicatorTemplate(const AValue: TAniIndicator);
procedure SetDownloadThreadCount(const AValue: Integer);
procedure SetItemBackgroundColor(const AValue: TAlphaColor);
procedure SetItemHeight(const AValue: Single);
procedure SetItemRadius(const AValue: Single);
procedure SetItemSpacing(const AValue: Single);
procedure SetItemSelected(
const AItem: TImageListViewItem;
const AValue: Boolean);
procedure SetItemWidth(const AValue: Single);
procedure SetMultiSelect(const AValue: Boolean);
procedure SetSelected(const AItem: TImageListViewItem);
procedure SetSelectionFrameColor(const AValue: TAlphaColor);
procedure SetSelectionFrameThickness(const AValue: Single);
procedure StartWorkers;
procedure StopWorkers;
procedure UpdateItemTemplates;
function TryDequeue(out ARequest: TImageListViewDownloadRequest): Boolean;
protected
procedure KeyDown(
var Key: Word;
var KeyChar: WideChar;
Shift: TShiftState); override;
procedure Notification(
AComponent: TComponent;
Operation: TOperation); override;
procedure QueueDownload(
const AItem: TImageListViewItem;
const AURL: string);
procedure Resize; override;
public
constructor Create(AOwner: TComponent); override;
destructor Destroy; override;
function Add: TImageListViewItem;
procedure Clear;
procedure ClearSelection;
procedure ClickItem(const AItem: TImageListViewItem);
procedure DblClickItem(const AItem: TImageListViewItem);
procedure Delete(const AIndex: Integer);
procedure Remove(const AItem: TImageListViewItem);
procedure SaveToFile(
const AIndex: Integer;
const AFileName: string); overload;
procedure SaveToFile(
const AItem: TImageListViewItem;
const AFileName: string); overload;
property Items: TObjectList<TImageListViewItem> read FItems;
property Selected: TImageListViewItem read GetSelected write SetSelected;
property SelectedCount: Integer read GetSelectedCount;
property SelectedItems: TArray<TImageListViewItem> read GetSelectedItems;
published
property AniIndicatorTemplate: TAniIndicator
read FAniIndicatorTemplate write SetAniIndicatorTemplate;
property DownloadThreadCount: Integer
read FDownloadThreadCount write SetDownloadThreadCount default 4;
property ItemBackgroundColor: TAlphaColor
read FItemBackgroundColor write SetItemBackgroundColor;
property ItemHeight: Single read FItemHeight write SetItemHeight;
property ItemRadius: Single read FItemRadius write SetItemRadius;
property ItemSpacing: Single read FItemSpacing write SetItemSpacing;
property ItemWidth: Single read FItemWidth write SetItemWidth;
property MultiSelect: Boolean
read FMultiSelect write SetMultiSelect default True;
property SelectionFrameColor: TAlphaColor
read FSelectionFrameColor write SetSelectionFrameColor;
property SelectionFrameThickness: Single
read FSelectionFrameThickness write SetSelectionFrameThickness;
property OnClick: TImageListViewItemEvent read FOnClick write FOnClick;
property OnDblClick: TImageListViewItemEvent
read FOnDblClick write FOnDblClick;
property OnSelected: TImageListViewItemEvent
read FOnSelected write FOnSelected;
end;
implementation
const
MinItemSize = 48.0;
SHTTPError = 'HTTP %d: %s';
SImageNotLoaded = 'The image has not been loaded yet.';
SIndexOutOfRange = 'Index is out of range.';
SItemNotOwned = 'The item does not belong to this TImageListView.';
SItemRequired = 'Item is not assigned.';
SItemRequiresListView =
'TImageListViewItem must be parented to TImageListView before loading ' +
'from URL.';
{ TImageListViewItem }
constructor TImageListViewItem.Create(AOwner: TComponent);
begin
inherited Create(AOwner);
ClipChildren := True;
Height := 160;
HitTest := True;
FBackground := TRectangle.Create(Self);
FBackground.Parent := Self;
FBackground.Align := TAlignLayout.Client;
FBackground.Fill.Color := TAlphaColors.Bisque;
FBackground.Stroke.Color := TAlphaColors.Lightgray;
FBackground.XRadius := 8;
FBackground.YRadius := 8;
FBackground.HitTest := False;
FImage := TImage.Create(Self);
FImage.Parent := FBackground;
FImage.Align := TAlignLayout.Client;
FImage.Margins.Rect := TRectF.Create(8, 8, 8, 8);
FImage.WrapMode := TImageWrapMode.Fit;
FImage.HitTest := False;
FSelectionFrame := TRectangle.Create(Self);
FSelectionFrame.Parent := Self;
FSelectionFrame.Align := TAlignLayout.Client;
FSelectionFrame.Fill.Kind := TBrushKind.None;
FSelectionFrame.Stroke.Color := TAlphaColors.Dodgerblue;
FSelectionFrame.Stroke.Thickness := 3;
FSelectionFrame.XRadius := 8;
FSelectionFrame.YRadius := 8;
FSelectionFrame.Visible := False;
FSelectionFrame.HitTest := False;
FAniIndicator := TAniIndicator.Create(Self);
FAniIndicator.Parent := Self;
FAniIndicator.Align := TAlignLayout.Center;
FAniIndicator.Width := 48;
FAniIndicator.Height := 48;
FAniIndicator.Enabled := False;
FAniIndicator.Visible := False;
FAniIndicator.HitTest := False;
if AOwner is TImageListView then
ApplyListViewTemplate(TImageListView(AOwner));
end;
procedure TImageListViewItem.ApplyListViewTemplate(
const AListView: TImageListView);
begin
FBackground.Fill.Color := AListView.ItemBackgroundColor;
FBackground.XRadius := AListView.ItemRadius;
FBackground.YRadius := AListView.ItemRadius;
FSelectionFrame.Stroke.Color := AListView.SelectionFrameColor;
FSelectionFrame.Stroke.Thickness := AListView.SelectionFrameThickness;
FSelectionFrame.XRadius := AListView.ItemRadius;
FSelectionFrame.YRadius := AListView.ItemRadius;
AListView.ApplyAniIndicatorTemplate(FAniIndicator);
end;
procedure TImageListViewItem.FinishLoading(
const ABytes: TBytes;
const AErrorMessage: string);
begin
SetLoading(False);
FLoadError := AErrorMessage;
if AErrorMessage <> '' then
Exit;
var LStream := TBytesStream.Create(ABytes);
try
FImage.Bitmap.LoadFromStream(LStream);
FLocalFileName := '';
finally
LStream.Free;
end;
end;
function TImageListViewItem.GetBitmap: TBitmap;
begin
Result := FImage.Bitmap;
end;
function TImageListViewItem.GetFileName: string;
begin
if FLocalFileName <> '' then
Exit(System.IOUtils.TPath.GetFileName(FLocalFileName));
if FURL = '' then
Exit('');
var LURI := TURI.Create(FURL);
Result := System.IOUtils.TPath.GetFileName(LURI.Path);
if Result = '' then
Result := System.IOUtils.TPath.GetFileName(FURL);
end;
function TImageListViewItem.GetHasImage: Boolean;
begin
Result := (FImage.Bitmap.Width > 0) and (FImage.Bitmap.Height > 0);
end;
function TImageListViewItem.GetIndex: Integer;
begin
var LOwner := OwnerImageListView;
if LOwner = nil then
Exit(-1);
Result := LOwner.Items.IndexOf(Self);
end;
procedure TImageListViewItem.LoadFromFile(const AFileName: string);
begin
SetLoading(True);
try
FImage.Bitmap.LoadFromFile(AFileName);
FLocalFileName := AFileName;
FURL := 'file://' + AFileName.Replace('\', '/');
FLoadError := '';
except
on E: Exception do
begin
FLoadError := E.Message;
raise;
end;
end;
SetLoading(False);
end;
procedure TImageListViewItem.LoadFromURL(const AURL: string);
begin
var LOwner := OwnerImageListView;
if LOwner = nil then
raise EInvalidOperation.Create(SItemRequiresListView);
FURL := AURL;
FLocalFileName := '';
FLoadError := '';
SetLoading(True);
LOwner.QueueDownload(Self, AURL);
end;
procedure TImageListViewItem.MouseDown(
Button: TMouseButton;
Shift: TShiftState;
X: Single;
Y: Single);
begin
inherited MouseDown(Button, Shift, X, Y);
if Button <> TMouseButton.mbLeft then
Exit;
var LOwner := OwnerImageListView;
if LOwner <> nil then
begin
LOwner.SetFocus;
LOwner.HandleItemMouseDown(Self, Shift);
if ssDouble in Shift then
LOwner.DblClickItem(Self)
else
LOwner.ClickItem(Self);
end;
end;
function TImageListViewItem.OwnerImageListView: TImageListView;
begin
var LParent := Parent;
while Assigned(LParent) and not (LParent is TImageListView) do
LParent := LParent.Parent;
if LParent is TImageListView then
Result := TImageListView(LParent)
else
Result := nil;
end;
procedure TImageListViewItem.SaveToFile(const AFileName: string);
begin
if not HasImage then
raise EInvalidOperation.Create(SImageNotLoaded);
FImage.Bitmap.SaveToFile(AFileName);
end;
procedure TImageListViewItem.SetLoading(const AValue: Boolean);
begin
FAniIndicator.Enabled := AValue;
FAniIndicator.Visible := AValue;
end;
procedure TImageListViewItem.SetSelected(const AValue: Boolean);
begin
if FSelected = AValue then
Exit;
FSelected := AValue;
FSelectionFrame.Visible := AValue;
end;
{ TImageListViewDownloadRequest }
constructor TImageListViewDownloadRequest.Create(
const AItem: TImageListViewItem;
const AURL: string);
begin
Item := AItem;
URL := AURL;
end;
{ TImageListViewDownloadWorker }
constructor TImageListViewDownloadWorker.Create(
const AListView: TImageListView);
begin
inherited Create(False);
FreeOnTerminate := False;
FListView := AListView;
end;
procedure TImageListViewDownloadWorker.Download(
const ARequest: TImageListViewDownloadRequest);
begin
var LBytes: TBytes := nil;
var LErrorMessage := '';
try
if ARequest.URL.StartsWith('file://', True) then
LBytes := System.IOUtils.TFile.ReadAllBytes(
FileURLToFileName(ARequest.URL))
else
begin
var LStream := TMemoryStream.Create;
try
var LHTTPClient := THTTPClient.Create;
try
var LResponse := LHTTPClient.Get(ARequest.URL, LStream);
if (LResponse.StatusCode < 200) or
(LResponse.StatusCode > 299) then
begin
raise ENetHTTPClientException.CreateFmt(
SHTTPError,
[LResponse.StatusCode, LResponse.StatusText]);
end;
SetLength(LBytes, LStream.Size);
LStream.Position := 0;
if LStream.Size > 0 then
LStream.ReadBuffer(LBytes[0], LStream.Size);
finally
LHTTPClient.Free;
end;
finally
LStream.Free;
end;
end;
except
on E: Exception do
LErrorMessage := E.Message;
end;
var LItem := ARequest.Item;
var LListView := FListView;
var LResultBytes := LBytes;
var LResultErrorMessage := LErrorMessage;
TThread.Queue(
nil,
procedure
begin
if not Terminated and LListView.ContainsItem(LItem) then
LItem.FinishLoading(LResultBytes, LResultErrorMessage);
end);
end;
procedure TImageListViewDownloadWorker.Execute;
begin
while not Terminated do
begin
var LRequest: TImageListViewDownloadRequest;
if FListView.TryDequeue(LRequest) then
Download(LRequest)
else
FListView.FQueueEvent.WaitFor(250);
end;
end;
class function TImageListViewDownloadWorker.FileURLToFileName(
const AURL: string): string;
begin
var LURI := TURI.Create(AURL);
Result := TNetEncoding.URL.Decode(LURI.Path);
if Result.StartsWith('/') and
(Result.Length >= 3) and
(Result.Chars[2] = ':')
then
begin
Result := Result.Substring(1);
end;
Result := StringReplace(Result, '/', PathDelim, [rfReplaceAll]);
end;
{ TImageListView }
constructor TImageListView.Create(AOwner: TComponent);
begin
inherited Create(AOwner);
CanFocus := True;
FAniIndicatorTemplate := TAniIndicator.Create(Self);
FAniIndicatorTemplate.Width := 48;
FAniIndicatorTemplate.Height := 48;
FAniIndicatorTemplate.Enabled := False;
FAniIndicatorTemplate.Visible := False;
FAniIndicatorTemplate.HitTest := False;
FDownloadThreadCount := 4;
FItemBackgroundColor := TAlphaColors.WhiteSmoke;
FItemHeight := 160;
FItemRadius := 8;
FItemSpacing := 8;
FItemWidth := 200;
FItems := TObjectList<TImageListViewItem>.Create(False);
FMultiSelect := True;
FQueue := TQueue<TImageListViewDownloadRequest>.Create;
FQueueEvent := TEvent.Create(nil, True, False, '');
FQueueLock := TObject.Create;
FSelectedItems := TList<TImageListViewItem>.Create;
FSelectionFrameColor := TAlphaColors.Dodgerblue;
FSelectionFrameThickness := 3;
FWorkers := TObjectList<TImageListViewDownloadWorker>.Create(True);
Padding.Rect := TRectF.Create(8, 8, 8, 8);
ShowScrollBars := True;
StartWorkers;
end;
destructor TImageListView.Destroy;
begin
StopWorkers;
Clear;
FSelectedItems.Free;
FWorkers.Free;
FQueueLock.Free;
FQueueEvent.Free;
FQueue.Free;
FItems.Free;
inherited Destroy;
end;
function TImageListView.Add: TImageListViewItem;
begin
Result := TImageListViewItem.Create(Self);
Result.Parent := Self;
Result.FreeNotification(Self);
Result.ApplyListViewTemplate(Self);
FItems.Add(Result);
ArrangeItems;
end;
procedure TImageListView.ApplyAniIndicatorTemplate(
const AIndicator: TAniIndicator);
begin
var LWasLoading := AIndicator.Enabled;
AIndicator.Align := TAlignLayout.Center;
AIndicator.Width := FAniIndicatorTemplate.Width;
AIndicator.Height := FAniIndicatorTemplate.Height;
AIndicator.Opacity := FAniIndicatorTemplate.Opacity;
AIndicator.StyleLookup := FAniIndicatorTemplate.StyleLookup;
AIndicator.Margins.Assign(FAniIndicatorTemplate.Margins);
AIndicator.Padding.Assign(FAniIndicatorTemplate.Padding);
AIndicator.HitTest := False;
AIndicator.Enabled := LWasLoading;
AIndicator.Visible := LWasLoading;
end;
procedure TImageListView.ArrangeItems;
begin
var LAvailableWidth := Max(
FItemWidth,
Width - Padding.Left - Padding.Right - 16);
var LColumnCount := GetColumnCount;
var LActualItemWidth := FItemWidth;
if LColumnCount = 1 then
LActualItemWidth := Min(FItemWidth, LAvailableWidth);
for var LIndex := 0 to FItems.Count - 1 do
begin
var LColumn := LIndex mod LColumnCount;
var LRow := LIndex div LColumnCount;
var LLeft := Padding.Left + LColumn * (FItemWidth + FItemSpacing);
var LTop := Padding.Top + LRow * (FItemHeight + FItemSpacing);
FItems[LIndex].SetBounds(
LLeft,
LTop,
LActualItemWidth,
FItemHeight);
end;
end;
procedure TImageListView.CheckIndex(const AIndex: Integer);
begin
if not InRange(AIndex, 0, FItems.Count - 1) then
raise EArgumentException.Create(SIndexOutOfRange);
end;
procedure TImageListView.Clear;
begin
while FItems.Count > 0 do
Delete(0);
FSelectionAnchor := nil;
end;
procedure TImageListView.ClearSelection;
begin
for var LItem in GetSelectedItems do
SetItemSelected(LItem, False);
FSelectionAnchor := nil;
end;
procedure TImageListView.ClickItem(const AItem: TImageListViewItem);
begin
if Assigned(FOnClick) then
FOnClick(Self, AItem);
end;
procedure TImageListView.DblClickItem(const AItem: TImageListViewItem);
begin
if Assigned(FOnDblClick) then
FOnDblClick(Self, AItem);
end;
function TImageListView.ContainsItem(const AItem: TImageListViewItem): Boolean;
begin
Result := (FItems <> nil) and (FItems.IndexOf(AItem) >= 0);
end;
function TImageListView.GetColumnCount: Integer;
begin
var LAvailableWidth := Max(
FItemWidth,
Width - Padding.Left - Padding.Right - 16);
Result := Max(
1,
Trunc((LAvailableWidth + FItemSpacing) / (FItemWidth + FItemSpacing)));
end;
procedure TImageListView.Delete(const AIndex: Integer);
begin
CheckIndex(AIndex);
var LItem := FItems[AIndex];
RemoveQueuedDownloads(LItem);
SetItemSelected(LItem, False);
if FSelectionAnchor = LItem then
FSelectionAnchor := nil;
FItems.Delete(AIndex);
LItem.RemoveFreeNotification(Self);
LItem.Parent := nil;
LItem.Free;
ArrangeItems;
end;
procedure TImageListView.DetachItem(const AItem: TImageListViewItem);
begin
var LIndex := FItems.IndexOf(AItem);
if LIndex < 0 then
Exit;
RemoveQueuedDownloads(AItem);
FSelectedItems.Remove(AItem);
if FSelectionAnchor = AItem then
FSelectionAnchor := nil;
FItems.Delete(LIndex);
ArrangeItems;
end;
function TImageListView.GetSelected: TImageListViewItem;
begin
if FSelectedItems.Count = 1 then
Result := FSelectedItems[0]
else
Result := nil;
end;
function TImageListView.GetSelectedCount: Integer;
begin
Result := FSelectedItems.Count;
end;
function TImageListView.GetSelectedItems: TArray<TImageListViewItem>;
begin
var LItems := TList<TImageListViewItem>.Create;
try
for var LItem in FItems do
begin
if LItem.Selected then
LItems.Add(LItem);
end;
Result := LItems.ToArray;
finally
LItems.Free;
end;
end;
procedure TImageListView.HandleItemMouseDown(
const AItem: TImageListViewItem;
const Shift: TShiftState);
begin
if not ContainsItem(AItem) then
Exit;
if not FMultiSelect then
begin
ClearSelection;
SetItemSelected(AItem, True);
FSelectionAnchor := AItem;
Exit;
end;
if ssShift in Shift then
begin
var LAnchor := FSelectionAnchor;
if not ContainsItem(LAnchor) then
LAnchor := AItem;
if not (ssCtrl in Shift) then
ClearSelection;
SelectRange(LAnchor.Index, AItem.Index);
FSelectionAnchor := LAnchor;
Exit;
end;
if ssCtrl in Shift then
begin
SetItemSelected(AItem, not AItem.Selected);
if AItem.Selected then
FSelectionAnchor := AItem;
Exit;
end;
ClearSelection;
SetItemSelected(AItem, True);
FSelectionAnchor := AItem;
end;
procedure TImageListView.KeyDown(
var Key: Word;
var KeyChar: WideChar;
Shift: TShiftState);
begin
inherited KeyDown(Key, KeyChar, Shift);
if FItems.Count = 0 then
Exit;
var LCurrentIndex := 0;
if ContainsItem(FSelectionAnchor) then
LCurrentIndex := FSelectionAnchor.Index
else if FSelectedItems.Count > 0 then
LCurrentIndex := FSelectedItems.Last.Index;
var LTargetIndex := LCurrentIndex;
case Key of
vkLeft:
Dec(LTargetIndex);
vkRight:
Inc(LTargetIndex);
vkUp:
Dec(LTargetIndex, GetColumnCount);
vkDown:
Inc(LTargetIndex, GetColumnCount);
else
Exit;
end;
LTargetIndex := Max(0, Min(FItems.Count - 1, LTargetIndex));
if FMultiSelect and (ssShift in Shift) then
begin
var LAnchor := FSelectionAnchor;
if not ContainsItem(LAnchor) then
LAnchor := FItems[LCurrentIndex];
if not (ssCtrl in Shift) then
ClearSelection;
SelectRange(LAnchor.Index, LTargetIndex);
FSelectionAnchor := LAnchor;
end
else
begin
ClearSelection;
SetItemSelected(FItems[LTargetIndex], True);
FSelectionAnchor := FItems[LTargetIndex];
end;
Key := 0;
end;
procedure TImageListView.Notification(
AComponent: TComponent;
Operation: TOperation);
begin
inherited Notification(AComponent, Operation);
if Operation <> opRemove then
Exit;
if AComponent is TImageListViewItem then
DetachItem(TImageListViewItem(AComponent));
end;
procedure TImageListView.QueueDownload(
const AItem: TImageListViewItem;
const AURL: string);
begin
TMonitor.Enter(FQueueLock);
try
FQueue.Enqueue(TImageListViewDownloadRequest.Create(AItem, AURL));
FQueueEvent.SetEvent;
finally
TMonitor.Exit(FQueueLock);
end;
end;
procedure TImageListView.Remove(const AItem: TImageListViewItem);
begin
if AItem = nil then
raise EArgumentException.Create(SItemRequired);
if not ContainsItem(AItem) then
raise EInvalidOperation.Create(SItemNotOwned);
Delete(AItem.Index);
end;
procedure TImageListView.RemoveQueuedDownloads(const AItem: TImageListViewItem);
begin
TMonitor.Enter(FQueueLock);
try
if FQueue.Count = 0 then
Exit;
var LRequests := FQueue.ToArray;
FQueue.Clear;
for var LRequest in LRequests do
begin
if LRequest.Item <> AItem then
FQueue.Enqueue(LRequest);
end;
if FQueue.Count > 0 then
FQueueEvent.SetEvent
else
FQueueEvent.ResetEvent;
finally
TMonitor.Exit(FQueueLock);
end;
end;
procedure TImageListView.Resize;
begin
inherited Resize;
ArrangeItems;
end;
procedure TImageListView.SaveToFile(
const AIndex: Integer;
const AFileName: string);
begin
CheckIndex(AIndex);
SaveToFile(FItems[AIndex], AFileName);
end;
procedure TImageListView.SaveToFile(
const AItem: TImageListViewItem;
const AFileName: string);
begin
if AItem <> nil then
AItem.SaveToFile(AFileName);
end;
procedure TImageListView.SelectRange(
const AStartIndex: Integer;
const AEndIndex: Integer);
begin
CheckIndex(AStartIndex);
CheckIndex(AEndIndex);
var LFirstIndex := Min(AStartIndex, AEndIndex);
var LLastIndex := Max(AStartIndex, AEndIndex);
for var LIndex := LFirstIndex to LLastIndex do
SetItemSelected(FItems[LIndex], True);
end;
procedure TImageListView.SetAniIndicatorTemplate(const AValue: TAniIndicator);
begin
if AValue = nil then
raise EArgumentException.Create(SItemRequired);
FAniIndicatorTemplate.Width := AValue.Width;
FAniIndicatorTemplate.Height := AValue.Height;
FAniIndicatorTemplate.Opacity := AValue.Opacity;
FAniIndicatorTemplate.StyleLookup := AValue.StyleLookup;
FAniIndicatorTemplate.Margins.Assign(AValue.Margins);
FAniIndicatorTemplate.Padding.Assign(AValue.Padding);
UpdateItemTemplates;
end;
procedure TImageListView.SetDownloadThreadCount(const AValue: Integer);
begin
var LNewValue := Max(1, AValue);
if FDownloadThreadCount = LNewValue then
Exit;
StopWorkers;
FDownloadThreadCount := LNewValue;
StartWorkers;
end;
procedure TImageListView.SetItemBackgroundColor(
const AValue: TAlphaColor);
begin
if FItemBackgroundColor = AValue then
Exit;
FItemBackgroundColor := AValue;
UpdateItemTemplates;
end;
procedure TImageListView.SetItemHeight(const AValue: Single);
begin
FItemHeight := Max(MinItemSize, AValue);
ArrangeItems;
end;
procedure TImageListView.SetItemRadius(const AValue: Single);
begin
FItemRadius := Max(0.0, AValue);
UpdateItemTemplates;
end;
procedure TImageListView.SetItemSpacing(const AValue: Single);
begin
var LNewValue := Max(0.0, AValue);
if FItemSpacing = LNewValue then
Exit;
FItemSpacing := LNewValue;
ArrangeItems;
end;
procedure TImageListView.SetItemSelected(
const AItem: TImageListViewItem;
const AValue: Boolean);
begin
if AItem = nil then
Exit;
if AItem.Selected = AValue then
Exit;
AItem.SetSelected(AValue);
if AValue then
begin
if FSelectedItems.IndexOf(AItem) < 0 then
FSelectedItems.Add(AItem);
if Assigned(FOnSelected) then
FOnSelected(Self, AItem);
end
else
FSelectedItems.Remove(AItem);
end;
procedure TImageListView.SetItemWidth(const AValue: Single);
begin
FItemWidth := Max(MinItemSize, AValue);
ArrangeItems;
end;
procedure TImageListView.SetMultiSelect(const AValue: Boolean);
begin
if FMultiSelect = AValue then
Exit;
FMultiSelect := AValue;
if FMultiSelect or (FSelectedItems.Count <= 1) then
Exit;
var LSelectedItem := GetSelectedItems[0];
ClearSelection;
SetItemSelected(LSelectedItem, True);
FSelectionAnchor := LSelectedItem;
end;
procedure TImageListView.SetSelected(const AItem: TImageListViewItem);
begin
if (AItem <> nil) and not ContainsItem(AItem) then
raise EInvalidOperation.Create(SItemNotOwned);
ClearSelection;
if AItem <> nil then
begin
SetItemSelected(AItem, True);
FSelectionAnchor := AItem;
end;
end;
procedure TImageListView.SetSelectionFrameColor(
const AValue: TAlphaColor);
begin
if FSelectionFrameColor = AValue then
Exit;
FSelectionFrameColor := AValue;
UpdateItemTemplates;
end;
procedure TImageListView.SetSelectionFrameThickness(
const AValue: Single);
begin
FSelectionFrameThickness := Max(0.0, AValue);
UpdateItemTemplates;
end;
procedure TImageListView.StartWorkers;
begin
for var LIndex := 1 to FDownloadThreadCount do
FWorkers.Add(TImageListViewDownloadWorker.Create(Self));
end;
procedure TImageListView.StopWorkers;
begin
if FWorkers = nil then
Exit;
for var LWorker in FWorkers do
LWorker.Terminate;
if FQueueEvent <> nil then
FQueueEvent.SetEvent;
for var LWorker in FWorkers do
begin
LWorker.WaitFor;
TThread.RemoveQueuedEvents(LWorker);
end;
FWorkers.Clear;
end;
function TImageListView.TryDequeue(
out ARequest: TImageListViewDownloadRequest): Boolean;
begin
TMonitor.Enter(FQueueLock);
try
Result := FQueue.Count > 0;
if Result then
ARequest := FQueue.Dequeue
else
FQueueEvent.ResetEvent;
finally
TMonitor.Exit(FQueueLock);
end;
end;
procedure TImageListView.UpdateItemTemplates;
begin
for var LItem in FItems do
LItem.ApplyListViewTemplate(Self);
end;
end.

