山寨一下 ATL 的 COM_INTERFACE

 

上一篇我们简单学习了下ATL 的继承链处理。可是,如果要裸写一个含内嵌IE控件的窗口,还是要写一个很长的QueryInterface,以及AddRefRelease,确保引用计数的正确性。于是我们不得不参考ATLCOM_TNTERFACE的处理技巧,来达到一定程度上的易用性。

 

首先,除了IUnknown以外,其余所有涉及到的接口,均按上一篇的形式,弄成相应的IXXXImpl,这部分代码见:

http://xllib.codeplex.com/SourceControl/changeset/view/19617#315460

细节不再赘述。现在关键是IUnknown的处理:

1.         首先,如果对象继承自两个或以上的COM接口,要保证所有的查询IUnknown的地方都会返回同一个IUnknown*

2.         其次,在同一对象中,无论哪个接口调用AddRefRelease,都修改同一个引用计数。

 

基于以上两点,各个IXXXImpl不能自己去实现IUnknown的这三个函数,就是return E_NOTIMPL也不行。对于COM接口来说,IUnknown必须进行有意义的实现。

 

就拿WebBrowser的例子来说,WebBrowser容器至少实现IOleClientSiteIOleInPlaceSiteIOleInPlaceFrame,通常还会实现IDocHostUIHandlerDWebBrowserEvents2,它们的继承关系是:

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)

 

其中出现了5IUnknown,我们要在最底层对象上一次性的实现了,而不是在中间层实现。

如果我们裸写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;            \

        }

 

然后前后用 BEGINEND宏定义,拼凑成完整的QueryInterface。可是这没有解决IUnknown的问题,如果查询IUnknown,“(i *)this”会有歧义。这种定义下,代码上无法知道“第一个接口”是啥,无法写出一个可行的thisIUnknown*的转换。

 

不会了,于是就抄ATLATL中是列了一张表,我简化一下,每一个项的结构定义是:

 

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以及它的BEGINEND宏了:

 

#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

 

现在来做OleContainerhttp://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去调用CanInPlaceActiveCanInPlaceActive发现窗口句柄有了,于是拒绝……所以形成了我在《裸写一个含内嵌IE控件的窗口》中的尴尬局面。按照上面的实现,逻辑上就没问题了。

 

另外,这里拆成两个,是为了让后面的WebBrowser继承OleContainerImpl,而OleContainer自己又可以独立使用。

 

还有需要注意的是,OleContainer的析构函数里最好主动调用一下父类的资源释放函数,不然,等子类完全析构后再自动调用到父类的析构函数,子类(子类部分)已经完全消亡了,但是QueryInterface之类的本来是定义在子类的,这时候可能会出现纯虚函数调用的错误。下同。

 

再来做WebBrowserhttp://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;

}

 

运行界面:

 

clip_image001

 

和原来一样。例子代码见WebBrowserSample2.rarhttp://pan.baidu.com/s/1c0q4skK

 

posted on 2012-09-03 23:17  溪流  阅读(21)  评论(0编辑  收藏  举报