在 TComboBox、TListBox、TListView、TTreeView 等控件中,根據文件類型動態顯示系統關聯圖標(如 .txt 顯示記事本圖標、.exe 顯示程序圖標),實現類似資源管理器的視覺效果。
SHGetFileInfo (ShellAPI) 根據文件路徑/屬性獲取 HICON。ImageList_AddIcon (CommCtrl) 將 HICON 放入 TImageList,獲得索引。Style := csOwnerDrawVariable/lbOwnerDrawVariable + OnDrawItem 繪製圖標+文本,或直接設 ImageList 屬性 + ImageIndex。
function SHGetFileInfo(
pszPath: PAnsiChar; // 文件路徑或 PIDL
dwFileAttributes: DWORD; // 文件屬性 (配合 SHGFI_USEFILEATTRIBUTES)
var psfi: TSHFileInfo; // 輸出結構
cbFileInfo: UINT; // 結構大小
uFlags: UINT // 請求信息類型組合
): DWORD;
| 字段 | 類型 | 含義 |
|---|---|---|
| hIcon | HICON | 圖標句柄 (核心輸出) |
| iIcon | Integer | 系統圖標索引 (可用於 ImageList 直接映射) |
| dwAttributes | DWORD | 文件屬性 |
| szDisplayName | array[0..MAX_PATH-1] of AnsiChar | 顯示名稱 |
| szTypeName | array[0..79] of AnsiChar | 類型名稱 (如「Text Document」) |
| 標志 | 值 | 用途 |
|---|---|---|
| SHGFI_ICON | $100 | 獲取 hIcon |
| SHGFI_DISPLAYNAME | $200 | 獲取顯示名 |
| SHGFI_TYPENAME | $400 | 獲取類型名 |
| SHGFI_ATTRIBUTES | $800 | 獲取屬性 |
| SHGFI_LARGEICON | $0 | 大圖標 (32x32 默認) |
| SHGFI_SMALLICON | $1 | 小圖標 (16x16) |
| SHGFI_USEFILEATTRIBUTES | $10 | 不訪問磁盤,僅按擴展名獲取圖標 (極快) |
| SHGFI_SYSICONINDEX | $4000 | 返回 iIcon (系統圖標索引),不返回 hIcon |
function ImageList_AddIcon(himl: HIMAGELIST; hIcon: HICON): Integer;
// 返回圖標在 ImageList 中的索引,-1 表示失敗
unit Unit1;
interface
uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs, StdCtrls, ComCtrls, ShellAPI, CommCtrl;
type
TForm1 = class(TForm)
ComboBox1: TComboBox;
ImageList1: TImageList;
btnLoad: TButton;
procedure FormCreate(Sender: TObject);
procedure btnLoadClick(Sender: TObject);
procedure ComboBox1DrawItem(Control: TWinControl; Index: Integer;
Rect: TRect; State: TOwnerDrawState);
private
procedure LoadFiles(const Dir: string);
function GetFileIconIndex(const FileName: string): Integer;
end;
var
Form1: TForm1;
implementation
{$R *.dfm}
procedure TForm1.FormCreate(Sender: TObject);
begin
// 1. ImageList 設置小圖標尺寸
ImageList1.Width := 16;
ImageList1.Height := 16;
// 關鍵:創建時指定尺寸,大圖標需 32x32
ImageList1.Handle := ImageList_Create(16, 16, ILC_COLOR32 or ILC_MASK, 0, 10);
// 2. ComboBox 設置 OwnerDraw 以繪製圖標+文本
ComboBox1.Style := csOwnerDrawVariable;
ComboBox1.ItemHeight := 20; // 足夠高度容納圖標+文本
end;
procedure TForm1.btnLoadClick(Sender: TObject);
begin
with TFileOpenDialog.Create(Self) do
try
Options := [fdoPickFolders];
if Execute then
LoadFiles(FileName);
finally
Free;
end;
end;
procedure TForm1.LoadFiles(const Dir: string);
var
SR: TSearchRec;
Idx: Integer;
begin
ComboBox1.Items.BeginUpdate;
ImageList1.Clear;
try
// 重新創建 Handle 確保尺寸生效
ImageList1.Handle := ImageList_Create(16, 16, ILC_COLOR32 or ILC_MASK, 0, 10);
ComboBox1.Items.Clear;
if FindFirst(IncludeTrailingPathDelimiter(Dir) + '*.*', faAnyFile, SR) = 0 then
try
repeat
if (SR.Name <> '.') and (SR.Name <> '..') then
begin
Idx := GetFileIconIndex(IncludeTrailingPathDelimiter(Dir) + SR.Name);
ComboBox1.Items.AddObject(SR.Name, TObject(Idx)); // 文本 + 圖標索引
end;
until FindNext(SR) <> 0;
finally
FindClose(SR);
end;
finally
ComboBox1.Items.EndUpdate;
end;
end;
function TForm1.GetFileIconIndex(const FileName: string): Integer;
var
ShInfo: TSHFileInfo;
hIcon: HICON;
begin
Result := -1;
// 使用 SHGFI_USEFILEATTRIBUTES 不訪問磁盤,極快
if SHGetFileInfo(PChar(FileName), FILE_ATTRIBUTE_NORMAL, ShInfo,
SizeOf(ShInfo), SHGFI_ICON or SHGFI_SMALLICON or SHGFI_USEFILEATTRIBUTES) <> 0 then
begin
hIcon := ShInfo.hIcon;
if hIcon <> 0 then
begin
Result := ImageList_AddIcon(ImageList1.Handle, hIcon);
DestroyIcon(hIcon); // SHGetFileInfo 返回的 hIcon 需手動銷毀
end;
end;
end;
procedure TForm1.ComboBox1DrawItem(Control: TWinControl; Index: Integer;
Rect: TRect; State: TOwnerDrawState);
var
Idx: Integer;
Text: string;
ImgX, ImgY, TxtX: Integer;
begin
with (Control as TComboBox) do
begin
Canvas.FillRect(Rect); // 清背景
Text := Items[Index];
Idx := Integer(Items.Objects[Index]); // 取回圖標索引
// 計算佈局
ImgX := Rect.Left + 2;
ImgY := Rect.Top + (Rect.Height - ImageList1.Height) div 2;
TxtX := ImgX + ImageList1.Width + 4;
// 繪製圖標
if (Idx >= 0) and (Idx < ImageList1.Count) then
ImageList1.Draw(Canvas, ImgX, ImgY, Idx);
// 繪製文本
Canvas.TextOut(TxtX, Rect.Top + (Rect.Height - Canvas.TextHeight(Text)) div 2, Text);
// 選中態高亮
if odSelected in State then
Canvas.FrameRect(Rect);
end;
end;
end.
SHGFI_USEFILEATTRIBUTES:離線獲取圖標的神器DestroyIcon 必須調用SHGetFileInfo 返回的 hIcon 由調用者擁有,ImageList_AddIcon 內部複製圖標位圖,**不接管句柄所有權**,必須在添加後 DestroyIcon(hIcon),否則 GDI 對象洩漏。
csOwnerDrawVariable vs csOwnerDrawFixedVariable:每項高度可異,觸發 OnMeasureItem + OnDrawItem,靈活但略慢。Fixed:統一 ItemHeight,僅觸發 OnDrawItem,性能更好。圖標固定 16x16 時推薦 Fixed。iIcon) 直接映射優化
// 獲取系統圖標索引,直接用於 ListView/TreeView 的 ImageIndex
SHGetFileInfo(PChar(FileName), 0, ShInfo, SizeOf(ShInfo),
SHGFI_SYSICONINDEX or SHGFI_SMALLICON or SHGFI_USEFILEATTRIBUTES);
// ShInfo.iIcon 即為系統映像列表中的索引
// 需先獲取系統 ImageList Handle: SHGetFileInfo('', 0, ShInfo, SizeOf(ShInfo), SHGFI_SYSICONINDEX or SHGFI_SMALLICON);
// 然後控件.Images.Handle := ShInfo.hIcon (系統 ImageList)
// 但系統 ImageList 只讀,不能添加自定義圖標,故常用自建 ImageList + ImageList_AddIcon
SHGFI_LARGEICON / 32x32)
ImageList1.Width := 32;
ImageList1.Height := 32;
ImageList1.Handle := ImageList_Create(32, 32, ILC_COLOR32 or ILC_MASK, 0, 10);
// SHGetFileInfo 使用 SHGFI_LARGEICON (默認) 獲取 32x32 hIcon
| 控件 | 模式 A: OwnerDraw 繪製 | 模式 B: 內建 ImageList 屬性 |
|---|---|---|
| TComboBox | csOwnerDrawFixed + OnDrawItem | 不支援 (無 Images 屬性) |
| TListBox | lbOwnerDrawFixed + OnDrawItem | 不支援 |
| TListView | OnCustomDrawItem (vsReport) | SmallImages/LargeImages + Item.ImageIndex ✓ |
| TTreeView | OnCustomDrawItem | Images + Node.ImageIndex/SelectedIndex ✓ |
| TStatusBar | Panel.Style := psOwnerDraw + OnDrawPanel | 不直接支援 |
動態圖標三部曲:SHGetFileInfo 獲取 hIcon → ImageList_AddIcon 入列表 → 控件綁定繪製。關鍵細節:**USEFILEATTRIBUTES 離線加速、DestroyIcon 防洩漏、OwnerDraw 佈局計算、ImageList 尺寸預創建**。掌握此套流程,即可為任何文件列表、進程列表、插件管理器賦予原生系統級視覺體驗。