山寨一下 ATL 的 COM_INTERFACE
上一篇我们简单学习了下ATL 的继承链处理。可是,如果要裸写一个含内嵌IE控件的窗口,还是要写一个很长的QueryInterface,以及AddRef和Release,确保引用计数的正确性。于是我们不得不参考ATL的COM_TNTERFACE的处理技巧,来达到一定程度上的易用性。
首先,除了IUnknown以外,其余所有涉及到的接口,均按上一篇的形式,弄成相应的IXXXImpl,这部分代码见:
http://xllib.codeplex.com/SourceControl/changeset/view/19617#315460
细节不再赘述。现在关键是IUnknown的处理:
1. 首先,如果对象继承自两个或以上的COM接口,要保证所有的查询IUnknown的地方都会返回同一个IUnknown*。
2. 其次,在同一对象中,无论哪个接口调用AddRef和Release,都修改同一个引用计数。
基于以上两点,各个IXXXImpl不能自己去实现IUnknown的这三个函数,就是return E_NOTIMPL也不行。对于COM接口来说,IUnknown必须进行有意义的实现。
就拿WebBrowser的例子来说,WebBrowser容器至少实现IOleClientSite、IOleInPlaceSite、IOleInPlaceFrame,通常还会实现IDocHostUIHandler、DWebBrowserEvents2,它们的继承关系是:
IOleClientSite : IUnknown
IOleInPlaceSite : IOleWindow : IUnknown
IOleInPlaceFrame : IOleInPlaceUIWindow : IOleWindow : IUnknown
IDocHostUIHandler : IUnknown
DWebBrowserEvents2 : IDispatch : IUnknown
如果看虚函数表,将会是这样:
IUnknown | IOleClientSite | ||
IOleClientSite | |||
IUnknown | IOleWindow | IOleInPlaceSite | |
IOleWindow | |||
IOleInPlaceSite |
| ||
IUnknown | IOleWindow | IOleInPlaceUIWindow | IOleInPlaceFrame |
IOleWindow | |||
IOleInPlaceUIWindow |
| ||
IOleInPlaceFrame |
|
| |
IUnknown | IDocHostUIHandler | ||
IDocHostUIHandler | |||
IUnknown | IDispatch (DWebBrowserEvents2) | ||
IDispatch (DWebBrowserEvents2) |
其中出现了5个IUnknown,我们要在最底层对象上一次性的实现了,而不是在中间层实现。
如果我们裸写QueryInterface,就像是上次一样,会是这个样子:
HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, LPVOID *ppvObject) { *ppvObject = nullptr;
if (riid == IID_IUnknown) { *ppvObject = (IOleClientSite *)this; } else if (riid == IID_IOleInPlaceSite) { *ppvObject = (IOleInPlaceSite *)this; } else if (riid == IID_IOleInPlaceUIWindow) { *ppvObject = (IOleInPlaceUIWindow *)this; } else if (riid == IID_IOleInPlaceFrame) { *ppvObject = (IOleInPlaceFrame *)this; } else if (riid == IID_IDocHostUIHandler) { *ppvObject = (IDocHostUIHandler *)this; } else if (riid == IID_IDispatch) { *ppvObject = (IDispatch *)this; } else if (riid == DIID_DWebBrowserEvents2) { *ppvObject = (DWebBrowserEvents2 *)this; }
if (*ppvObject == nullptr) { return E_NOINTERFACE; }
AddRef(); return S_OK; } |
注意高亮的那一句,查询IUnknown接口时,我们指定了IOleClientSite对应的IUnknown,也就是表中最前面的那个IUnknown。
很容易想到的一个做法是:
#define COM_INTERFACE(i) \ \ if (riid == __uuidof(i)) \ { \ *ppvObject = (i *)this; \ AddRef(); \ return S_OK; \ } |
然后前后用 BEGIN、END宏定义,拼凑成完整的QueryInterface。可是这没有解决IUnknown的问题,如果查询IUnknown,“(i *)this”会有歧义。这种定义下,代码上无法知道“第一个接口”是啥,无法写出一个可行的this到IUnknown*的转换。
不会了,于是就抄ATL。ATL中是列了一张表,我简化一下,每一个项的结构定义是:
struct InterfaceEntry { const IID *piid; DWORD_PTR dwOffset; }; |
然后定义如下函数:
typedef c ComClass;
static const InterfaceEntry *GetEntries() { static const InterfaceEntry entries[] = { { &__uuidof(i), (DWORD_PTR)((i *)(ComClass *)8) - 8 }, { &__uuidof(i), (DWORD_PTR)((i *)(ComClass *)8) - 8 }, // ... { nullptr, 0 } };
return entries; } |
其中c是最终那个对象的类,i是每一个要对外暴露的接口。每一项的第二列存了该接口到类的首地址的偏移量。至于那个8,为什么要用8,是不是他们拍脑袋的?用别常数有没有问题呢……谁来告诉我?(在下面的实际实现中我用了sizeof(nullptr),不知道有木有问题。)
为此,我们定义一个额外的辅助类:
template <typename T> class ComClass { public: ComClass() : m_nRefCount(0) { InternalAddRef(); }
~ComClass() {
}
public: STDMETHODIMP InternalQueryInterface(const InterfaceEntry *pEntries, REFIID riid, LPVOID *ppvObject) { *ppvObject = nullptr; T *pThis = (T *)this;
IUnknown *pUnknown = (IUnknown *)((INT_PTR)pThis + pEntries->dwOffset);
if (riid == __uuidof(IUnknown)) { *ppvObject = pUnknown; pUnknown->AddRef(); return S_OK; }
for (const InterfaceEntry *pEntry = pEntries; pEntry->piid != nullptr; ++pEntry) { if (riid == *pEntry->piid) { *ppvObject = (IUnknown *)((INT_PTR)pThis + pEntry->dwOffset); pUnknown->AddRef(); return S_OK; } }
return E_NOINTERFACE; }
ULONG STDMETHODCALLTYPE InternalAddRef() { return (ULONG)InterlockedIncrement(&m_nRefCount); }
ULONG STDMETHODCALLTYPE InternalRelease() { LONG nRefCount = InterlockedDecrement(&m_nRefCount);
if (nRefCount <= 0) { delete this; }
return (ULONG)nRefCount; }
protected: LONG m_nRefCount; }; |
它来对三个IUnknown的接口作实际的实现,其中InternalQueryInterface完成从Entries里面找到偏移量,然后算出接口地址返回给外界的过程。然后……规定:所有COM类必须继承刚才这个ComClass!
现在可以来定义COM_INTERFACE以及它的BEGIN、END宏了:
#define XL_COM_INTERFACE_BEGIN(c) \ \ typedef c ComClass; \ \ static const InterfaceEntry *GetEntries() \ { \ static const InterfaceEntry entries[] = \ { \
#define XL_COM_INTERFACE(i) \ \ { &__uuidof(i), (DWORD_PTR)((i *)(ComClass *)sizeof(nullptr)) - sizeof(nullptr) }, \
#define XL_COM_INTERFACE_END() \ \ { nullptr, 0 } \ }; \ \ return entries; \ } \ \ STDMETHODIMP QueryInterface(REFIID riid, LPVOID *ppvObject) \ { \ return InternalQueryInterface(GetEntries(), riid, ppvObject); \ } \ \ ULONG STDMETHODCALLTYPE AddRef() \ { \ return InternalAddRef(); \ } \ \ ULONG STDMETHODCALLTYPE Release() \ { \ return InternalRelease(); \ } \
|
其实主要就是在拼凑那张表,其余都是直接带上的。
上面的代码位于:
http://xllib.codeplex.com/SourceControl/changeset/view/19617#315452
http://xllib.codeplex.com/SourceControl/changeset/view/19617#315458
现在来做OleContainer(http://xllib.codeplex.com/SourceControl/changeset/view/19617#315455)。
class OleContainerImpl : public IOleClientSiteImpl<>, public IOleInPlaceSiteImpl<>, public IOleInPlaceFrameImpl<> { public: OleContainerImpl() : m_hOleParent(nullptr), m_pStorage(nullptr), m_pOleObj(nullptr), m_pInPlaceObj(nullptr), m_bInPlaceActived(false) { ZeroMemory(&m_rect, sizeof(RECT));
OleInitialize(nullptr); }
virtual ~OleContainerImpl() { DestroyOleObject();
OleUninitialize(); }
public: bool CreateOleObject(const IID &clsid) { DestroyOleObject();
HRESULT hr = StgCreateDocfile(nullptr, STGM_READWRITE | STGM_SHARE_EXCLUSIVE | STGM_DIRECT | STGM_CREATE, 0, &m_pStorage); if (FAILED(hr)) { return false; }
hr = OleCreate(clsid, IID_IOleObject, OLERENDER_DRAW, 0, this, m_pStorage, (LPVOID *)&m_pOleObj);
if (FAILED(hr)) { return false; }
hr = m_pOleObj->QueryInterface(IID_IOleInPlaceObject, (LPVOID *)&m_pInPlaceObj);
if (FAILED(hr)) { return false; }
return true; }
void DestroyOleObject() { if (m_pInPlaceObj != nullptr) { m_pInPlaceObj->Release(); m_pInPlaceObj = nullptr; }
if (m_pOleObj != nullptr) { m_pOleObj->Release(); m_pOleObj = nullptr; }
if (m_pStorage != nullptr) { m_pStorage->Release(); m_pStorage = nullptr; } }
bool InPlaceActive(HWND hWnd, LPCRECT lpRect = nullptr) { if (hWnd == nullptr || m_pOleObj == nullptr) { return false; }
m_hOleParent = hWnd;
if (lpRect != nullptr) { CopyMemory(&m_rect, lpRect, sizeof(RECT)); } else { GetClientRect(m_hOleParent, &m_rect); }
HRESULT hr = m_pOleObj->DoVerb(OLEIVERB_INPLACEACTIVATE, nullptr, this, 0, m_hOleParent, &m_rect);
if (FAILED(hr)) { return false; }
return true; }
public: STDMETHOD(GetWindow)(HWND *phwnd) { *phwnd = m_hOleParent; return S_OK; }
STDMETHOD(CanInPlaceActivate)() { return m_bInPlaceActived ? S_FALSE : S_OK; }
STDMETHOD(GetWindowContext)(IOleInPlaceFrame **ppFrame, IOleInPlaceUIWindow **ppDoc, LPRECT lprcPosRect, LPRECT lprcClipRect, LPOLEINPLACEFRAMEINFO lpFrameInfo) { if (m_hOleParent == nullptr) { return E_NOTIMPL; }
*ppFrame = (IOleInPlaceFrame*)this; (*ppFrame)->AddRef();
*ppDoc = NULL;
CopyMemory(lprcPosRect, &m_rect, sizeof(RECT)); CopyMemory(lprcClipRect, &m_rect, sizeof(RECT));
lpFrameInfo->cb = sizeof(OLEINPLACEFRAMEINFO); lpFrameInfo->fMDIApp = false; lpFrameInfo->hwndFrame = GetParent(m_hOleParent); lpFrameInfo->haccel = nullptr; lpFrameInfo->cAccelEntries = 0;
return S_OK; }
protected: HWND m_hOleParent; IStorage *m_pStorage; IOleObject *m_pOleObj; IOleInPlaceObject *m_pInPlaceObj; bool m_bInPlaceActived; RECT m_rect; };
class OleContainer : public ComClass<OleContainer>, public OleContainerImpl { public: OleContainer() {
}
~OleContainer() { DestroyOleObject(); }
public: XL_COM_INTERFACE_BEGIN(OleContainer) XL_COM_INTERFACE(IOleClientSite) XL_COM_INTERFACE(IOleInPlaceSite) XL_COM_INTERFACE(IOleInPlaceFrame) XL_COM_INTERFACE_END() }; |
对于InPlaceActive,之前理解有误,OleCreate的时候,并不需要给出窗口,到InPlaceActive之前的瞬间给出即可。而CanInPlaceActive,之前是根据窗口句柄是否为空来确定S_OK还是S_FALSE,这有错误。快要InPlaceActive了,于是去把m_hWnd设上,InPlaceActive去调用CanInPlaceActive,CanInPlaceActive发现窗口句柄有了,于是拒绝……所以形成了我在《裸写一个含内嵌IE控件的窗口》中的尴尬局面。按照上面的实现,逻辑上就没问题了。
另外,这里拆成两个,是为了让后面的WebBrowser继承OleContainerImpl,而OleContainer自己又可以独立使用。
还有需要注意的是,OleContainer的析构函数里最好主动调用一下父类的资源释放函数,不然,等子类完全析构后再自动调用到父类的析构函数,子类(子类部分)已经完全消亡了,但是QueryInterface之类的本来是定义在子类的,这时候可能会出现纯虚函数调用的错误。下同。
再来做WebBrowser(http://xllib.codeplex.com/SourceControl/changeset/view/19615#315453):
class WebBrowserImpl : public OleContainerImpl, public IDocHostUIHandlerImpl<>, public DWebBrowserEvents2Impl<> { public: WebBrowserImpl() : m_pWebBrowser(nullptr), m_pCPC(nullptr), m_pCP(nullptr) {
}
~WebBrowserImpl() { DestroyWebBrowser(); }
public: bool CreateWebBrowser(HWND hWnd, LPCRECT lpRect = nullptr) { DestroyWebBrowser();
if (!CreateOleObject(__uuidof(::WebBrowser))) { return false; }
if (!InPlaceActive(hWnd, lpRect)) { return false; }
HRESULT hr = m_pOleObj->QueryInterface(__uuidof(IWebBrowser2), (LPVOID *)&m_pWebBrowser);
if (FAILED(hr)) { return false; }
hr = m_pWebBrowser->QueryInterface(__uuidof(IConnectionPointContainer), (LPVOID *)&m_pCPC);
if (FAILED(hr)) { return false; }
hr = m_pCPC->FindConnectionPoint(__uuidof(DWebBrowserEvents2), &m_pCP);
if (FAILED(hr)) { return false; }
DWORD dwCookie = 0; hr = m_pCP->Advise((DWebBrowserEvents2 *)this, &dwCookie);
if (FAILED(hr)) { return false; }
return true; }
void DestroyWebBrowser() { if (m_pCP != nullptr) { m_pCP->Release(); m_pCP = nullptr; }
if (m_pCPC != nullptr) { m_pCPC->Release(); m_pCPC = nullptr; }
if (m_pWebBrowser != nullptr) { m_pWebBrowser->Release(); m_pWebBrowser = nullptr; }
DestroyOleObject(); }
protected: IWebBrowser2 *m_pWebBrowser; IConnectionPointContainer *m_pCPC; IConnectionPoint *m_pCP; };
class WebBrowser : public ComClass<WebBrowser>, public WebBrowserImpl { public: WebBrowser() {
}
~WebBrowser() { DestroyWebBrowser(); }
public: XL_COM_INTERFACE_BEGIN(WebBrowser) XL_COM_INTERFACE(IOleClientSite) XL_COM_INTERFACE(IOleInPlaceSite) XL_COM_INTERFACE(IOleInPlaceFrame) XL_COM_INTERFACE(IDocHostUIHandler) XL_COM_INTERFACE(DWebBrowserEvents2) XL_COM_INTERFACE_END() }; |
这里很普通,没什么要说的。现在就可以跟原来一样使用了:
Main.cpp
class WebBrowser : public xl::WebBrowser { public: void Navigate(LPCTSTR lpUrl) { BSTR bstrUrl = SysAllocString(lpUrl); m_pWebBrowser->Navigate(bstrUrl, nullptr, nullptr, nullptr, nullptr); SysFreeString(bstrUrl); } };
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) { switch (message) { case WM_DESTROY: PostQuitMessage(0); break; default: return DefWindowProc(hWnd, message, wParam, lParam); }
return 0; }
int APIENTRY _tWinMain(_In_ HINSTANCE hInstance, _In_opt_ HINSTANCE hPrevInstance, _In_ LPTSTR lpCmdLine, _In_ int nCmdShow) { UNREFERENCED_PARAMETER(hPrevInstance); UNREFERENCED_PARAMETER(lpCmdLine);
const LPCTSTR CLASS_NAME = _T("WebBrowserContainer");
WNDCLASSEX wcex = { sizeof(WNDCLASSEX) }; wcex.style = CS_HREDRAW | CS_VREDRAW; wcex.lpfnWndProc = WndProc; wcex.cbClsExtra = 0; wcex.cbWndExtra = 0; wcex.hInstance = hInstance; wcex.hCursor = LoadCursor(nullptr, IDC_ARROW); wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1); wcex.lpszClassName = CLASS_NAME;
RegisterClassEx(&wcex);
HWND hWnd = CreateWindow(CLASS_NAME, _T("WebBrowser Sample"), WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_MINIMIZEBOX, CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, nullptr, nullptr, hInstance, nullptr);
if (hWnd == nullptr) { return 0; }
ShowWindow(hWnd, nCmdShow); UpdateWindow(hWnd);
WebBrowser wb;
if (!wb.CreateWebBrowser(hWnd)) { return 0; }
wb.Navigate(_T("http://www.baidu.com/"));
MSG msg = {};
while (GetMessage(&msg, nullptr, 0, 0)) { if (!TranslateAccelerator(msg.hwnd, nullptr, &msg)) { TranslateMessage(&msg); DispatchMessage(&msg); } }
return (int)msg.wParam; } |
运行界面:
和原来一样。例子代码见WebBrowserSample2.rar(http://pan.baidu.com/s/1c0q4skK)。