預(yù)備性閱讀
在閱讀本文之前,建議先對列表視圖控件和系統(tǒng)外殼有一個基本的了解。建議閱讀以下SDK文章
Shell FAQ
List-View Controls Overview
Using List-View Controls
Customizing a Control's Appearance Using Custom Draw
創(chuàng)建應(yīng)用程序
使用MFC應(yīng)用程序向?qū)?chuàng)建一個SDI應(yīng)用程序,在最后一步選擇視圖的基類為CListView。創(chuàng)建完成之后,在資源中去掉保存、編輯和打印等功能的菜單和工具欄按鈕(因為這些功能沒有實現(xiàn))。
虛列表的創(chuàng)建
本文采用虛列表技術(shù),使得顯示信息是在第一次顯示的時候才被獲取。為了創(chuàng)建虛列表,在創(chuàng)建之前需要指定列表的風(fēng)格
BOOL CPicViewView::PreCreateWindow(CREATESTRUCT& cs)
{
??? cs.style&=~LVS_TYPEMASK;
??? cs.style|=LVS_ICON|LVS_OWNERDATA;
??? return CListView::PreCreateWindow(cs);
}
同時,因為列表項的Overlay圖標(biāo)也是被動態(tài)獲取的,所以需要設(shè)置動態(tài)Overlay圖標(biāo)
void CPicViewView::OnInitialUpdate()
{
??? CListView::OnInitialUpdate();
??? GetListCtrl().SetCallbackMask(LVIS_OVERLAYMASK);
}
緩存顯示信息
在列表需要顯示一個范圍的項目之前,列表會發(fā)送LVN_ODCACHEHINT通知,應(yīng)用程序可以捕獲這個消息來緩存部分列表的顯示信息,以提高性能。
void CPicViewView::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;
}
在列表需要顯示一個項目之前,列表會發(fā)送LVN_GETDISPINFO通知,應(yīng)用程序可以捕獲這個消息來提供項目的顯示信息。如果顯示時需要顯示的列表項在緩存中,那么可以從緩存中獲取顯示信息。否則需要重新從文件獲得。
void CPicViewView::OnGetdispinfo(NMHDR* pNMHDR, LRESULT* pResult)
{
??? LV_DISPINFO* pDispInfo = (LV_DISPINFO*)pNMHDR;
??? if(pDispInfo->item.iItem==-1)return;
??? HRESULT hr=S_OK;
??? LPCITEMIDLIST pidlItem=m_arpFolderItems[pDispInfo->item.iItem];
??? CFolderItemInfo* pFolderItemInfo=FindItemInCache(pidlItem);
??? BOOL bCached=TRUE;
??? if(pFolderItemInfo==NULL){
??????? bCached=FALSE;
??????? pFolderItemInfo=new CFolderItemInfo;
??????? 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)
??????? delete pFolderItemInfo;
??? *pResult = 0;
}
文件圖標(biāo)的顯示
默認(rèn)情況下,列表項的圖標(biāo)就是其系統(tǒng)圖標(biāo)。首先獲得系統(tǒng)圖像列表
int CPicViewView::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
??? if (CListView::OnCreate(lpCreateStruct) == -1)
??????? return -1;
??? HRESULT hr = SHGetMalloc(&m_pMalloc); if(FAILED(hr)) return -1;
??? hr = SHGetDesktopFolder(&m_psfDesktop);if(FAILED(hr)) return -1;
??? SHFILEINFO shfi;
??? ZeroMemory(&shfi,sizeof(SHFILEINFO));
??? HIMAGELIST hi=(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);
??? return 0;
}
然后在獲取文件信息時,從文件獲得其圖標(biāo)在系統(tǒng)圖像列表中的索引。
如果列表項是圖像文件,并且從文件成功載入圖像,那么使用自畫功能以替換默認(rèn)的圖標(biāo)。
void CPicViewView::OnCustomDraw(NMHDR* pNMHDR, LRESULT* pResult)
{
??? LPNMLVCUSTOMDRAW lpNMCustomDraw = (LPNMLVCUSTOMDRAW) pNMHDR;
??? switch(lpNMCustomDraw ->nmcd.dwDrawStage) {
??????? case CDDS_PREPAINT : *pResult=CDRF_NOTIFYITEMDRAW;return;
??????? case CDDS_ITEMPREPAINT:*pResult=CDRF_NOTIFYPOSTPAINT;return;
??????? case CDDS_ITEMPOSTPAINT:
??????? {
??????????? int iItem=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;
??????????? }
??????????? CRect rectIcon;
??????????? 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;
}
上面的代碼是使用獲取的文件顯示信息中的圖像,在列表項圖標(biāo)的區(qū)域畫圖。
獲取顯示信息
為了緩存列表項的顯示信息,或者顯示列表項,需要獲取列表項的文字、圖標(biāo)、Overlay圖標(biāo)和縮略圖等信息。這里使用了ILCombine來把緩存中的相對PIDL轉(zhuǎn)化為完整的Pidl,再據(jù)此獲得文件的完整路徑,然后調(diào)用OleLoadPicturePath函數(shù)載入圖像。
void CPicViewView::GetItemInfo(LPCITEMIDLIST pidl,CFolderItemInfo* pItemInfo)
{
??? HRESULT hr = 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){
??????? int nOverlay=0;
??????? pShellIconOverlay->GetOverlayIndex(pidl,&nOverlay);
??????? pItemInfo->state=INDEXTOOVERLAYMASK (nOverlay);
??????? pShellIconOverlay->Release();
??? }
??? LPITEMIDLIST pidlItemFull=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("OleLoadPicturePath failed %s\r\n",pItemInfo->tszPath);
??????????????? }
??????????? }
??????? }
??????? m_pMalloc->Free(pidlItemFull);
??? }
}
緩存目錄的數(shù)據(jù)
在更改目錄時,需要重建目錄內(nèi)容的緩存。這包括目錄的pidl和IShellFolder接口指針,目錄內(nèi)容的相對pidl,以及列表項的顯示信息(基于性能上的考慮,列表項的顯示信息是在接收到LVN_ODCACHEHINT通知的時候緩存的)。
LPITEMIDLIST m_pidlFolder;
IShellFolder * m_psfFolder;
CTypedPtrArray<CPtrArray,LPITEMIDLIST> m_arpFolderItems;
CTypedPtrMap<CMapPtrToPtr,LPITEMIDLIST,CFolderItemInfo*> m_mapCache;
?
void CPicViewView::EnterFolder(LPCITEMIDLIST pidl)
{
??? USES_CONVERSION;
??? m_pidlFolder=ILClone(pidl);
??? if(m_pidlFolder){
??????? LPENUMIDLIST ppenum = NULL;
??????? LPITEMIDLIST pidlItems = NULL;
??????? ULONG celtFetched;
??????? HRESULT hr;
??????? 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());
??? }
}
?
打開文件夾
本應(yīng)用程序顯示文件夾的內(nèi)容而不是顯示文檔的內(nèi)容,所以我重載了打開文件時的處理,顯示目錄選擇對話框而不是文件打開對話框。
void CPicViewApp::OnFileOpen()
{
??? TCHAR tszDisplayName[_MAX_PATH];
??? TCHAR tszPathSelected[_MAX_PATH];
??? LPITEMIDLIST pidlSelected=PidlBrowse(m_pMainWnd->GetSafeHwnd(),0,tszDisplayName);
??? if(pidlSelected){
??????? if(SHGetPathFromIDList(pidlSelected,tszPathSelected)){
??????????? CDocument* pDocument=OpenDocumentFile(tszPathSelected);
??????????? pDocument->SetTitle(tszDisplayName);
??????????? ILFree(pidlSelected);
??????? }
??? }
}
注意從外殼調(diào)用獲得的PIDL一般都需要調(diào)用ILFree或者IMalloc::Free釋放。一個例外是調(diào)用函數(shù)SHBindToParent獲得的相對pidl,因為它是輸入的參數(shù)完整pidl的一部分,所以不必另外釋放。
在新建或者打開“文件”時候,文檔需要通知視圖當(dāng)前文件夾的更改,這是通過調(diào)用CDocument::UpdateAllViews和重載CView::OnUpdate實現(xiàn)的。視圖對這個通知的處理是清除上一個目錄的緩存數(shù)據(jù),緩存新目錄的數(shù)據(jù),以及更新文檔標(biāo)題。
?
打開文件或者目錄
為了使用方便,雙擊列表項時可以在同一窗口打開子目錄,或者調(diào)用系統(tǒng)的默認(rèn)處理程序打開文件。如果文件是快捷方式,那么打開快捷方式的目標(biāo)。
void CPicViewView::OnDblclk(NMHDR* pNMHDR, LRESULT* pResult)
{
??? LPNMLISTVIEW lpnm=(LPNMLISTVIEW)pNMHDR;
??? if(lpnm->iItem==-1)return;
??? *pResult = 0;
??? HRESULT hr=S_OK;
??? LPCITEMIDLIST pidlItem=m_arpFolderItems[lpnm->iItem];
??? LPITEMIDLIST pidlItemFull=ILCombine(m_pidlFolder,pidlItem);
??? LPITEMIDLIST pidlItemTarget=NULL;
??? hr=theApp.SHGetTargetFolderIDList(pidlItemFull,&pidlItemTarget);
??? if(pidlItemTarget){
??????? if(theApp.ILIsFolder(pidlItemTarget)){
??????????? CFolderChange FolderChange;
??????????? FolderChange.m_pidlFolder=pidlItemTarget;
??????????? OnFolderChange(&FolderChange);
??????? }
??????? else{
??????????? SHELLEXECUTEINFO ShExecInfo;
??????????? 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);
??? }
}
?
性能的優(yōu)化
為了更好的用戶體驗,可以使用自定義的圖標(biāo)大小(這需要完全自行繪制列表項的圖標(biāo)區(qū)域),用單獨的線程來載入圖像,或者使用調(diào)整到圖標(biāo)大小的縮略圖緩沖(這樣每次繪制時不必拉伸圖像)。但是這超出了本文的范圍。有興趣的讀者可以自己試一下。
參考
需要更多信息的話,可以參考
Shell FAQ
List-View Controls Overview
Using List-View Controls
Customizing a Control's Appearance Using Custom Draw
本文來自焦點核(X)軟件安全技術(shù)網(wǎng),原文地址:http://www.xfocusx.com