# Part X - Implementing a Drag and Drop Source
原作 :[**Michael Dunn**](http://www.codeproject.com/wtl/WTL4MFC10.asp)
翻譯 :[yaker](http://www.yakergong.com/blog)
## 內容
* [簡介](#intro)
* [創建工程](#starting)
* [處理 File-Open 操作](#fileopen)
* [拖動源](#dragsource)
* [拖動源的接口](#dragsrcitf)
* [調用者的輔助方法](#dragsrchelpers)
* [IDropSource接口的方法](#IDropSource)
* [查看器里拖放操作的實現](#appdragdrop)
* [添加一個最近使用文件列表](#mru)
* [設置MRU對象](#mrusetup)
* [處理MRU命令并更新列表](#handlingmru)
* [保存MRU列表](#savemru)
* [其他的UI相關的東西](#otherui)
* [半透明的拖放效果](#dragimage)
* [半透明的矩形選擇框](#alphamarquee)
* [按列排序](#lvsortcol)
* [使用平鋪視圖模式](#lvtilemode)
* [設置平鋪視圖圖像列表](#tileimglistsetup)
* [使用平鋪視圖圖片列表](#tileimglistusing)
* [設置而外的幾行文字](#tileaddllines)
* [版權與協議](#copying)
* [修訂歷史](#revisionhistory)
## 簡介
支持拖放操作是很多現代程序的特性。雖然實現拖動源很直接,但是釋放目標則要復雜得多。MFC 中的類 `COleDataObject` 和 `COleDropSource` 可以輔助管理拖動源所必須提供的數據,可是WTL中并沒有提供這樣的輔助類。對我們這些WTL用戶來說,幸運的是: [Raymond Chen](http://blogs.msdn.com/oldnewthing/) 寫了一篇MSDN文章 ("[The Shell Drag/Drop Helper Object Part 2](http://msdn.microsoft.com/library/en-us/dnwui/html/ddhelp_pt2.asp)") ,文中提供了一個 `IDataObject`的純C++語言實現,這對于在WTL程序中實現拖放操作來說是一個巨大的幫助。
這篇文章的樣例工程是一個CAB文件查看工具,它支持通過將文件從查看工具窗口拖動到windows文件夾窗口來實現解壓操作。這篇文章也將討論一些關于框架窗口的主題,比如處理 File-Open 操作和與MFC中文檔視圖框架類似的數據管理。我也將介紹 WTL的 MRU (最近經常使用,most-recently-used) 文件列表類,還有一些6.0版本列表視圖空間的一些新特性。
**注意**: 你需要下載安裝 Microsoft 的 CAB SDK才能編譯樣例代碼。Microsoft的Konwledge Base網站中的一篇文章里有CAB SDK的鏈接: [Q310618](http://support.microsoft.com/?kbid=310618). 樣例程序假定SDK被放置在源代碼目錄下名為"cabsdk"的目錄里。
注意,如果你在安裝WTL或者編譯樣例代碼時遇到任何問題,在提問之前請閱讀 [第一部分里 readme 這一節](./parti.html#readme)
## 創建工程
現在開始創建我們的 CAB 查看器程序,運行WTL AppWizard 然后創建一個名為 _WTLCabView_ 的工程。它是一個SDI(single document interface,單文檔界面)應用程序,在第一頁選擇“SDI Application”:

下一頁,取消選中 _Command Bar_ ,然后將 _View Type_ 改為 _List View_. 向導會為我們的視圖窗口創建一個C++類,賓切它繼承自 `CListViewCtrl` 類。

視圖窗口類看起來像這樣:
```
class CWTLCabViewView :
public CWindowImpl<CWTLCabViewView, CListViewCtrl>
{
public:
DECLARE_WND_SUPERCLASS(NULL, CListViewCtrl::GetWndClassName())
// Construction
CWTLCabViewView();
// Maps
BEGIN_MSG_MAP(CWTLCabViewView)
END_MSG_MAP()
// ...
};
```
和[第二部分](./PartII.htm)我們使用的視圖窗口一樣,我們可以使用`CWindowImpl`的第三方模板參數設置默認窗口風格:
```
#define VIEW_STYLES \
(LVS_REPORT | LVS_SHOWSELALWAYS | \
LVS_SHAREIMAGELISTS | LVS_AUTOARRANGE )
#define VIEW_EX_STYLES (WS_EX_CLIENTEDGE)
class CWTLCabViewView :
public CWindowImpl<CWTLCabViewView, CListViewCtrl,
CWinTraitsOR<VIEW_STYLES,VIEW_EX_STYLES> >
{
//...
};
```
因為WTL不包含 文檔/視圖 框架,視圖類要承擔UI和保存CAB文件信息。拖放操作過程中操作的數據結構是 `CDraggedFileInfo`:
```
struct CDraggedFileInfo
{
// Data set at the beginning of a drag/drop:
CString sFilename; // name of the file as stored in the CAB
CString sTempFilePath; // path to the file we extract from the CAB
int nListIdx; // index of this item in the list ctrl
// Data set while extracting files:
bool bPartialFile; // true if this file is continued in another cab
CString sCabName; // name of the CAB file
bool bCabMissing; // true if the file is partially in this cab and
// the CAB it's continued in isn't found, meaning
// the file can't be extracted
CDraggedFileInfo ( const CString& s, int n ) :
sFilename(s), nListIdx(n), bPartialFile(false),
bCabMissing(false)
{ }
};
```
視圖類對于初始化,操作文件列表和在開始拖放操作時建立一個 `CDraggedFileInfo` 的列表相應的方法(函數)。我不想花費太多時間解釋UI的內部工作原理,因為這篇文章是關于拖放操作的實現的,所以關于UI的部分請參考工程里的 _WTLCabViewView.h_ 文件。
## 處理 File-Open 操作
想要查看一個CAB文件,用戶可以使用 _File-Open_ 命令,然后選擇一個CAB文件。向導為 `CMainFrame` 生成的代碼包含了處理 _File-Open_ 菜單項的代碼:
```
BEGIN_MSG_MAP(CMainFrame)
COMMAND_ID_HANDLER_EX(ID_FILE_OPEN, OnFileOpen)
END_MSG_MAP()
```
`OnFileOpen()` 使用了 `CMyFileDialog` 類,在 [第四部分](./PartIX.htm#usingcfiledialog) 中介紹的改進版的 `CFileDialog` 類,來顯示一個標準的打開文件對話框。
```
void CMainFrame::OnFileOpen (
UINT uCode, int nID, HWND hwndCtrl )
{
CMyFileDialog dlg ( true, _T("cab"), 0U,
OFN_HIDEREADONLY|OFN_FILEMUSTEXIST,
IDS_OPENFILE_FILTER, *this );
if ( IDOK == dlg.DoModal(*this) )
ViewCab ( dlg.m_szFileName );
}
```
`OnFileOpen()` 調用了 `ViewCab()`的幫助函數:
```
void CMainFrame::ViewCab ( LPCTSTR szCabFilename )
{
if ( EnumCabContents ( szCabFilename ) )
m_sCurrentCabFilePath = szCabFilename;
}
```
`EnumCabContents()` 函數比較復雜,并且使用了 CAB SDK 調用來枚舉 `OnFileOpen()`里選中CAB文件中的內容,并且填充視圖窗口。雖然目前 `ViewCab()` 的功能還不夠,我們會逐漸添加代碼來實現更多的功能。這里 CAB查看器 打開一個CAB文件時的效果:

`EnumCabContents()` 在視圖類中使用了兩個方法來填充UI: `AddFile()` 和 `AddPartialFile()`。當一個文件部分存儲于該CAB文件(其余的部分在另外的CAB文件內)時調用 `AddPartialFile()` 方法。上圖所示的截圖中,列表中的第一個文件就是部分存儲于該CAB文件中。剩余的項使用 `AddFile()` 方法添加到視圖窗口中。這兩種方法都為添加的文件創建了同一種數據結構,所以視圖能夠獲得它所顯示的文件的細節信息。
如果 `EnumCabContents()` 返回值是 true,那說明枚舉過程和UI建立都成功的執行。如果我們僅僅是想寫個簡單的CAB查看器,現在做的這些就已經足夠了,但是程序就不會那么有趣了。要讓這個工具變得真正易用起來,我們要為它添加拖放操作使得用戶可以通過拖動來解壓文件。
## 拖動源
拖動源是實現了以下兩個接口的 COM對象: `IDataObject` 和 `IDropSource`. `IDataObject` 用來存儲拖放操作過程中客戶端想要傳輸的所有數據;對我們來說就是一個 `HDROP` 結構,結構體里保存要從CAB文件里解壓出來的文件列表 。OLE在拖放操作過程中調用 `IDropSource` 接口來通知事件的來源。
### 拖動源的接口
實現了拖動源的C++類是 `CDragDropSource`. 它開始于 [這篇MSDN文章](http://msdn.microsoft.com/library/en-us/dnwui/html/ddhelp_pt2.asp) 里描述的 `IDataObject` 的實現 ,簡介里我們介紹了這篇文章。在那篇文章里你能找到關于這段代碼的全部細節信息,這里我就不在贅述了。接下來我們向類中添加了 `IDropSource` 和它的兩個方法:
```
class CDragDropSource :
public CComObjectRootEx<CComSingleThreadModel>,
public CComCoClass<CDragDropSource>,
public IDataObject,
public IDropSource
{
public:
// Construction
CDragDropSource();
// Maps
BEGIN_COM_MAP(CDragDropSource)
COM_INTERFACE_ENTRY(IDataObject)
COM_INTERFACE_ENTRY(IDropSource)
END_COM_MAP()
// IDataObject methods not shown...
// IDropSource
STDMETHODIMP QueryContinueDrag (
BOOL fEscapePressed, DWORD grfKeyState );
STDMETHODIMP GiveFeedback ( DWORD dwEffect );
};
```
### 調用者的輔助方法
`CDragDropSource` 使用了一些輔助方法包裝了 `IDataObject`的管理和拖放操作過程中的通信。一次拖放操作遵循以下模式:
1. 用戶開始一次拖放操作時主框架得到通知。
2. 主框架調用視圖窗口的方法來創建一個被拖動的文件的列表。視圖窗口類使用一個 `vector<CDraggedFileInfo>`結構返回這些信息。
3. 主框架創建一個 `CDragDropSource` 對象并且把 vector<CDraggedFileInfo>傳遞給它,這樣它就可以了解要從CAB里解壓的文件的信息。
4. 主框架開始拖放操作。
5. 如果用戶在一個適當的位置釋放目標,`CDragDropSource` 對象會解壓縮相應的文件。
6. 主框架更新UI來指出任何未能解壓的文件。
第 3-6 步是通過輔助方法來實現的。初始化功能由 `Init()` 方法實現:
```
bool Init(LPCTSTR szCabFilePath, vector<CDraggedFileInfo>& vec);
```
`Init()` 會復制數據到受保護(protected)的成員變量里,填充到一個 `HDROP` 結構里,并且存儲起來。`Init()` 所做的另外一項重要工作就是:它在臨時目錄為每個被拖放的文件創建了一個0比特的臨時文件。舉個例子,比如用戶拖動了CAB文件內的 _buffy.txt_ 和 _willow.txt_ 兩個文件, `Init()` 函數會在臨時目錄創建兩個相應的同名文件。僅當釋放目標驗證了從`HDROP`里讀出的文件名的合法性之后才會產生這樣的操作,如果文件不存在,釋放操作會失敗。
下一個要介紹的函數是 `DoDragDrop()`:
```
HRESULT DoDragDrop(DWORD dwOKEffects, DWORD* pdwEffect);
```
`DoDragDrop()` 從參數 `dwOKEffects` 里獲取了一系列 `DROPEFFECT_*` 標志位,說明了拖動源上允許進行的操作。它查詢必要的借口,然后調用系統API `DoDragDrop()`。若果拖放成功,`*pdwEffect` 被置為 `DROPEFFECT_*` 系列的值,該值正好反映了用戶想做的操作。
最后一個方法是 `GetDragResults()`:
```
const vector<CDraggedFileInfo>& GetDragResults();
```
`CDragDropSource` 對象維護了一個 `vector<CDraggedFileInfo>` 結構,在拖放操作過程中這個結構也被更新了。如果一個文件只是部分的存在于這個CAB文件中,或者解壓縮錯誤,`CDraggedFileInfo` 都會被更新。主框架調用 `GetDragResults()` 來獲取這個vector,所以它能夠檢查錯誤,并相應地更新UI。
### IDropSource接口的方法
`IDropSource` 接口要提供的第一個方法是 `GiveFeedback()`,它用來通知拖動源用戶想要做的操作(移動,復制或者鏈接)。如果需要的話,拖動源也可以更改光標。`CDragDropSource` 跟蹤用戶操作,并且通知OLE使用默認的拖放圖標。
```
STDMETHODIMP CDragDropSource::GiveFeedback(DWORD dwEffect)
{
m_dwLastEffect = dwEffect;
return DRAGDROP_S_USEDEFAULTCURSORS;
}
```
另外一個 `IDropSource` 方法是 `QueryContinueDrag()`. 當用戶移動光標的時候OLE調用這個方法,并且通知拖動源哪些鼠標鍵和鍵盤按鍵被按下。如下是多數 `QueryContinueDrag()` 實現所采用的樣例代碼。
```
STDMETHODIMP CDragDropSource::QueryContinueDrag (
BOOL fEscapePressed, DWORD grfKeyState )
{
// If ESC was pressed, cancel the drag.
// If the left button was released, do drop processing.
if ( fEscapePressed )
return DRAGDROP_S_CANCEL;
else if ( !(grfKeyState & MK_LBUTTON) )
{
// If the last DROPEFFECT we got in GiveFeedback()
// was DROPEFFECT_NONE, we abort because the allowable
// effects of the source and target don't match up.
if ( DROPEFFECT_NONE == m_dwLastEffect )
return DRAGDROP_S_CANCEL;
// TODO: Extract files from the CAB here...
return DRAGDROP_S_DROP;
}
else
return S_OK;
}
```
鼠標左鍵釋放的時候,選中文件從CAB文件中釋放出來。
```
STDMETHODIMP CDragDropSource::QueryContinueDrag (
BOOL fEscapePressed, DWORD grfKeyState )
{
// If ESC was pressed, cancel the drag.
// If the left button was released, do the drop.
if ( fEscapePressed )
return DRAGDROP_S_CANCEL;
else if ( !(grfKeyState & MK_LBUTTON) )
{
// If the last DROPEFFECT we got in GiveFeedback()
// was DROPEFFECT_NONE, we abort because the allowable
// effects of the source and target don't match up.
if ( DROPEFFECT_NONE == m_dwLastEffect )
return DRAGDROP_S_CANCEL;
// If the drop was accepted, do the extracting here,
// so that when we return, the files are in the temp dir
// and ready for Explorer to copy.
if ( ExtractFilesFromCab() )
return DRAGDROP_S_DROP;
else
return E_UNEXPECTED; }
else
return S_OK;
}
```
`CDragDropSource::ExtractFilesFromCab()` 是另外一段比較復雜的代碼,它使用了 CAB SDK 來解壓文件到臨時目錄,覆蓋我們之前創建的0字節文件。`QueryContinueDrag()` 返回 `DRAGDROP_S_DROP`時,它通知OLE完成拖放操作。如果釋放目標是一個Windows資源瀏覽器窗口,Explorer會從臨時目錄復制文件到拖放操作的目標文件夾。
## 查看器里拖放操作的實現
我們已經說明了實現拖放邏輯的類,接下來讓我們看一下查看器是如何使用這些類的。當主框架窗口接收到一個 `LVN_BEGINDRAG` 消息,它調用視圖來獲取一個被選中文件的列表,然后建立一個 `CDragDropSource` 對象:
```
LRESULT CMainFrame::OnListBeginDrag(NMHDR* phdr)
{
vector<CDraggedFileInfo> vec;
CComObjectStack<CDragDropSource> dropsrc;
DWORD dwEffect = 0;
HRESULT hr;
// Get a list of the files being dragged (minus files
// that we can't extract from the current CAB).
if ( !m_view.GetDraggedFileInfo(vec) )
return 0; // do nothing
// Init the drag/drop data object.
if ( !dropsrc.Init(m_sCurrentCabFilePath, vec) )
return 0; // do nothing
// Start the drag/drop!
hr = dropsrc.DoDragDrop(DROPEFFECT_COPY, &dwEffect);
return 0;
}
```
第一個調用的方法是視圖的 `GetDraggedFileInfo()` 方法,用來獲取被選擇文件的列表。該方法返回一個 `vector<CDraggedFileInfo>`結構,我們使用這個結構來初始化 `CDragDropSource` 對象。如果被選中的文件都不能解壓縮(比如文件都部分的存儲于該CAB中),`GetDraggedFileInfo()` 可能會失敗。如果`GetDraggedFileInfo()` 失敗, `OnListBeginDrag()` 也會失敗并切不做任何操作直接返回。最后我們調用 `DoDragDrop()` 進行拖放操作,由 `CDragDropSource` 完成剩下的事情。
上面所提到的列表的第六步--即更新UI,在拖放操作之后完成。處于CAB壓縮包末尾的文件可能只是部分的存儲于該CAB中,剩下的部分在后面的CAB文件中。(這對于 Windows 9x 系列安裝文件來說很普通,因為需要限制單個 CAB 文件的大小使得能夠放入軟盤中)。我們試圖解壓這樣一個文件的時候,CAB SDK會告訴我們包含該文件剩余部分的CAB文件么名。它會在相同目錄下尋找包含該文件的起始CAB文件,并且解壓接下來的CAB文件(如果存在)。
當我們想要指出視圖窗口中的部分存儲文件的時候,,`OnListBeginDrag()` 檢查拖放結果看是否有部分存儲文件:
```
LRESULT CMainFrame::OnListBeginDrag(NMHDR* phdr)
{
//...
// Start the drag/drop!
hr = dropsrc.DoDragDrop(DROPEFFECT_COPY, &dwEffect);
if ( FAILED(hr) )
ATLTRACE("DoDragDrop() failed, error: 0x%08X\n", hr);
else
{
// If we found any files continued into other CABs, update the UI.
const vector<CDraggedFileInfo>& vecResults = dropsrc.GetDragResults();
vector<CDraggedFileInfo>::const_iterator it;
for ( it = vecResults.begin(); it != vecResults.end(); it++ )
{
if ( it->bPartialFile )
m_view.UpdateContinuedFile ( *it );
}
}
return 0;
}
```
我們調用 `GetDragResults()` 來獲取更新過得 `vector<CDraggedFileInfo>` 結構,它反映了拖放操作的輸出結果。如果成員變量 `bPartialFile` 被設置為 `true`,那說明該文件部分存儲于 CAB 文件中。我們使用 `UpdateContinuedFile()` 來處理剩下的工作,把相應的 CDraggedFileInfo 結構體傳給它,使得它能夠更新該文件相應的視圖列表項目。下圖說明了當程序指出一個文件部分的存儲于該 CAB 中,并且顯示出下一步分所在文件的情形:

如果后續 CAB 文件無法找到,程序會通過設置該項樣式為 `LVIS_CUT` 表明該文件無法解壓,同時圖標變為灰色。

出于安全的考慮,程序將解壓出的文件留在臨時目錄中,而不是拖放操作完成后立即清除它們。當 `CDragDropSource::Init()` 創建0字節文件的時候,它也把每個文件名添加到一個全局 vector `g_vecsTempFiles`中。當主框架窗口關閉的時候臨時文件才會被清除。
## 添加一個最近使用文件列表
下面我們要探討的文檔/視圖樣式特性就是一個最近使用文件列表(MRU)。WTL的MRU實現是一個模板類: `CRecentDocumentListBase`. 如果你不需要重載默認MRU的任何行為(默認行為通常很重要),你可以使用派生類 `CRecentDocumentList`.
`CRecentDocumentListBase` 模板類有如下參數:
```
template <class T, int t_cchItemLen = MAX_PATH,
int t_nFirstID = ID_FILE_MRU_FIRST,
int t_nLastID = ID_FILE_MRU_LAST> CRecentDocumentListBase
```
`T`
用來特化 `CRecentDocumentListBase` 的派生類名。
`t_cchItemLen`
要存在MRU列表中的項的長度,以 `TCHAR`計。該項至少為6。
`t_nFirstID`
MRU項所使用的ID中的最小ID。
`t_nLastID`
MRU項所使用的ID中的最大ID。 該項必須大于 `t_nFirstID`。
要為我們的程序加入MRU特性,只需要幾步。
1. 插入一個ID為 `ID_FILE_MRU_FIRST` 的菜單項。菜單項文字設置為若MRU列表是空時你希望顯示的消息。
2. 添加一個ID為 `ATL_IDS_MRU_FILE`的字符串表(string table)。這個字符串表用來顯示MRU項選中時的浮動提示。如果你使用 WTL AppWizard 來生成工程,該字符串默認已經創建。
3. 向 `CMainFrame` 添加一個 `CRecentDocumentList` 對象。
4. 在 `CMainFrame::Create()` 里初始化這個對象。
5. 處理ID在`ID_FILE_MRU_FIRST` 和 `ID_FILE_MRU_LAST` 之間的 `WM_COMMAND` 消息。
6. 打開一個CAB文件時更新MRU列表。
7. 應用程序關閉時保存MRU列表。
另外,如果 `ID_FILE_MRU_FIRST` and `ID_FILE_MRU_LAST` 對于你的程序來說不合適,你可以通過一個新的特化的 `CRecentDocumentListBase`類來替換它們。
### 設置MRU對象
第一步是添加一個菜單項指明MRU列表的位置。通常將MRU文件列表放置于 _File_ 菜單下,我們的程序里也是這么做的。菜單項的位置如下圖所示:

WTL AppWizard already 添加了ID為 `ATL_IDS_MRU_FILE` 字符串到字符串表里,我們將它的內容修改為 "Open this CAB file"。接下來我們添加一個 `CRecentDocumentList` 成員變量到 `CMainFrame`中,變量名是 `m_mru`,然后在 `OnCreate()`將其初始化:
```
#define APP_SETTINGS_KEY \
_T("software\\Mike's Classy Software\\WTLCabView");
LRESULT CMainFrame::OnCreate ( LPCREATESTRUCT lpcs )
{
HWND hWndToolBar = CreateSimpleToolBarCtrl(...);
CreateSimpleReBar ( ATL_SIMPLE_REBAR_NOBORDER_STYLE );
AddSimpleReBarBand ( hWndToolBar );
CreateSimpleStatusBar();
m_hWndClient = m_view.Create ( m_hWnd, rcDefault );
m_view.Init();
// Init MRU list
CMenuHandle mainMenu = GetMenu();
CMenuHandle fileMenu = mainMenu.GetSubMenu(0);
m_mru.SetMaxEntries(9);
m_mru.SetMenuHandle ( fileMenu );
m_mru.ReadFromRegistry ( APP_SETTINGS_KEY );
// ...
}
```
前兩個被調用的方法用于設置MRU中項的數目(默認值是16),并且將該成員變臉關聯到菜單上。`ReadFromRegistry()` 從注冊表中讀取MRU列表。它接受我們傳遞的鍵,然后在相應位置創建一個新的鍵來保存列表。以我們的程序為例,鍵的值是 `HKCU\Software\Mike's Classy Software\WTLCabView\Recent Document List`。
導入文件列表后, `ReadFromRegistry()` 調用另外一個 `CRecentDocumentList` 方法`UpdateMenu()`,它查找MRU菜單項并且使實際的MRU項替代它的內容。
### 處理MRU命令并更新列表
當用戶選中一個MRU項時,主框架窗口會收到一個 `WM_COMMAND` 消息,消息的command ID等于菜單項的ID。我們可以使用一條宏語句來處理整個消息映射。
```
BEGIN_MSG_MAP(CMainFrame)
COMMAND_RANGE_HANDLER_EX(
ID_FILE_MRU_FIRST, ID_FILE_MRU_LAST, OnMRUMenuItem)
END_MSG_MAP()
```
消息處理函數從MRU對象中獲取選中項的完整路徑,然后調用 `ViewCab()` 方法,這樣應用程序就顯示出該文件的內容。
```
void CMainFrame::OnMRUMenuItem (
UINT uCode, int nID, HWND hwndCtrl )
{
CString sFile;
if ( m_mru.GetFromList ( nID, sFile ) )
ViewCab ( sFile, nID );
}
```
正如前面提到的一樣,我們擴展了 `ViewCab()` 方法使得它能夠獲取MRU對象的信息,并且更新MRU文件列表。ViewCab() 方法原型如下:
```
void ViewCab ( LPCTSTR szCabFilename, int nMRUID = 0 );
```
如果 `nMRUID` 值為 0,那么`ViewCab()` 方法是通過 `OnFileOpen()`調用的。否則,就是用戶選中MRU菜單項調用的,并且 `nMRUID` 的值為 `OnMRUMenuItem()` 所接收到的值。下面是更新后的代碼:
```
void CMainFrame::ViewCab ( LPCTSTR szCabFilename, int nMRUID )
{
if ( EnumCabContents ( szCabFilename ) )
{
m_sCurrentCabFilePath = szCabFilename;
// If this CAB file was already in the MRU list,
// move it to the top of the list. Otherwise,
// add it to the list.
if ( 0 == nMRUID )
m_mru.AddToList ( szCabFilename );
else
m_mru.MoveToTop ( nMRUID );
}
else
{
// We couldn't read the contents of this CAB file,
// so remove it from the MRU list if it was in there.
if ( 0 != nMRUID )
m_mru.RemoveFromList ( nMRUID );
}
}
```
如果 `EnumCabContents()` 沒有失敗,我們就根據選中該文件的不同情況來更新MRU列表。如果是通過 _File-Open_ 選中的,我們調用 `AddToList()` 方法把文件添加到MRU列表中。如果是通過MRU菜單項選中的,我們使用 `MoveToTop()` 方法把它移動到列表的頂端。如果 `EnumCabContents()` 方法失敗,我們要調用 `RemoveFromList()` 方法從列表中移除該文件。這些方法都會在內部調用 `UpdateMenu()` 方法,所以 _File_ 菜單也會自動得到更新。
### 保存MRU列表
應用程序關閉時,我們保存MRU列表到注冊表中。這個很簡單,一行代碼搞定:
```
m_mru.WriteToRegistry ( APP_SETTINGS_KEY );
```
這行代碼在 `CMainFrame` 里與 `WM_DESTROY` 和 `WM_ENDSESSION` 對應的消息處理函數中調用。
## 其他的UI相關的東西
### 半透明的拖放效果
Windows 2000 以及后續版本的windows操作系統有一個內置的 COM 對象: drag/drop helper,用來在拖放操作過程中提供一個很好的半透明效果。拖動源可以通過 `IDragSourceHelper` 接口使用這個對象。下面是些額外的代碼,加粗標記過,把它添加到 `OnListBeginDrag()` 方法來使用helper 對象:
```
LRESULT CMainFrame::OnListBeginDrag(NMHDR* phdr)
{
NMLISTVIEW* pnmlv = (NMLISTVIEW*) phdr;
CComPtr<IDragSourceHelper> pdsh;
vector<CDraggedFileInfo> vec;
CComObjectStack<CDragDropSource> dropsrc;
DWORD dwEffect = 0;
HRESULT hr;
if ( !m_view.GetDraggedFileInfo(vec) )
return 0; // do nothing
if ( !dropsrc.Init(m_sCurrentCabFilePath, vec) )
return 0; // do nothing
// Create and init a drag source helper object
// that will do the fancy drag image when the user drags
// into Explorer (or another target that supports the
// drag/drop helper interface).
hr = pdsh.CoCreateInstance ( CLSID_DragDropHelper );
if ( SUCCEEDED(hr) )
{
CComQIPtr<IDataObject> pdo;
if ( pdo = dropsrc.GetUnknown() )
pdsh->InitializeFromWindow ( m_view, &pnmlv->ptAction, pdo );
}
// Start the drag/drop!
hr = dropsrc.DoDragDrop(DROPEFFECT_COPY, &dwEffect);
// ...
}
```
我們從創建drag/drop helper COM對象開始。如果成功了,我們調用 `InitializeFromWindow()` 方法并且傳遞三個參數:拖動源窗口的 `HWND` 句柄,光標的位置,以及一個 `CDragDropSource` 對象上的 `IDataObject` 接口。drag/drop helper 使用這個接口來存儲它自己的數據,并且如果釋放目標也使用了helper 對象,這些數據用來生成拖動圖像。
為了使 `InitializeFromWindow()` 工作起來,拖動源窗口需要處理`DI_GETDRAGIMAGE` 消息,并且創建一個做為拖動圖片的位圖回應消息。幸運的是,列表視圖控件支持這個特性,所以不需要太多工作就可以得到拖動圖片。效果圖如下圖所示:

如果我們使用其他類型的窗口做為視圖類,這種況口恰好不能處理 `DI_GETDRAGIMAGE` 消息,我們可以自己創建拖動圖并調用 `InitializeFromBitmap()` 方法來存儲到drag/drop helper對象中。
### 半透明的矩形選擇框
從Windows XP開始,列表視圖空間可以顯示一個半透明的矩形選擇覆蓋框。這個特性是默認關閉的,可以通過在控件上設置 `LVS_EX_DOUBLEBUFFER` 屬性來開啟它。我們的程序在視圖窗口初始化函數 `CWTLCabViewView::Init()` 里完成了這些工作。結果如下圖說示。

如果半透明覆蓋區域沒有出現,檢查你的系統是否開啟了這個特性:

### 按列排序
Windows XP 以及之后的windows操作體統中,一個report 模式的列表視圖控件可以擁有一個選中的列,用一種不同的背景色顯示。這個特性通常用來指出列表按這個列進行了排序,我們的CAB查看器也是這么做的。頭部空間也有兩種樣式,在列的頂端顯示一個向上或者向下的箭頭。這個通常用來顯示排序的方向(從小到大或者從大到小)。
視圖窗口通過響應 `LVN_COLUMNCLICK` 消息進行排序操作。下面用黑體高亮顯示的代碼用來按列排序。
```
LRESULT CWTLCabViewView::OnColumnClick ( NMHDR* phdr )
{
int nCol = ((NMLISTVIEW*) phdr)->iSubItem;
// If the user clicked the column that is already sorted,
// reverse the sort direction. Otherwise, go back to
// ascending order.
if ( nCol == m_nSortedCol )
m_bSortAscending = !m_bSortAscending;
else
m_bSortAscending = true;
if ( g_bXPOrLater )
{
HDITEM hdi = { HDI_FORMAT };
CHeaderCtrl wndHdr = GetHeader();
// Remove the sort arrow indicator from the
// previously-sorted column.
if ( -1 != m_nSortedCol )
{
wndHdr.GetItem ( m_nSortedCol, &hdi );
hdi.fmt &= ~(HDF_SORTDOWN | HDF_SORTUP);
wndHdr.SetItem ( m_nSortedCol, &hdi );
}
// Add the sort arrow to the new sorted column.
hdi.mask = HDI_FORMAT;
wndHdr.GetItem ( nCol, &hdi );
hdi.fmt |= m_bSortAscending ?HDF_SORTUP : HDF_SORTDOWN;
wndHdr.SetItem ( nCol, &hdi );
}
// Store the column being sorted, and do the sort
m_nSortedCol = nCol;
SortItems ( SortCallback, (LPARAM)(DWORD_PTR) this );
// Indicate the sorted column.
if ( g_bXPOrLater )
SetSelectedColumn ( nCol );
return 0;
}
```
第一部分的高亮代碼移除之前用作排序的列頭部的箭頭。如果之前沒有列做為排序的依據,這一步被跳過。接下來,在用戶單擊過的列的頂端添加箭頭。如果按升序排列則肩頭向上,按降序排列箭頭向下。排序完成之后,我們調用 `SetSelectedColumn()` 方法,它是 `LVM_SETSELECTEDCOLUMN` 消息的一個包裝,用來將我們排序的列設置為選中狀態。
按文件大小排序的情況如下圖所示:

### 使用平鋪視圖模式
在Windows XP以及后續的windows操作系統中,列表視圖空間有一種顯得樣式叫做 _平鋪視圖模式_. 做為視圖窗口初始化的一部分,如果程序運行在XP級后續版本的系統上,會設置視圖列表模式為平鋪視圖模式。 使用了 `SetView()` 方法(它是對 `LVM_SETVIEW` 消息的一個封裝)。然后填充一個 `LVTILEVIEWINFO` 結構來設置空間的一些屬性控制平鋪過程。成員變量 `cLines` 被設置為2,在每個平鋪視圖圖標的旁邊顯示兩行文本。成員變量 `dwFlags` 被設置為 `LVTVIF_AUTOSIZE`,使得控件能夠自動縮放平鋪區域。
```
void CWTLCabViewView::Init()
{
// ...
// On XP, set some additional properties of the list ctrl.
if ( g_bXPOrLater )
{
// Turning on LVS_EX_DOUBLEBUFFER also enables the
// transparent selection marquee.
SetExtendedListViewStyle ( LVS_EX_DOUBLEBUFFER,
LVS_EX_DOUBLEBUFFER );
// Default to tile view.
SetView ( LV_VIEW_TILE );
// Each tile will have 2 additional lines (3 lines total).
LVTILEVIEWINFO lvtvi = { sizeof(LVTILEVIEWINFO),
LVTVIM_COLUMNS };
lvtvi.cLines = 2;
lvtvi.dwFlags = LVTVIF_AUTOSIZE;
SetTileViewInfo ( &lvtvi );
}
}
```
#### 設置平鋪視圖圖像列表
對于平鋪視圖模式來說,我們使用了一個特大的系統圖片列表 (默認顯示設置下有 48x48 個圖標 )。我們使用了 `SHGetImageList()` API來獲取這個圖片列表。`SHGetImageList()` 不同于 `SHGetFileInfo()`,它返回一個圖片列表對象上的COM接口。視圖窗口有兩個成員變量用來管理這個圖片列表:
```
CImageList m_imlTiles; // the image list handle
CComPtr<IImageList> m_TileIml; // COM interface on the image list
```
視圖窗口將這個特大圖片列表保存在 `InitImageLists()`里:
```
HRESULT (WINAPI* pfnGetImageList)(int, REFIID, void);
HMODULE hmod = GetModuleHandle ( _T("shell32") );
(FARPROC&) pfnGetImageList = GetProcAddress(hmod, "SHGetImageList");
hr = pfnGetImageList ( SHIL_EXTRALARGE, IID_IImageList,
(void) &m_TileIml );
if ( SUCCEEDED(hr) )
{
// HIMAGELIST and IImageList* are interchangeable,
// so this cast is OK.
m_imlTiles = (HIMAGELIST)(IImageList*) m_TileIml;
}
```
如果 `SHGetImageList()` 操作成功,我們可以強制轉換 `IImageList*` 接口為 `HIMAGELIST` 類型,然后像其他圖片列表一樣使用它。
#### 使用平鋪視圖圖片列表
因為列表控件沒有為平鋪視圖模式生成一個單獨的圖片列表,我們需要當用戶切換顯示模式時動態改變視圖列表。視圖類有一個 `SetViewMode()` 方法,它用來處理切換視圖列表和查看模式:
```
void CWTLCabViewView::SetViewMode ( int nMode )
{
if ( g_bXPOrLater )
{
if ( LV_VIEW_TILE == nMode )
SetImageList ( m_imlTiles, LVSIL_NORMAL );
else
SetImageList ( m_imlLarge, LVSIL_NORMAL );
SetView ( nMode );
}
else
{
// omitted - no image list changing necessary on
// pre-XP, just modify window styles
}
}
```
如果空間進入視圖模式,我們設置控件的列表為48x48的那一個圖片列表,否則設置為32x32的那個。
#### 設置而外的幾行文字
初始化過程中,我們建立平鋪視圖來顯示額外的兩行文本。第一行文本是項目名稱,這一點和在大圖標/小圖標模式下一樣。額外的兩行顯示的是子項內容,和report模式下的列接近。我們可以為每個項單獨設置子項文本。下列代碼說明了視圖如何使用 `AddFile()`方法設置文本:
```
// Add a new list item.
int nIdx;
nIdx = InsertItem ( GetItemCount(), szFilename, info.iIcon );
SetItemText ( nIdx, 1, info.szTypeName );
SetItemText ( nIdx, 2, szSize );
SetItemText ( nIdx, 3, sDateTime );
SetItemText ( nIdx, 4, sAttrs );
// On XP+, set up the additional tile view text for the item.
if ( g_bXPOrLater )
{
UINT aCols[] = { 1, 2 };
LVTILEINFO lvti = { sizeof(LVTILEINFO), nIdx,
countof(aCols), aCols };
SetTileInfo ( &lvti );
}
```
`aCols` 數組包含了要顯示的子項的數據,在這個例子中子項一是文件類型,子項二是文件大小。查看器如下圖所示:

注意,在你按列排序列表之后這兩行文本的內容會相應改變。當選中的列擁有 `LVM_SETSELECTEDCOLUMN` 樣式的時候,子項的文本總是優先顯示,覆蓋了我們在 `LVTILEINFO` 結構中傳遞的子項文本。
- 中文版序言
- Part I - ATL GUI Classes
- Part II - WTL GUI Base Classes
- Part III - Toolbars and Status Bars
- Part IV - Dialogs and Controls
- Part V - Advanced Dialog UI Classes
- Part VI - Hosting ActiveX Controls
- Part VII - Splitter Windows
- Part VIII - Property Sheets and Wizards
- Part IX - GDI Classes, Common Dialogs, and Utility Classes
- Part X - Implementing a Drag and Drop Source