前作《自訂 CFileDialog 實例》留下一個尾巴:當使用者在資源管理器勾選「隱藏已知副檔名」時,列表控制項 GetItemText 僅回傳不含副檔名的檔名(如 foo),導致 foo.txt、foo.jpg、foo.doc 無法區分。
Windows Shell 使用 PIDL (Pointer to Item ID List) 唯一標識 Shell 物件(檔案、資料夾、連結、虛擬資料夾)。CListCtrl::GetItemData 回傳的 DWORD 正是該項目在當前資料夾中的 相對 PIDL。
// 發送 CDM_GETFOLDERIDLIST 兩次:第一次取長度,第二次取資料
LPITEMIDLIST pidlFolder = NULL;
UINT cb = (UINT)SendMessage(CDM_GETFOLDERIDLIST, 0, 0); // 取得大小
if (cb > 0) {
pidlFolder = (LPITEMIDLIST)CoTaskMemAlloc(cb);
SendMessage(CDM_GETFOLDERIDLIST, cb, (LPARAM)pidlFolder);
}
LVITEM lvi = { 0 };
lvi.mask = LVIF_PARAM;
lvi.iItem = nIndex;
GetListCtrl().GetItem(&lvi);
LPITEMIDLIST pidlRel = (LPITEMIDLIST)lvi.lParam; // 相對 PIDL
// 1. 取得資料夾的 IShellFolder 介面
IShellFolder* pFolder = NULL;
SHBindToParent(pidlFolder, IID_IShellFolder, (void**)&pFolder, NULL);
// 2. 合併 PIDL:資料夾 PIDL + 相對 PIDL = 完整 PIDL
LPITEMIDLIST pidlFull = ILCombine(pidlFolder, pidlRel);
// 3. 用 SHGDN_FORPARSING 取得完整物理路徑(含副檔名)
STRRET strret;
pFolder->GetDisplayNameOf(pidlFull, SHGDN_FORPARSING, &strret);
TCHAR szPath[MAX_PATH];
StrRetToBuf(&strret, pidlFull, szPath, MAX_PATH);
// szPath 即為 "C:\Path\foo.txt" 等完整路徑
// 4. 清理
pFolder->Release();
CoTaskMemFree(pidlFolder);
CoTaskMemFree(pidlFull);
CString CFileDlgHelper::GetItemPathName(int nIndex) {
// 內部實作上述三步驟,並處理記憶體管理、錯誤檢查
// 呼叫端極簡單:
// CString path = m_dlgHelper.GetItemPathName(i);
return path; // 保證含副檔名的完整路徑
}
圖 1:即使資源管理器隱藏副檔名,程式仍正確顯示完整路徑 "C:\Test\foo.txt"
| API | 用途 |
|---|---|
CDM_GETFOLDERIDLIST | 取得對話框當前資料夾 PIDL |
SHBindToParent | 由 PIDL 取得父資料夾 IShellFolder |
ILCombine | 合併兩個 PIDL |
IShellFolder::GetDisplayNameOf | PIDL 轉顯示名稱,SHGDN_FORPARSING 產生完整路徑 |
StrRetToBuf | 統一處理 STRRET 輸出緩衝區 |
CoTaskMemAlloc/Free | Shell 記憶體管理(必須成對使用) |
SHGDN_FORPARSING 可能失敗,需容錯IShellFolder* 避免重複 SHBindToParentCoTaskMemAlloc 必有對應 CoTaskMemFree利用 Shell PIDL 機制,一行 GetItemPathName 徹底解決副檔名隱藏導致的檔名歧義。這也是 Windows Shell 程式設計「以 PIDL 為中心」設計哲學的經典體現。原始碼已整理至附件,歡迎直接整合專案。