|
一、对象拖放
对象拖放是指对某一指定的对象,利用鼠标拖动的方法,在不同应用的窗口之间、同一
应用的不同窗口之间或同一应用的同一窗口内进行移动、复制(粘贴)等操作的技术。
对象拖放是在操作系统的帮助下完成的。被拖动的对象首先指定使用的数据格式,并按
指定的数据格式提供数据;在拖放结束时,接收拖放对象的窗口按指定的数据格式提取
有关数据,并根据提取的数据生成对象。
二、MFC中用于对象拖放的类
MFC为实现对象拖放提供以下类:
1、C01eDataSouroe用于启动一次拖放操作,并向系统提供拖放对象的数据。类中重要
的成员函数有如下三种:
a.设定提供数据的方式和使用的数据格式,提供数据的方式有两种,一种是即时方式,
另一种是延迟方式;延迟方式不需要立即提供数据,当需要提供数据时,系统将请求
数据,应当重载onRendrData()或其他虚函数,以响应对数据的请求。
数据格式可以是CF—TEXT等常用的剪贴板格式,也可以是自己利用
函数RegisterClipboardFormat(…)函数注册的特殊格式。 CacheData() 利用结构STGMEDIUM,为指定格式提供数据
CacheGlobalData() 利用全局句柄HGLOBAL,为指定格式提供数据
DelayRenderData() 使用延迟方式按指定格式数据提供数据
DelayRenderFileData() 使用延迟方式利用CFile为指定格式提供数据
b.响应请求,提供数据
onRenderFileData() 为延迟方式提供CFile型数据。
onRenderGlobalData() 为延迟方式提供HGLOBAL数据。
onRenerData 为延迟方式提供各种所支持的类型的数据。
c.实施拖放操作
DoDragDrop() 开始实施拖放操作
2、C01eDataTarget用于接收拖放对象的目标窗口,一个窗口要想能够接收拖放对象,必
须包含一个C01eDataTarget对象,并注册该对象。类中主要成员函数。
a.注册
Register() 并注册该对象,以便使窗口能够接收拖放对象
b.响应拖放过程中的动作(虚成员函数)
onDragEnter() 当鼠标首次进入窗口时被调用
onDragLeave() 当鼠标移出窗口时被调用
onDragOver() 当鼠标在窗口内,重复调用被调用
onDrop() 当鼠标在窗口内落下被调用
虚函数onDragEnter()和OnDragOver()的返回值具有重要的含义,为以下数值之一;
DROPEFFECT_MOVE:允许对象落在此窗口,落下时要删除原业的对象
DROPEFFECT_COPY:允许对象落在此窗口,落下时不删除原来的对象
DROPFFECT_NONE: 不允许对象落在此窗口
3、COleDataObject用于接收拖放对象,类中主要成员函数有:
a.确定可以使用的数据格式
BeginEnumFormats() 为枚举数据格式作准备
GetNextFormat() 返回下一个数据格式
IsDataAvailable() 检查指定的数据格式是否可用
b.获取数据
GetData() 按指定数据格式,获得数据
GetFileData() 按指定数据格式,获得GFile型数据
GetGloba1Data() 按指定数据格式,获得HGLOBAL型数据
三、利用MFC实现对象拖放
1、拖放操作的启动
拖放操作的启动
拖放操作一般是从单击鼠标左键开始。在消息WM_LBUTTONDOWN的响应函数
onLButtonDown(…)中,首先要判定是否选定了某一对象,如果未选定或选定多个,
则不能进行拖放操作;如果选定了一个对象,则可以进行拖放操作。
要启动一次拖放操作,需要先准备一个COleDataSource对象。注意到类
COleClientIten和类COleServerItem都是从类COleDataSource上派生的,如果选定
的是COleClientItem对象或者是COleClientItem对象,则可以直接使用所选定的对象;
否则,需要生成一个COleDataSourxe对象,值得注意的是:需要象上文中所说的,应
该指定使用的数据格式,并按指定格式提供对象的有关数据。
下面给出准备数据源的例子:
class myDataSource:public COleDataSource
{
public:
CString str;
protected:
virtual BOOL onRenderFileData(LPFORMATETC,CFile*);
//.....
};
BOOL myDataSource::onRenderFileData(LPFORMATETC lpFormatEtc,CFile*pFile)
{
if(lpFormatEtc->cfFormat==CF_TEXT)
{
pFile,Write(str,str.GetLength());
return TRUE;
}
COleDataSource::OnRenderFileDataSource::OnRenderFileData(lpFormatEtc,pFile);
return FALSE;
}
程序中可以按如下方式,指定数据格式。
myDataSource*pItemDragDrop=new myDataSource;
pItemDragDrop->str="0123456789ABCDEF";
pItemDragDrop->Delayrenderfiledata(CF_TEXT,NULL);
准备好COleDataSource对象之后,调用此对象的成员函数DoDragDrop(...)启动对象拖
放操作。
需要注意的是,函数DoDragDrop(...)并不立即返回,而是要等到鼠标按钮弹起之后。
从执行函数DoDragDrop(...)到从该函数返回,鼠标经过的窗口,接收到一系列的消息,
这些窗口可以根据消息作出相应的动作。
启运拖放操作,可以使用以下语句:
int DragEffect=pItemDragDrop->DoDragDrop(rect,ptOffset,FALSE,DROPEFFECT_MOVE);
2、拖放对象的接收缺省情况下,一般的窗口是不能接收拖放对象的。要使窗口可以接收
拖放对象,需要在窗口类定义中加放成员对象COleDataTarget,并在生成窗口时调用函数
COledataTarger::register().
例如:
Class myView : public CScro11View
{
private:
COleDataTarget oleTarget;
protected:
virtual int OnCreate(LPCREATESTRUCT);
//.....
}
int myView::onCreate(LPCREATESTRUCT lpCreateStruct)
{
if(CScrollView::OnCreate(lpCreateStruct)==-1)
return -1;
dropTarget.Register(this);
return 0;
}
为实现拖放对象的接收,还应为消息
WM_DRAGENTER,WM_DRAGMOVE,WM_DRAGLEVER,WM_DRAGDROP提供响应函数。
消息WM_DRAGMOVE的响应函数OnDragMove(...)应根据鼠标在窗口中的位置,返回以下
数值:
DROPEFFECT_MOVE——表明可以把对象复制到现在的窗口、现在的位置
DROPEFFECT_CIPY——表明可以把对象从原来的窗口、原来的位置移到现在的窗口、现
在的位置
DROPEFFECT_NONE——表明不能在该窗口的该位置放下
例如:
DROPEFFECT myView::OnDragEnter(COleData ject*pDataobject,DWORD dwKeyState,CPoint point)
{
return DROPEFFECT_MOVE;
}
DROPEFFECT myView::OnDragOver(ColeDatalbject*pDataobject, DWORD dwKeyState,CPoint point)
{
return DROPEFFECT_MOVE:
}
消息WM_DRAGDROP的响应函数OnDrop(COleDataject*pDataObject,DROPEFFECT
dropEffect,CPoint point)应处理拖动对象放下后的工作。该函数的参数pDataobjec指
向一个COledataObject对象,利用指针,可以获取有关数据。该函数的一般实现是:
a.检查对象的数据格式:利用COleDataObject::IsDataAvailable()
b.按指定的格式获取数据:利用COleDataObject::GetFileData()等函数
c.建立对象(可能与原对象同,也可能不建立对象,仅使用对象中的数据):利用以上步
骤得到的数据。
例如:
CString str;
myDataSource*pMyData;
if(IsDataAvailable(CF_TEXT))
{
CFile file=GetFileData(CF_TEXT);
file.Read(str,16);
CClientDC dc(this);
dc.TextOut(100,50,str,16);
pMyData=new myDataSource;
pMyData->str=str;
}
对于COleClientItem或COleServerItem对象,可以按以下方法重建对象:
COleClient*pItem=GetDocument()->CreateNewItem();
pItem->CreateForm(pDataObject)
3、拖放操作的结束函数DoDragDrop()返回时,拖放过程结束。函DoDragDrop()的返回
值,表明了对象的拖放结果。
DROPEFFECT_MOVE:对象被放到他处,需删除原对象
DROPEFFECT_COPY:对象被复制到他处,不删除原对象 DROPEFFECT_NONE:未能实现拖效,无需删除原对象
例如:
int DragEffect=pItemTracking->DoDranDrop(rect,ptOffset,FALSE,DROPEFFECT_MOVE);
switch(DragEffect)
{
case DROPEFFECT_MOVE:
delete pItemTracking;
GetDocument()->UpdateAllltems(NULL);
GetDocument()->UpdateAllliews(NULL);
break;
case DROPEFFECT_MOVE;
case DROPEFFECT_NONE:
default:
break;
}
CListCtrlEx:一个支持文件拖放和实时监视的列表控件——用未公开API函数实现Shell实时监视
一、需求
无论何时,当你在Explorer窗口中创建、删除或重命名一个文件夹/文件,或者插入拔除移动存储器时,Windows总是能非常快速地更新它所有的视图。有时候我们的程序中也需要这样的功能,以便当用户在Shell中作出创建、删除、重命名或其他动作时,我们的应用程序也能快速地随之更新。
二、原理
Windows内部有两个未公开的函数(注:在最新的MSDN中,已经公开了这两个函数),分别叫做SHChangeNotifyRegister和SHChangeNotifyDeregister,可以实现以上的功能。这两个函数位于Shell32.dll中,是用序号方式导出的。这就是为什么我们用VC自带的Depends工具察看Shell32.dll时,找不到这两个函数的原因。SHChangeNotifyRegister的导出序号是2;而SHChangeNotifyDeregister的导出序号是4。
SHChangeNotifyRegister可以把指定的窗口添加到系统的消息监视链中,这样窗口就能接收到来自文件系统或者Shell的通知了。而对应的另一个函数,SHChangeNotifyDeregister,则用来取消监视钩挂。SHChangeNotifyRegister的原型和相关参数如下:
ULONG SHChangeNotifyRegister
(
HWND hwnd,
int fSources,
LONG fEvents,
UINT wMsg,
Int cEntries,
SHChangeNotifyEntry *pfsne
);
其中:
hwnd
将要接收改变或通知消息的窗口的句柄。
fSource
指示接收消息的事件类型,将是下列值的一个或多个(注:这些标志没有被包括在任何头文件中,使用者须在自己的程序中加以定义或者直接使用其对应的数值)
SHCNRF_InterruptLevel
0x0001。接收来自文件系统的中断级别通知消息。
SHCNRF_ShellLevel
0x0002。接收来自Shell的Shell级别通知消息。
SHCNRF_RecursiveInterrupt
0x1000。接收目录下所有子目录的中断事件。此标志必须和SHCNRF_InterruptLevel 标志合在一起使用。当使用该标志时,必须同时设置对应的SHChangeNotifyEntry结构体中的fRecursive成员为TRUE(此结构体由函数的最后一个参数pfsne指向),这样通知消息在目录树上是递归的。
SHCNRF_NewDelivery
0x8000。接收到的消息使用共享内存。必须先调用SHChangeNotification_Lock,然后才能存取实际的数据,完成后调用SHChangeNotification_Unlock函数释放内存。
fEvents
要捕捉的事件,其所有可能的值请参见MSDN中关于SHChangeNotify函数的注解。
wMsg
产生对应的事件后,发往窗口的消息。
cEntries
pfsne指向的数组的成员的个数。
pfsne
SHChangeNotifyEntry结构体数组的起始指针。此结构体承载通知消息,其成员个数必须设置成1,否则SHChangeNotifyRegister或者SHChangeNotifyDeregister将不能正常工作(但是据我试验,如果cEntries设为大于1的值,依然可以注册成功,不知何故)。
如果函数调用成功,则返回一个整型注册标志号,否则将返回0。同时系统就会将hwnd指定的窗口加入到操作监视链中,当有文件操作发生时,系统会向hwnd标识的窗口发送wMsg指定的消息,我们只要在程序中加入对该消息的处理函数就可以实现对系统操作的监视了。
如果要退出程序监视,就要调用另外一个未公开得函数SHChangeNotifyDeregister来取消程序监视。该函数的原型如下:
BOOL SHChangeNotifyDeregister(ULONG ulID);
其中ulID指定了要注销的监视注册标志号,如果卸载成功,返回TRUE,否则返回FALSE。
三、实例
在前一文中,我们派生了一个支持文件拖放的列表控件CListCtrlEx,但它还有一个小小的缺憾,就是当用户把一个文件拖放进来之后,如果用户在Shell中对该文件进行移动、删除、重命名等操作时,CListCtrlEx将不能保证实时更新。经过上面的讨论,现在就让我们为CListCtrlEx加上实时文件监控功能。
在使用这两个函数之前,必须要先声明它们的原型,同时还要添加一些宏和结构定义。我们在原工程中添加一个ShellDef.h头文件,然后加入如下声明:
#define SHCNRF_InterruptLevel 0x0001 //Interrupt level notifications from the file system
#define SHCNRF_ShellLevel 0x0002 //Shell-level notifications from the shell
#define SHCNRF_RecursiveInterrupt 0x1000 //Interrupt events on the whole subtree
#define SHCNRF_NewDelivery 0x8000 //Messages received use shared memory
typedef struct
{
LPCITEMIDLIST pidl; //Pointer to an item identifier list (PIDL) for which to receive notifications
BOOL fRecursive; //Flag indicating whether to post notifications for children of this PIDL
}SHChangeNotifyEntry;
typedef struct
{
DWORD dwItem1; // dwItem1 contains the previous PIDL or name of the folder.
DWORD dwItem2; // dwItem2 contains the new PIDL or name of the folder.
}SHNotifyInfo;
typedef ULONG
(WINAPI* pfnSHChangeNotifyRegister)
(
HWND hWnd,
int fSource,
LONG fEvents,
UINT wMsg,
int cEntries,
SHChangeNotifyEntry* pfsne
);
typedef BOOL (WINAPI* pfnSHChangeNotifyDeregister)(ULONG ulID);
这些宏和函数的声明,以及参数含义,如前所述。下面我们要在CListCtrlEx体内添加两个函数指针和一个ULONG型的成员变量,以保存函数地址和返回的注册号。
接下来我们为CListCtrlEx类添加一个公有成员函数Initialize,在其中,我们首先进行加载Shell32.dll以及初始化函数指针动作,接着调用注册函数向Shell注册。用户在使用我们提供的CListCtrlEx类时,应当在CListCtrlEx创建完毕后跟着调用Initialize函数以确保注册了消息监视钩子。否则将失去实时监视功能。初始化函数如下(已略去涉及OLE初始化的部分):
BOOL CListCtrlEx::Initialize()
{
…………
//加载Shell32.dll
m_hShell32 = LoadLibrary("Shell32.dll");
if(m_hShell32 == NULL)
{
return FALSE;
}
//取函数地址
m_pfnDeregister = NULL;
m_pfnRegister = NULL;
m_pfnRegister = (pfnSHChangeNotifyRegister)GetProcAddress(m_hShell32,MAKEINTRESOURCE(2));
m_pfnDeregister = (pfnSHChangeNotifyDeregister)GetProcAddress(m_hShell32,MAKEINTRESOURCE(4));
if(m_pfnRegister==NULL || m_pfnDeregister==NULL)
{
return FALSE;
}
SHChangeNotifyEntry shEntry = {0};
shEntry.fRecursive = TRUE;
shEntry.pidl = 0;
m_ulNotifyId = 0;
//注册Shell监视函数
m_ulNotifyId = m_pfnRegister(
GetSafeHwnd(),
SHCNRF_InterruptLevel|SHCNRF_ShellLevel,
SHCNE_ALLEVENTS,
WM_USERDEF_FILECHANGED, //自定义消息
1,
&shEntry
);
if(m_ulNotifyId == 0)
{
MessageBox("Register failed!","ERROR",MB_OK|MB_ICONERROR);
return FALSE;
}
return TRUE;
}
在CListCtrlEx类中再添加如下函数。该函数的作用是从PIDL中解出实际字符路径。
CString CListCtrlEx::GetPathFromPIDL(DWORD pidl)
{
char szPath[MAX_PATH];
CString strTemp = _T("");
if(SHGetPathFromIDList((struct _ITEMIDLIST *)pidl, szPath))
{
strTemp = szPath;
}
return strTemp;
}
现在我们的程序就可以接收到来自Shell或文件系统的通知消息了,只要编写一个处理自定义消息的函数,就可以对系统范围内的更改动作作出我们希望的响应动作。这里lParam参数中存放的是当前发生的事件(譬如SHCNE_CREATE),wParam参数中存放的是SHNotifyInfo结构。下面是一段框架代码:
LRESULT CListCtrlEx::OnFileChanged(WPARAM wParam, LPARAM lParam)
{
CString strOriginal = _T("");
CString strCurrent = _T("");
SHNotifyInfo* pShellInfo = (SHNotifyInfo*)wParam;
strOriginal = GetPathFromPIDL(pShellInfo->dwItem1);
if(strOriginal.IsEmpty())
{
return NULL;
}
switch(lParam)
{
case SHCNE_CREATE:
break;
case SHCNE_DELETE:
break;
case SHCNE_RENAMEITEM:
break;
}
return NULL;
}
四、总结
至此我们完成了对CListCtrlEx的扩充工作,通过这两个未公开的API函数,编写一个文件夹/文件监视器不再是一件难事。当然同样的功能也可以通过编写设备驱动程序来,但这种方法来实现,难度大,周期长,开发上也有不少困难。