上一張我們著重介紹了怎么樣使用OLE和IDataObject來訪問windows粘貼板。本章主要實(shí)現(xiàn)一個IDataObject接口,然后使用我們完成的數(shù)據(jù)對象來存儲文本“Hello World”到粘貼板中。
創(chuàng)建一個COM接口-IDataObject
為了創(chuàng)建一個COM對象,我們需要定義一個實(shí)現(xiàn)所有這些函數(shù)的C++類,并且讓COM的虛函數(shù)表為我們自動包含,我們使用C++類繼承:
class CDataObject : public IDataObject
{
Public:
HRESULT __stdcall QueryInterface (REFIID iid, void ** ppvObject);
ULONG __stdcall AddRef (void);
ULONG __stdcall Release (void);
HRESULT __stdcall GetData (FORMATETC *pFormatEtc, STGMEDIUM *pmedium);
HRESULT __stdcall GetDataHere (FORMATETC *pFormatEtc, STGMEDIUM *pmedium);
HRESULT __stdcall QueryGetData (FORMATETC *pFormatEtc);
HRESULT __stdcall GetCanonicalFormatEtc (FORMATETC *pFormatEct, FORMATETC *pFormatEtcOut);
HRESULT __stdcall SetData (FORMATETC *pFormatEtc, STGMEDIUM *pMedium, BOOL fRelease);
HRESULT __stdcall EnumFormatEtc (DWORD dwDirection, IEnumFORMATETC **ppEnumFormatEtc);
HRESULT __stdcall DAdvise (FORMATETC *pFormatEtc, DWORD advf, IAdviseSink *, DWORD *);
HRESULT __stdcall DUnadvise (DWORD dwConnection);
HRESULT __stdcall EnumDAdvise (IEnumSTATDATA **ppEnumAdvise);
stgmed
CDataObject (FORMATETC *fmtetc, STGMEDIUM *, int count);
~CDataObject ()
private:
LONG m_lRefCount;
int LookupFormatEtc(FORMATETC *pFormatEtc);
};
上面列出了所有IDataObject成員,包括IUnknown接口成員,這是因?yàn)槲覀儸F(xiàn)在需要實(shí)現(xiàn)整個COM對象,因此每個成員必須正確的包含。
由于IUnknown函數(shù)我們在前面已經(jīng)介紹了,我們繼續(xù)介紹IDataObject函數(shù)。有些好的消息,同時(shí)也有些壞的消息;好的消息是,不是所有餓函數(shù)都需要實(shí)現(xiàn),在IDataObject的9個函數(shù)中,我們僅僅需要實(shí)現(xiàn)3個來支持OLE的拖放操作,因此顯著節(jié)省了我們的工作量。
壞的消息是:一般我們已經(jīng)實(shí)現(xiàn)了IDataObject方法,我們需要實(shí)現(xiàn)完全獨(dú)立的COM接口-IEnumFORMATETC接口。然而到這步還有很大的距離,因此讓我們以一個簡單分配新IDataObject的實(shí)例作為一個開始。
構(gòu)造IDataObject
IDataObject的主要任務(wù)是允許一個消費(fèi)者查詢數(shù)據(jù),這些查詢從QueryData或EnumFormatEtc調(diào)用來發(fā)起的,因此,IDataObject需要知道存儲什么樣的數(shù)據(jù)格式,并且在消費(fèi)者需要數(shù)據(jù)的時(shí)候,它能夠提供。
我們因此需要找到一些辦法來以FORMATETC結(jié)構(gòu)的形式用真正的數(shù)據(jù)片來組裝IDataObject且說明數(shù)據(jù)是什么。
IDataObject在C++類構(gòu)造函數(shù)的時(shí)候組裝,為了更彈性,可能需要添加內(nèi)部幫助程序來執(zhí)行這個任務(wù),但對于我們簡單實(shí)現(xiàn)僅在構(gòu)造函數(shù)中使用。
CDataObject::CDataObject (FORMATETC *fmtetc, STGMEDIUM *stgmed, int count)
{
m_lRefCount = 1;
m_nNumFormats = count;
m_pFormatEtc = new FORMATETC[count];
m_pStgMedium = new STGMEDIUM[count];
for(int i = 0; i < count; i++)
{
m_pFormatEtc[i] = fmtetc[i];
m_pStgMedium[i] = stgmed[i];
}
}
構(gòu)造函數(shù)執(zhí)行兩個重要的任務(wù),首先是初始化COM對象引用記數(shù)為1。我看到過許多不正確的COM代碼,他們初始化記數(shù)為0,COM規(guī)約明確地聲明,一個COM對象必須以“1”作為生命周期的開始,如果你記得,一個記數(shù)為0的COM對象應(yīng)該被刪除,因此它應(yīng)該從不應(yīng)該被初始化為這個值。
第二個任務(wù)是在類構(gòu)造函數(shù)中做一個私有的FORMATETC和STGMEDIUM的副本。數(shù)據(jù)對象不是每個STGMEDIUM結(jié)構(gòu)體內(nèi)部的所有者,它純粹是引用并且在請求調(diào)用GetData的時(shí)候復(fù)制數(shù)據(jù)。
創(chuàng)建IDataObject對象
現(xiàn)在我們有一個定義良好的IDataObject構(gòu)造函數(shù),我可以寫一個包裝函數(shù)來隱藏類的細(xì)節(jié):
HRESULT CreateDataObject (FORMATETC *fmtetc, STGMEDIUM *stgmeds, UINT count, IDataObject **ppDataObject)
{
if(ppDataObject == 0)
return E_INVALIDARG;
*ppDataObject = new CDataObject (fmtetc, stgmeds, count);
return (*ppDataObject) ? S_OK: E_OUTOFMEMORY;
}
現(xiàn)在創(chuàng)建一個IDataObject變的非常簡單:
FORMATETC fmtetc = {CF_TEXT, 0, DVASPECT_CONTENT, -1, TYMED_HGLOBAL};
STGMEDIUM stgmed = {TYMED_HGLOBAL, {0}, 0};
stgmed.hGlobal = StringToHandle ("Hello, World!");
IDataObject *pDataObject;
CreateDataObject (&fmtetc, &stgmed, 1, &pDataObject);
許多IDataObject的實(shí)現(xiàn)包含許多接口內(nèi)部執(zhí)行內(nèi)存分配的程序指定編碼;在這個實(shí)現(xiàn)后面的思想是可以提供一個用于各種程序的通用IDataObject。好了,在創(chuàng)建數(shù)據(jù)對象之前有點(diǎn)工作需要做就是創(chuàng)建FORMATETC和STGMEDIUM結(jié)構(gòu),但這很容易被隔離,并且不會污染接口編碼。
IDataObject::QueryGetData
該成員函數(shù)在某程序想檢查IDataObject看是否包含指定類型的數(shù)據(jù)時(shí)候調(diào)用。一個指向FORMATETC結(jié)構(gòu)的指針作為一個參數(shù),且IDataObject::QueryGetData來檢查這個結(jié)構(gòu)且返回一個值來指示請求的數(shù)據(jù)是否可用。
HRESULT __stdcall IDataObject::QueryGetData(FORMATETC *pFormatEtc)
{
return (LookupFormatEtc(pFormat) == -1) ? DV_E_FORMATETC : S_OK;
}
這個例子中的QueryGetData函數(shù)非常簡單,我們放棄私有協(xié)助函數(shù)-LookupFormatEtc的所有工作:
int CDataObject::LookupFormatEtc(FORMATETC *pFormatEtc)
{
for(int i = 0; i < m_nNumFormats; i++)
{
if((m_pFormatEtc[i].tymed & pFormatEtc->tymed) &&
m_pFormatEtc[i].cfFormat == pFormatEtc->cfFormat &&
m_pFormatEtc[i].dwAspect == pFormatEtc->dwAspect)
{
return i;
}
}
return -1;
}
上面的函數(shù)盡量在我們數(shù)據(jù)對象的可用結(jié)構(gòu)中查找一個與指定FORMATETC結(jié)構(gòu)匹配的對象,如果找到一個匹配的,就簡單的返回相應(yīng)m_pFormatEtc數(shù)組的索引,如果找不到,返回-1表示一個錯誤。
注意,在if從句中的位與操作符:
if( m_pFormatEtc[i].tymed & pFormatEtc->tymed )
AND操作符用在這里是因?yàn)?/SPAN>FORMATETC::tymed成員實(shí)際上是一個位標(biāo)志,它能夠包含不止一個值;例如:QueryGetData的調(diào)用者可以完全指定一個FORMATETC::tymed值(TYMED_HGLOBAL|TYMED_ISTREAM)就意味著你支持HGLOBAL或IStream嗎?
IDataObject::GetData
GetData函數(shù)和QueryGetData有許多相似之處,除了如果支持請求的數(shù)據(jù)格式,它必須返回指定的存儲類型。
HRESULT __stdcall CDataObject::GetData (FORMATETC *pFormatEtc, STGMEDIUM *pStgMedium)
{
int idx;
if((idx = LookupFormatEtc(pFormatEtc)) == -1)
return DV_E_FORMATETC;
pMedium->tymed = m_pFormatEtc[idx].tymed;
pMedium->pUnkForRelease = 0;
switch(m_pFormatEtc[idx].tymed)
{
case TYMED_HGLOBAL:
pMedium->hGlobal = DupGlobalMem(m_pStgMedium[idx].hGlobal);
break;
default:
return DV_E_FORMATETC;
}
return S_OK;
}
同樣要調(diào)用內(nèi)部協(xié)助函數(shù)LookupFormatEtc來檢查是否支持請求的數(shù)據(jù)格式,如果支持,相應(yīng)的STGMEDIUM數(shù)據(jù)被復(fù)制到調(diào)用者提供的結(jié)構(gòu)。
注意,現(xiàn)在調(diào)用DupGlobalMem程序,這是一個協(xié)助函數(shù),它返回指定HGLOBAL內(nèi)存的HANDLE的副本,并且必須返回部分,因?yàn)槊總€GetData調(diào)用都要求一個新的數(shù)據(jù)副本。
HGLOBAL DupGlobalMemMem (HGLOBAL hMem)
{
DWORD len = GlobalSize (hMem);
PVOID source = GlobalLock (hMem);
PVOID dest = GlobalAlloc (GMEM_FIXED, len);
memcpy (dest, source, len);
GlobalUnlock (hMem);
return dest;
}
我們需要同樣的程序來支持TYMED_xxx存儲類型,但現(xiàn)在我們設(shè)想實(shí)現(xiàn)的支持格式是IStream。
IDataObject::EnumFormatEtc
這是最后需要自己動手的成員,不幸的是這個成員函數(shù)實(shí)現(xiàn)如此簡單,但也要求我們寫IEnumFORMATETC對象。
HRESULT __stdcall CDataObject::EnumFormatEtc (DWORD dwDirection, IEnumFORMATETC **ppEnumFormatEtc)
{
if(dwDirection == DATADIR_GET)
{
return CreateEnumFormatEtc(m_NumFormats, m_FormatEtc, ppEnumFormatEtc);
}
else
{
return E_NOTIMPL;
}
}
看到上面的代碼,你會提到SHCreateStdEnumFmtEtc這個API調(diào)用,它能夠代表我們創(chuàng)建IEnumFORMATETC接口,不幸的是,這個API僅僅在WIN2K上可用,因此,我們需要提供其他創(chuàng)建IEnumFORMATETC對象。
因此下面的旅程中,我們將提供一個CreateEnumFormatEtc的完整實(shí)現(xiàn),來代替Shell API調(diào)用。
不支持的IDataObject函數(shù)
仍然有一些IDataObject函數(shù)需要實(shí)現(xiàn),而同時(shí)每個函數(shù)必須是一個有效的程序,有個簡單的辦法可以指定給OLE,我們不支持這些拖放操作以外的函數(shù)。
IDataObject::DAdvise、IDataObject::EnumDAdvise和IDataObject::DUnadivise函數(shù)簡單的返回OLE_E_ADVISENOTSUPPORTED。
HRESULT CDataObject::DAdvise (FORMATETC *pFormatEtc, DWORD advf, IAdviseSink *pAdvSink, DWORD *pdwConnection)
{
return OLE_E_ADVISENOTSUPPORTED;
}
HRESULT CDataObject::DUnadvise (DWORD dwConnection)
{
return OLE_E_ADVISENOTSUPPORTED;
}
HRESULT CDataObject::EnumDAdvise (IEnumSTATDATA **ppEnumAdvise)
{
return OLE_E_ADVISENOTSUPPORTED;
}
GetDataHere只需要實(shí)現(xiàn)IStream和IStorage接口來支持?jǐn)?shù)據(jù)對象,在我們的例子中,我們只支持HGLOBAL數(shù)據(jù),因此返回DATA_E_FORMATETC是一個明智的選擇。
HRESULT CDataObject::GetDataHere (FORMATETC *pFormatEtc, STGMEDIUM *pMedium)
{
return DATA_E_FORMATETC;
}
SetData和GetCanonicalFormatEtc也只要簡單的實(shí)現(xiàn),本例中可以返回E_NOTIMPL值,即使我們返回錯誤的值,一個GetCanonicalFormatEtc記名票據(jù),輸出的FORMATETC結(jié)構(gòu)ptd成員應(yīng)該是0。
HRESULT CDataObject::GetCanonicalFormatEtc (FORMATETC *pFormatEct, FORMATETC *pFormatEtcOut)
{
pFormatEtcOut->ptd = NULL;
return E_NOTIMPL;
}
HRESULT CDataObject::SetData (FORMATETC *pFormatEtc, STGMEDIUM *pMedium, BOOL fRelease)
{
return E_NOTIMPL;
}
添加數(shù)據(jù)到粘貼板
好了,這里有一個簡單那的程序用來通過OLE和數(shù)據(jù)對象來添加“Hello World”到Windows的粘貼板。
#include <windows.h>
int main(void)
{
OleInitialize (0);
IDataObject *pDataObject;
FORMATETC fmtetc = {CF_TEXT, 0, DVASPECT_CONTENT, -1, TYMED_HGLOBAL};
STGMEDIUM stgmed = {TYMED_HGLOBAL, {0}, 0};
stgmed.hGlobal = StringToHandle ("Hello, World!”, -1);
if (CreateDataObject(&fmtetc, &stgmed, 1, &pDataObject) == S_OK)
{
OleSetClipboard (pDataObject);
OleFlushClipboard ();
pDataObject->Release();
}
ReleaseStgMedium (&stgmed);
OleUninitialize ();
return 0;
}
不幸的是這個程序不能工作,因?yàn)槲覀冞€沒有實(shí)現(xiàn)IEnumFORMATETC和CreateEnumFormatEtc函數(shù)。