Introduction
Sometimes we want to protect our control being accessed from automation clients.
Background
Some people ask on the microsoft.public.vc.atl NG:
"I have an Active X control that uses dispinterfaces for Source interfaces to
support automation based clients. As a VC++ client if I use Sink
implementation
using either IDispEventImpl
or IDispEventSimpleImpl
, my application is getting
the events and working fine. "
"I am wondering these only ways we can use or is there any way I can use the
control to get the events. The other way I am trying as my class is deriving
from IDispathImpl<_Iabcevents>
and I am calling ATLAdvise
/
ATLUnadvise
on the object. I can see Advise
is success but I am not
receiving any events. Is
this because of dispInterface will not generate VTBL for events and works based
on dispids or I am missing anything. Is this also proves that C++ clients can
not use this until the control is modified to use the custom / Dual interfaces
as outgoing interfaces."
So I had a look at it, and I made a sample to test my point.
How to make your control automation-incompatible ?
The following steps do the trick.
Step 1
You use ATL control wizard to generate control, remember to tick "supporting
connection point" option, because we will fire control events in vc
client. And also remember to choose custom
Interface, let the automation compatible blank.
As
you know, this is what I want to prove.
#include "olectl.h"
import "oaidl.idl";
import "ocidl.idl";
[
object,
uuid(C86E745D-D971-41E1-AA85-E63C8B26EE48),
helpstring("Icontrol2 Interface"),
pointer_default(unique)
]
interface Icontrol2 : IUnknown{
[propget, helpstring("property text")]
HRESULT text([out, retval] BSTR* pVal);
[propput, helpstring("property text")]
HRESULT text([in] BSTR newVal);
};
[
uuid(97338ED1-60EC-40DF-9CE5-24F2F2B317AC),
helpstring("_Icontrol2Events Interface")
]
interface _Icontrol2Events : IUnknown
{
[id(1), helpstring("method onclick")] HRESULT onclick(void);
};
[
uuid(232E5CB8-8D52-4A3A-BF01-726E8C213C75),
version(1.0),
helpstring("testvtable 1.0 Type Library")
]
library testvtableLib
{
importlib("stdole2.tlb");
[
uuid(CDC37F32-27EE-402F-B0A1-57BF7DE54F27),
helpstring("control2 Class")
]
coclass control2
{
[default] interface Icontrol2;
[default, source] interface _Icontrol2Events;
};
};
Note that here, the dispinterface attribute has been changed to a normal interface
which derived from IUnknown
. So client can only access our control using vtable. So several changes should be made to accommodate this.
template<class T>
class CProxy_Icontrol2Events :
public IConnectionPointImpl<T, &__uuidof(_Icontrol2Events)>
{
public:
HRESULT Fire_onclick()
{
HRESULT hr = S_OK;
T * pThis = static_cast<T *>(this);
int cConnections = m_vec.GetSize();
for (int iConnection = 0; iConnection < cConnections; iConnection++)
{
pThis->Lock();
CComPtr<IUnknown> punkConnection = m_vec.GetAt(iConnection);
pThis->Unlock();
static_cast<IDispatch *>(punkConnection.p);
CComQIPtr<_Icontrol2Events> pEvents(punkConnection);
hr = pEvents->onclick();
DISPATCH_METHOD, �ms, &varResult, NULL, NULL);
}
return hr;
}
};
Another thing is in the onDraw
, textout
should look like this, the "text"
declared as CComBSTR
.
TextOut(di.hdcDraw, (rc.left + rc.right) / 2,
(rc.top + rc.bottom) / 2, pszText, text);
From the above idl, you can see that I implemented an event called
"
onclick
". I fired it in
WM_LBUTTONDOWN
. In order to respond the client-click
event, I implement a property called "text", so that when click on our
control in the client side, the control will change it appearance, to draw
the new text assigned by client.
Step
2
Create a WTL dialog client to test the control. it's also a wizard generated
project. Right click on the dlg template to insert the above control, add event
hander in the normal way.
Note: from there, you should make maindlg to be an object. and
it looks like the following.
#import "..\testvtable\debug\testvtable.dll"
no_namespace,raw_interfaces_only
class CMainDlg :
public CComObjectRootEx<CComSingleThreadModel>,
public CAxDialogImpl<CMainDlg>, public CUpdateUI<CMainDlg>,
public CMessageFilter, public CIdleHandler,
public _Icontrol2Events
{
public:
CComPtr<Icontrol2> spUnk;
DWORD dwSink;
enum { IDD = IDD_MAINDLG };
...
BEGIN_COM_MAP(CMainDlg)
COM_INTERFACE_ENTRY(_Icontrol2Events)
END_COM_MAP()
...
LRESULT OnInitDialog(UINT , WPARAM ,
LPARAM , BOOL& )
{
...
UIAddChildWindowContainer(m_hWnd);
HWND h = GetDlgItem(IDC_CONTROL21);
HRESULT hr = AtlAxGetControl(h, reinterpret_cast(&spUnk));
hr = AtlAdvise(spUnk,this,__uuidof(_Icontrol2Events),&dwSink);
return TRUE;
}
...
void CloseDialog(int nVal)
{
AtlUnadvise(spUnk,__uuidof(_Icontrol2Events),dwSink);
DestroyWindow();
::PostQuitMessage(nVal);
}
HRESULT __stdcall onclick ( )
{
spUnk->put_text(L"clicked");
return S_OK;
}
};
Points of Interest
As you know, if you make maindlg to be an object, then you should do the
following, not normally the way you do.
int Run(LPTSTR = NULL, int nCmdShow = SW_SHOWDEFAULT)
{
CMessageLoop theLoop;
_Module.AddMessageLoop(&theLoop);
CComObject<CMainDlg>* dlgMain = NULL;
int nRet = 0;
if(SUCCEEDED(CComObject<CMainDlg>::CreateInstance(&dlgMain)))
{
dlgMain->AddRef();
if(dlgMain->Create(NULL) == NULL)
{
ATLTRACE(_T("Main dialog creation failed!\n"));
return 0;
}
dlgMain->ShowWindow(nCmdShow);
nRet = theLoop.Run();
}
dlgMain->Release();
_Module.RemoveMessageLoop();
return nRet;
}
That's all!
Another way to hook up control events by way of dispatch
After finishing the sample, the OP also asked about using IDispatch
to access
control events. so I added a new control called control3, and I used
IDispatchImpl
to declare my client side have interest about its events.
So
I add an entry for _Icontrol3Events
in that com map. Now the maindlg has been
changed into the following:
GUID LIBID_testvtableLib = {0x232E5CB8,0x8D52,0x4A3A,
{0xBF,0x01,0x72,0x6E,0x8C,0x21,0x3C,0x75}};
class CMainDlg :
public CComObjectRootEx<CComSingleThreadModel>,
public CAxDialogImpl<CMainDlg>, public CUpdateUI<CMainDlg>,
public CMessageFilter, public CIdleHandler,
public _Icontrol2Events,
public IDispatchImpl<_Icontrol3Events,
&__uuidof(_Icontrol3Events), &LIBID_testvtableLib, 1,0>
{
public:
enum { IDD = IDD_MAINDLG };
CComPtr<Icontrol2> spUnk;
DWORD dwSink;
CComPtr<Icontrol3> spUnk3;
DWORD dwSink3;
...
BEGIN_UPDATE_UI_MAP(CMainDlg)
END_UPDATE_UI_MAP()
BEGIN_COM_MAP(CMainDlg)
COM_INTERFACE_ENTRY(_Icontrol2Events)
COM_INTERFACE_ENTRY(_Icontrol3Events)
END_COM_MAP()
...
LRESULT OnInitDialog(UINT , WPARAM ,
LPARAM , BOOL& )
{
...
HWND h = GetDlgItem(IDC_CONTROL21);
HRESULT hr = AtlAxGetControl(h, reinterpret_cast<IUnknown**>(&spUnk));
hr = AtlAdvise(spUnk,GetUnknown(),__uuidof(_Icontrol2Events),&dwSink);
HWND h3 = GetDlgItem(IDC_CONTROL31);
hr = AtlAxGetControl(h3, reinterpret_cast<IUnknown**>(&spUnk3));
hr = AtlAdvise(spUnk3,GetUnknown(),__uuidof(_Icontrol3Events),&dwSink3);
return TRUE;
}
void CloseDialog(int nVal)
{
AtlUnadvise(spUnk,__uuidof(_Icontrol2Events),dwSink);
AtlUnadvise(spUnk3,__uuidof(_Icontrol3Events),dwSink3);
DestroyWindow();
::PostQuitMessage(nVal);
}
HRESULT __stdcall onclick ( )
{
spUnk->put_text(L"clicked");
return S_OK;
}
STDMETHOD(Invoke)(DISPID dispidMember, REFIID ,LCID lcid,
WORD , DISPPARAMS* pdispparams,
VARIANT* pvarResult,EXCEPINFO* , UINT* )
{
spUnk3->put_text(L"dual clicked");
return S_OK;
}
};
History
So from my view-point, VC++ client is a lot more flexible to access components.