本示例演示了列表控件的虛列表和自畫功能,也演示了一些系統外殼的函數和接口的使用方法。
單擊這裡下載本文的代碼。
預備性閱讀
在閱讀本文之前,建議先對列表視圖控件和系統外殼有一個基本的了解。建議閱讀以下SDK文章
ShellFAQ
List-ViewControlsOverview
UsingList-ViewControls
CustomizingaControl'sAppearanceUsingCustomDraw
創建應用程序
使用MFC應用程序向導創建一個SDI應用程序,在最後一步選擇視圖的基類為CListView。創建完成之後,在資源中去掉保存、編輯和打印等功能的菜單和工具欄按鈕(因為這些功能沒有實現)。
虛列表的創建
本文采用虛列表技術,使得顯示信息是在第一次顯示的時候才被獲取。為了創建虛列表,在創建之前需要指定列表的風格
BOOLCPicViewView::PreCreateWindow(CREATESTRUCT&cs)
{
cs.style&=~LVS_TYPEMASK;
cs.style|=LVS_ICON|LVS_OWNERDATA;
returnCListView::PreCreateWindow(cs);
}
同時,因為列表項的Overlay圖標也是被動態獲取的,所以需要設置動態Overlay圖標
voidCPicViewView::OnInitialUpdate()
{
CListView::OnInitialUpdate();
GetListCtrl().SetCallbackMask(LVIS_OVERLAYMASK);
}
緩存顯示信息
在列表需要顯示一個范圍的項目之前,列表會發送LVN_ODCACHEHINT通知,應用程序可以捕獲這個消息來緩存部分列表的顯示信息,以提高性能。
voidCPicViewView::OnOdcachehint(NMHDR*pNMHDR,LRESULT*pResult)
{
NMLVCACHEHINT*pCacheHint=(NMLVCACHEHINT*)pNMHDR;
PrepCache(0,min(5,m_arpFolderItems.GetSize()));
PrepCache(pCacheHint->iFrom,pCacheHint->iTo);
PrepCache(max(0,m_arpFolderItems.GetSize()-5),m_arpFolderItems.GetSize());
*pResult=0;
}
在列表需要顯示一個項目之前,列表會發送LVN_GETDISPINFO通知,應用程序可以捕獲這個消息來提供項目的顯示信息。如果顯示時需要顯示的列表項在緩存中,那麼可以從緩存中獲取顯示信息。否則需要重新從文件獲得。
voidCPicViewView::OnGetdispinfo(NMHDR*pNMHDR,LRESULT*pResult)
{
LV_DISPINFO*pDispInfo=(LV_DISPINFO*)pNMHDR;
if(pDispInfo->item.iItem==-1)return;
HRESULThr=S_OK;
LPCITEMIDLISTpidlItem=m_arpFolderItems[pDispInfo->item.iItem];
CFolderItemInfo*pFolderItemInfo=FindItemInCache(pidlItem);
BOOLbCached=TRUE;
if(pFolderItemInfo==NULL){
bCached=FALSE;
pFolderItemInfo=newCFolderItemInfo;
GetItemInfo(pidlItem,pFolderItemInfo);
}
if(pDispInfo->item.mask&LVIF_TEXT){
lstrcpyn(pDispInfo->item.pszText,pFolderItemInfo->tszDisplayName,pDispInfo->item.cchTextMax);
}
if(pDispInfo->item.mask&LVIF_IMAGE){
pDispInfo->item.iImage=pFolderItemInfo->iIcon;
}
if(pDispInfo->item.mask&LVIF_STATE){
pDispInfo->item.state=pFolderItemInfo->state;
}
if(!bCached)
deletepFolderItemInfo;
*pResult=0;
}
文件圖標的顯示
默認情況下,列表項的圖標就是其系統圖標。首先獲得系統圖像列表
intCPicViewView::OnCreate(LPCREATESTRUCTlpCreateStruct)
{
if(CListView::OnCreate(lpCreateStruct)==-1)
return-1;
HRESULThr=SHGetMalloc(&m_pMalloc);if(FAILED(hr))return-1;
hr=SHGetDesktopFolder(&m_psfDesktop);if(FAILED(hr))return-1;
SHFILEINFOshfi;
ZeroMemory(&shfi,sizeof(SHFILEINFO));
HIMAGELISThi=(HIMAGELIST)SHGetFileInfo(NULL,0,&shfi,sizeof(SHFILEINFO),SHGFI_ICON|SHGFI_SYSICONINDEX|SHGFI_SMALLICON);
GetListCtrl().SetImageList(CImageList::FromHandle(hi),LVSIL_SMALL);
hi=(HIMAGELIST)SHGetFileInfo(NULL,0,&shfi,sizeof(SHFILEINFO),SHGFI_ICON|SHGFI_SYSICONINDEX|SHGFI_LARGEICON);
GetListCtrl().SetImageList(CImageList::FromHandle(hi),LVSIL_NORMAL);
return0;
}
然後在獲取文件信息時,從文件獲得其圖標在系統圖像列表中的索引。
如果列表項是圖像文件,並且從文件成功載入圖像,那麼使用自畫功能以替換默認的圖標。
voidCPicViewView::OnCustomDraw(NMHDR*pNMHDR,LRESULT*pResult)
{
LPNMLVCUSTOMDRAWlpNMCustomDraw=(LPNMLVCUSTOMDRAW)pNMHDR;
switch(lpNMCustomDraw->nmcd.dwDrawStage){
caseCDDS_PREPAINT:*pResult=CDRF_NOTIFYITEMDRAW;return;
caseCDDS_ITEMPREPAINT:*pResult=CDRF_NOTIFYPOSTPAINT;return;
caseCDDS_ITEMPOSTPAINT:
{
intiItem=lpNMCustomDraw->nmcd.dwItemSpec;
if(iItem==-1){
*pResult=CDRF_DODEFAULT;return;
}
CFolderItemInfo*pItemInfo=FindItemInCache(m_arpFolderItems[iItem]);
if(pItemInfo==NULL||pItemInfo->bFailLoadPic||pItemInfo->pic.m_pPict==NULL){
*pResult=CDRF_DODEFAULT;return;
}
CRectrectIcon;
GetListCtrl().GetItemRect(iItem,&rectIcon,LVIR_ICON);
CDC*pDC=CDC::FromHandle(lpNMCustomDraw->nmcd.hdc);
pItemInfo->pic.Render(pDC,rectIcon,rectIcon);
}
*pResult=CDRF_NEWFONT;return;
}
*pResult=0;
}
上面的代碼是使用獲取的文件顯示信息中的圖像,在列表項圖標的區域畫圖。
獲取顯示信息
為了緩存列表項的顯示信息,或者顯示列表項,需要獲取列表項的文字、圖標、Overlay圖標和縮略圖等信息。這裡使用了ILCombine來把緩存中的相對PIDL轉化為完整的Pidl,再據此獲得文件的完整路徑,然後調用OleLoadPicturePath函數載入圖像。
voidCPicViewView::GetItemInfo(LPCITEMIDLISTpidl,CFolderItemInfo*pItemInfo)
{
HRESULThr=theApp.SHGetDisplayNameOf(pidl,pItemInfo->tszDisplayName);
IShellIcon*pShellIcon=NULL;
hr=m_psfFolder->QueryInterface(IID_IShellIcon,(LPVOID*)&pShellIcon);
if(SUCCEEDED(hr)&&pShellIcon){
pShellIcon->GetIconOf(pidl,0,&pItemInfo->iIcon);
pShellIcon->Release();
}
IShellIconOverlay*pShellIconOverlay=NULL;
hr=m_psfFolder->QueryInterface(IID_IShellIconOverlay,(LPVOID*)&pShellIconOverlay);
if(SUCCEEDED(hr)&&pShellIconOverlay){
intnOverlay=0;
pShellIconOverlay->GetOverlayIndex(pidl,&nOverlay);
pItemInfo->state=INDEXTOOVERLAYMASK(nOverlay);
pShellIconOverlay->Release();
}
LPITEMIDLISTpidlItemFull=ILCombine(m_pidlFolder,pidl);
if(pidlItemFull){
if(SHGetPathFromIDList(pidlItemFull,pItemInfo->tszPath)){
USES_CONVERSION;
hr=OleLoadPicturePath(
T2OLE(pItemInfo->tszPath)
,NULL,0,RGB(255,255,255)
,IID_IPicture,(LPVOID*)&pItemInfo->pic.m_pPict);
if(FAILED(hr)){
pItemInfo->bFailLoadPic=TRUE;
TRACE("OleLoadPicturePathfailed%s\r\n",pItemInfo->tszPath);
}
}
}
m_pMalloc->Free(pidlItemFull);
}
}
緩存目錄的數據
在更改目錄時,需要重建目錄內容的緩存。這包括目錄的pidl和IShellFolder接口指針,目錄內容的相對pidl,以及列表項的顯示信息(基於性能上的考慮,列表項的顯示信息是在接收到LVN_ODCACHEHINT通知的時候緩存的)。
LPITEMIDLISTm_pidlFolder;
IShellFolder*m_psfFolder;
CTypedPtrArraym_arpFolderItems;
CTypedPtrMapm_mapCache;
voidCPicViewView::EnterFolder(LPCITEMIDLISTpidl)
{
USES_CONVERSION;
m_pidlFolder=ILClone(pidl);
if(m_pidlFolder){
LPENUMIDLISTppenum=NULL;
LPITEMIDLISTpidlItems=NULL;
ULONGceltFetched;
HRESULThr;
hr=m_psfDesktop->BindToObject(m_pidlFolder,NULL,IID_IShellFolder,(LPVOID*)&m_psfFolder);
if(SUCCEEDED(hr)){
hr=m_psfFolder->EnumObjects(NULL,SHCONTF_FOLDERS|SHCONTF_NONFOLDERS,&ppenum);
if(SUCCEEDED(hr)){
while(hr=ppenum->Next(1,&pidlItems,&celtFetched)==S_OK&&(celtFetched)==1){
m_arpFolderItems.Add(pidlItems);
}
}
}
GetListCtrl().SetItemCount(m_arpFolderItems.GetSize());
}
}
打開文件夾
本應用程序顯示文件夾的內容而不是顯示文檔的內容,所以我重載了打開文件時的處理,顯示目錄選擇對話框而不是文件打開對話框。
voidCPicViewApp::OnFileOpen()
{
TCHARtszDisplayName[_MAX_PATH];
TCHARtszPathSelected[_MAX_PATH];
LPITEMIDLISTpidlSelected=PidlBrowse(m_pMainWnd->GetSafeHwnd(),0,tszDisplayName);
if(pidlSelected){
if(SHGetPathFromIDList(pidlSelected,tszPathSelected)){
CDocument*pDocument=OpenDocumentFile(tszPathSelected);
pDocument->SetTitle(tszDisplayName);
ILFree(pidlSelected);
}
}
}
注意從外殼調用獲得的PIDL一般都需要調用ILFree或者IMalloc::Free釋放。一個例外是調用函數SHBindToParent獲得的相對pidl,因為它是輸入的參數完整pidl的一部分,所以不必另外釋放。
在新建或者打開“文件”時候,文檔需要通知視圖當前文件夾的更改,這是通過調用CDocument::UpdateAllViews和重載CView::OnUpdate實現的。視圖對這個通知的處理是清除上一個目錄的緩存數據,緩存新目錄的數據,以及更新文檔標題。
打開文件或者目錄
為了使用方便,雙擊列表項時可以在同一窗口打開子目錄,或者調用系統的默認處理程序打開文件。如果文件是快捷方式,那麼打開快捷方式的目標。
voidCPicViewView::OnDblclk(NMHDR*pNMHDR,LRESULT*pResult)
{
LPNMLISTVIEWlpnm=(LPNMLISTVIEW)pNMHDR;
if(lpnm->iItem==-1)return;
*pResult=0;
HRESULThr=S_OK;
LPCITEMIDLISTpidlItem=m_arpFolderItems[lpnm->iItem];
LPITEMIDLISTpidlItemFull=ILCombine(m_pidlFolder,pidlItem);
LPITEMIDLISTpidlItemTarget=NULL;
hr=theApp.SHGetTargetFolderIDList(pidlItemFull,&pidlItemTarget);
if(pidlItemTarget){
if(theApp.ILIsFolder(pidlItemTarget)){
CFolderChangeFolderChange;
FolderChange.m_pidlFolder=pidlItemTarget;
OnFolderChange(&FolderChange);
}
else{
SHELLEXECUTEINFOShExecInfo;
ShExecInfo.cbSize=sizeof(SHELLEXECUTEINFO);
ShExecInfo.fMask=SEE_MASK_IDLIST;
ShExecInfo.hwnd=NULL;
ShExecInfo.lpVerb=NULL;
ShExecInfo.lpFile=NULL;
ShExecInfo.lpIDList=pidlItemTarget;
ShExecInfo.lpParameters=NULL;
ShExecInfo.lpDirectory=NULL;
ShExecInfo.nShow=SW_MAXIMIZE;
ShExecInfo.hInstApp=NULL;
ShellExecuteEx(&ShExecInfo);
}
m_pMalloc->Free(pidlItemTarget);
m_pMalloc->Free(pidlItemFull);
}
}
性能的優化
為了更好的用戶體驗,可以使用自定義的圖標大小(這需要完全自行繪制列表項的圖標區域),用單獨的線程來載入圖像,或者使用調整到圖標大小的縮略圖緩沖(這樣每次繪制時不必拉伸圖像)。但是這超出了本文的范圍。有興趣的讀者可以自己試一下。
參考
需要更多信息的話,可以參考
ShellFAQ
List-ViewControlsOverview
UsingList-ViewControls
CustomizingaControl'sApearanceUsingCustomDraw