CWinTraitsOR>
{
//...
};
由于在WTL中没有文档/视图框架,视图类需要做双份的工作,既是UI,也是存放有关CAB信息的地方。
在拖放操作中传递的数据结构为CDraggedFileInfo:
structCDraggedFileInfo
{
//Datasetatthebeginningofadrag/drop:
CStringsFilename;//nameofthefileasstoredintheCAB
CStringsTempFilePath;//pathtothefileweextractfromtheCAB
intnListIdx;//indexofthisiteminthelistctrl
//Datasetwhileextractingfiles:
boolbPartialFile;//trueifthisfileiscontinuedinanothercab
CStringsCabName;//nameoftheCABfile
boolbCabMissing;//trueifthefileispartiallyinthiscaband
//theCABit'scontinuedinisn'tfound,meaning
//thefilecan'tbeextracted
CDraggedFileInfo(constCString&s,intn):
sFilename(s),nListIdx(n),bPartialFile(false),
bCabMissing(false)
{}
};
视图类中还有如下方法:
初始化、管理文件列表,以及在拖放操作开始的时候准备一个 CDraggedFileInfo 列表。
由于本文是讲关于拖放的,所以我不会深入到UI工作的内部去,需要了解所有细节的话可以检视示例工程中的 WTLCabViewView.h。
文件打开处理
要查看一个CAB文件,用户可以使用 File-Open 命令并选择一个CAB文件。
向导为 CMainFrame 生成的代码包含了 File-Open 菜单项的一个处理器:
BEGIN_MSG_MAP(CMainFrame)
COMMAND_ID_HANDLER_EX(ID_FILE_OPEN,OnFileOpen)
END_MSG_MAP()
OnFileOpen() 使用了 CMyFileDialog 类,该类是在第九部分里介绍到的WTL的 CFileDialog 的增强版本,用以显示一个标准的文件打开对话框。
voidCMainFrame:
:
OnFileOpen(
UINTuCode,intnID,HWNDhwndCtrl)
{
CMyFileDialogdlg(true,_T("cab"),0U,
OFN_HIDEREADONLY|OFN_FILEMUSTEXIST,
IDS_OPENFILE_FILTER,*this);
if(IDOK==dlg.DoModal(*this))
ViewCab(dlg.m_szFileName);
}
OnFileOpen() 调用了辅助函数 ViewCab():
voidCMainFrame:
:
ViewCab(LPCTSTRszCabFilename)
{
if(EnumCabContents(szCabFilename))
m_sCurrentCabFilePath=szCabFilename;
}
EnumCabContents() 相当的复杂,它使用CABSDK调用来枚举在 OnFileOpen() 中选中的文件的内容,并填充视图窗口。
不过 ViewCab() 现在还不完善,我们后面会给它加入支持MRU列表的代码。
下面是查看器的样子,其中显示着Windows98的某个CAB文件的内容:
EnumCabContents() 使用了视图类中的两个方法来填充UI:
AddFile() 和 AddPartialFile()。
AddPartialFile() 在当一个文件被部分存储在CAB中时被调用,因为其头部是在前面的CAB里。
在上面的截图里,列表中的第一个文件就是一个部分文件。
其余的文件是通过 AddFile()添加的。
这两个方法都会为要添加的文件分配一个数据结构,从而视图可以知道其显示的每个文件的所有相关细节。
如果 EnumCabContents() 返回真,则代表所有的枚举以及UI设置工作成功完成。
如果我们只是写一个简单的CAB查看器,我们就可以收手了,尽管此应用不那么有趣。
为了使它真正地有用,我们将对它添加拖放支持,以使用户可以从CAB中提取文件。
拖动源
拖放源是一个COM对象,它实现了两个接口:
IDataObject 和 IDropSource。
IDataObject 用来存放在拖放操作中客户端需要传递的任意数据,在我们这种情况下,此数据应该是一个 HDROP 结构,其中列出了要从CAB中提取的文件。
IDropSource 的方法会由OLE调用,用以在拖放操作中向源通知事件。
拖动源接口
实现了我们的拖放源的C++类为 CDragDropSource。
该类以我在简介中提到过的 MSDN文章中的 IDataObject 实现为起始。
在该文中你可以找到所有代码相关的细节,因此我在这儿就不重复了。
然后我们再向类中加入 IDropSource 及其两个方法:
classCDragDropSource:
publicCComObjectRootEx,
publicCComCoClass,
publicIDataObject,
publicIDropSource
{
public:
//Construction
CDragDropSource();
//Maps
BEGIN_COM_MAP(CDragDropSource)
COM_INTERFACE_ENTRY(IDataObject)
COM_INTERFACE_ENTRY(IDropSource)
END_COM_MAP()
//IDataObjectmethodsnotshown...
//IDropSource
STDMETHODIMPQueryContinueDrag(
BOOLfEscapePressed,DWORDgrfKeyState);
STDMETHODIMPGiveFeedback(DWORDdwEffect);
};
用于调用者的辅助方法
CDragDropSource 使用几个辅助方法封装了 IDataObject 的管理以及拖放的通信。
一个拖放操作遵循以下模式:
1.主框架得到用户开始拖放操作的通知。
2.主框架调用视图窗口来创建一个被拖动的文件的列表。
视图在一个 vector 向量中返回此信息。
3.主框架创建一个 CDragDropSource 对象并将上述向量传递给它,以使它得知要从CAB中提取哪些文件。
4.主框架开始拖放操作。
5.如果用户在一个适当的拖放目标上放下,则 CDragDropSource 对象提取文件。
6.主框架更新UI以标示不能被提取的文件。
步骤3到6由辅助方法处理。
初始化在 Init() 方法中完成:
boolInit(LPCTSTRszCabFilePath,vector&vec);
Init() 将数据复制到保护成员中,填入到一个 HDROP 结构,并使用 IDataObject 方法将该结构保存到数据对象中。
Init() 还作了另一个重要的步骤:
它在TEMP目录下为每个拖动的文件创建了一个零字节的文件。
例如,如果用户从CAB文件中拖动 buffy.txt 和willow.txt,Init() 将在TEMP目录下使用这两个名字创建两个文件。
这是为了预防,万一拖放目标要验证从 HDROP 读入的文件名,如果文件不存在,则目标有可能会拒绝放下。
接下来的方法是 DoDragDrop():
HRESULTDoDragDrop(DWORDdwOKEffects,DWORD*pdwEffect);
DoDragDrop() 接受 dwOKEffects 中的一组 DROPEFFECT_* 标志,这些标志表明了源所允许的那些动作。
它会查询必要的接口,然后调用DoDragDrop() API。
如果拖放成功,*pdwEffect 就被设置为用户希望执行的 DROPEFFECT_* 值。
最后一个方法是 GetDragResults():
constvector&GetDragResults();
CDragDropSource 对象维护的 vector 会在拖放操作过程中被更新。
如果某个文件被发现还连着另一个CAB,或者是不能被提取,则 CDraggedFileInfo 结构会被执行必要的更新。
主框架调用 GetDragResults() 来获取此向量,查找错误并相应更新UI。
IDropSource的方法
第一个 IDropSource 方法是 GiveFeedback(),它通知源,用户是想采取哪种操作(移动、复制或者链接)。
如果愿意的话源可以改变光标。
CDragDropSource 对操作保持了跟踪,并告诉OLE要使用缺省的拖放光标。
STDMETHODIMPCDragDropSource:
:
GiveFeedback(DWORDdwEffect)
{
m_dwLastEffect=dwEffect;
returnDRAGDROP_S_USEDEFAULTCURSORS;
}
另一个 IDropSource 方法是 QueryContinueDrag()。
OLE在用户把光标移来移去时调用此方法,并告诉源哪个鼠标键,以及键盘键,被按下了。
下边是大多数 QueryContinueDrag() 的实现所采用的样板代码:
STDMETHODIMPCDragDropSource:
:
QueryContinueDrag(
BOOLfEscapePressed,DWORDgrfKeyState)
{
//IfESCwaspressed,cancelthedrag.
//Iftheleftbuttonwasreleased,dodropprocessing.
if(fEscapePressed)
returnDRAGDROP_S_CANCEL;
elseif(!
(grfKeyState&MK_LBUTTON))
{
//IfthelastDROPEFFECTwegotinGiveFeedback()
//wasDROPEFFECT_NONE,weabortbecausetheallowable
//effectsofthesourceandtargetdon'tmatchup.
if(DROPEFFECT_NONE==m_dwLastEffect)
returnDRAGDROP_S_CANCEL;
//TODO:
ExtractfilesfromtheCABhere...
returnDRAGDROP_S_DROP;
}
else
returnS_OK;
}
当我们发现左键被释放了,就到了我们要从CAB中提取选中的文件的地方了。
STDMETHODIMPCDragDropSource:
:
QueryContinueDrag(
BOOLfEscapePressed,DWORDgrfKeyState)
{
//IfESCwaspressed,cancelthedrag.
//Iftheleftbuttonwasreleased,dothedrop.
if(fEscapePressed)
returnDRAGDROP_S_CANCEL;
elseif(!
(grfKeyState&MK_LBUTTON))
{
//IfthelastDROPEFFECTwegotinGiveFeedback()
//wasDROPEFFECT_NONE,weabortbecausetheallowable
//effectsofthesourceandtargetdon'tmatchup.
if(DROPEFFECT_NONE==m_dwLastEffect)
returnDRAGDROP_S_CANCEL;
//Ifthedropwasaccepted,dotheextractinghere,
//sothatwhenwereturn,thefilesareinthetempdir
//andreadyforExplorertocopy.
if(ExtractFilesFromCab())
returnDRAGDROP_S_DROP;
else
returnE_UNEXPECTED;
}
else
returnS_OK;
}
CDragDropSource:
:
ExtractFilesFromCab() 是另一个复杂点的代码,它使用CABSDK把文件提取到TEMP目录下,覆盖掉我们先前创建的零字节的文件。
当 QueryContinueDrag() 返回 DRAGDROP_S_DROP 时,也即告诉了OLE完成此拖放操作。
如果拖放目标是一个资源浏览器窗口,资源浏览器会把文件从TEMP目录复制到发生拖放的目录。
从查看器中拖放
我们已经看过了实现拖放操作逻辑的类,现在,我们来看一下我们的查看器应用是怎样使用这个类的。
当主框架窗口接收到 LVN_BEGINDRAG 通知消息时,它会调用视图以获取选中文件的列表,而后设置 CDragDropSource 对象:
LRESULTCMainFrame:
:
OnListBeginDrag(NMHDR*phdr)
{
vectorvec;
CComObjectStackdropsrc;
DWORDdwEffect=0;
HRESULThr;
//Getalistofthefilesbeingdragged(minusfiles
//thatwecan'textractfromthecurrentCAB).
if(!
m_view.GetDraggedFileInfo(vec))
return0;//donothing
//Initthedrag/dropdataobject.
if(!
dropsrc.Init(m_sCurrentCabFilePath,vec))
return0;//donothing
//Startthedrag/drop!
hr=dropsrc.DoDragDrop(DROPEFFECT_COPY,&dwEffect);
return0;
}
第一个调用的是视图的 GetDraggedFileInfo() 方法,用以得到选中文件的列表。
此方法返回一个 vector,我们要用它来初始化 CDragDropSource 对象。
GetDraggedFileInfo() 在选定的文件都不能被提取的情况下(例如文件被分块存放在不同的CAB文件中)有可能失败。
如果发生了这种情况,则 OnListBeginDrag() 也静静地失败,不做任何事情就返回。
最后,我们调用 DoDragDrop() 来开始操作,并让 CDragDropSource 处理剩余的事情。
上面列出的步骤6提到了拖放结束后对UI的更新。
因为有可能在CAB末尾的一个文件仅仅是部分存储于此CAB中,而剩余的则在后续的一个CAB里。
(这在Windows9x的安装文件里非常普遍,在那儿CAB需要能符合软盘的大小)当我们试图提取这样的一个文件时,CABSDK会告诉我们含有该文件剩余部分的CAB的名字。
它还会在原始CAB所在的相同目录下寻找那个CAB,如果存在的话则从中提取文件的剩余部分。
因为我们要在视图窗口中标示分块文件,所以 OnListBeginDrag() 会检查拖放的结果,看是否找到了分块文件:
LRESULTCMainFrame:
:
OnListBeginDrag(NMHDR*phdr)
{
//...
//Startthedrag/drop!
hr=dropsrc.DoDragDrop(DROPEFFECT_COPY,&dwEffect);
if(FAILED(hr))
ATLTRACE("DoDragDrop()failed,error:
0x%08X\n",hr);
else
{
//IfwefoundanyfilescontinuedintootherCABs,updatetheUI.
constvector&vecResults=dropsrc.GetDragResults();
vector:
:
const_iteratorit;
for(it=vecResults.begin();it!
=vecResults.end();it++)
{
if(it->bPartialFile)
m_view.UpdateContinuedFile(*it);
}
}
return0;
}
我们调用 GetDragResults() 来获取反映了拖放操作结果的更新过的 vector。
如果结构中的 bPartialFile 成员为 true,则表示该文件仅部分存在于此CAB中。
我们再调用视图方法 UpdateContinuedFile(),并将信息结构传递给它,因而它可以相应地文件列表视图中的项。
下面就是当发现有后续CAB时,应用程