有了这一个想法后,又做了一个简单的Demo:
(1)在原来的Atltest项目里面添加新类TestSecond,并且为之添加连接点.
(2)为类TestSecond添加接口RaiseEvent,AtlTest.idl文件中,代码如下:
[id(1), helpstring("方法RaiseEvent")] HRESULT RaiseEvent([in] LONG a);
在TestSecond.cpp文件中添加如下代码:
STDMETHODIMP CTestSecond::RaiseEvent(LONG a)
{
// TODO: 在此添加实现代码
if(3==a)
{
Program_List *pList=NULL;
pList=(Program_List*)CoTaskMemAlloc(sizeof(Program_List));
pList->aBstr=SysAllocString(TEXT("abcdef"));
pList->check_offset2=34;
pList->check_size=23;
pList->file_size=12;
pList->file_type=1;
pList->pid=1;
this->Fire_OnProgramList(pList);
}
return S_OK;
}
此方法用于引发事件,当输入参数a==3时引发OnProgramList事件,然后把Program_List的指针做为事件的参数传出去.
(3)为_ITestSecondEvents(自动生成的)添加接口,AtlTest.idl文件中,代码如下:
dispinterface _ITestSecondEvents
{
properties:
methods:
[id(1), helpstring("方法OnProgramList")] HRESULT OnProgramList([in]struct Program_List* pList);
};
(4)为类TestSecond添加连接点,然后自动生成一个引发事情时,用于回调方法的代理类:CProxy_ITestSecondEvents
上面这一个项目完成后,我还先写一个C++的测试程序,用于测试上面的程序的成功性:
Project:cpptest1
Files:
--------新添类:TestSecondEvent---------
TestSecondEvent.h中:
#pragma once
#include "stdafx.h"
extern _ATL_FUNC_INFO FuncOnProgramList;
class TestSecondEvent:public IDispEventSimpleImpl<1,TestSecondEvent,&__uuidof(_ITestSecondEvents)>
{
public:
BEGIN_SINK_MAP(TestSecondEvent)
SINK_ENTRY_INFO(1,__uuidof(_ITestSecondEvents),0x1,OnProgramList,&FuncOnProgramList)
END_SINK_MAP()
STDMETHOD(OnProgramList)(Program_List* pList);
public:
TestSecondEvent(void);
public:
virtual ~TestSecondEvent(void);
};
TestSecondEvent.cpp中,
#include "StdAfx.h"
#include "TestSecondEvent.h"
_ATL_FUNC_INFO FuncOnProgramList={CC_STDCALL, VT_EMPTY,1,VT_I4};
TestSecondEvent::TestSecondEvent(void)
{
}
TestSecondEvent::~TestSecondEvent(void)
{
}
HRESULT TestSecondEvent::OnProgramList(Program_List *pList)//事件响应时回调的方法
{
_bstr_t t(pList->aBstr,false);//第一次运行的错误的地方
std::cout<<t<<std::endl;
CoTaskMemFree(pList);
return 0;
}//TestSecondEvent.cpp文件结束
-------cpptest1.cpp的main函数中----------------
CoInitialize(NULL);
{
CComPtr<ITestSecond> pTestSecond;
HRESULT hr=pTestSecond.CoCreateInstance(__uuidof(TestSecond),NULL,CLSCTX_SERVER);
if(FAILED(hr))
{
cout<<"create com instance error"<<endl;
getchar();
return 0;
}
TestSecondEvent secondEvent;
secondEvent.DispEventAdvise(pTestSecond);//建立与事件源的连接
pTestSecond->RaiseEvent(3);
}
CoUninitialize();
getchar();
return 0;
TestSecondEvent是用于连接源的类,更具体的麻烦兄弟自己看书,会讲得更具体一些.
最后运行测试的程序,会发现错误发生在_bstr_t t(pList->aBstr,false);然后调试运行可以看到在函数HRESULT TestSecondEvent::OnProgramList(Program_List *pList)中,当
pList=(Program_List*)CoTaskMemAlloc(sizeof(Program_List));时,pList的指针是0xbaadf00d,而当响应事件的回调函数HRESULT TestSecondEvent::OnProgramList(Program_List *pList)是,传入的参数pList的值却变成了0xccccffff.而这一个值的Program_List的指针里面没有任何数据,所以在生成_bstr_t时显然出错.(上面函数的详细代码可参考上面)查看原来是在自动生成的代理类_ITestSecondEvents_CP中.原来自动生成的代码如下:
#pragma once
template<class T>
class CProxy_ITestSecondEvents :
public IConnectionPointImpl<T, &__uuidof(_ITestSecondEvents)>
{
public:
HRESULT Fire_OnProgramList( Program_List * pList)
{
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();
IDispatch * pConnection = static_cast<IDispatch *>(punkConnection.p);
if (pConnection)
{
CComVariant avarParams[1];
avarParams[0] = pList;//注意这里,他将被默认以VT_BOOL传递
CComVariant varResult;
DISPPARAMS params = { avarParams, NULL, 1, 0 };
hr = pConnection->Invoke(1, IID_NULL, LOCALE_USER_DEFAULT, DISPATCH_METHOD, ¶ms, &varResult, NULL, NULL);
}
}
return hr;
}
};
看到上面的注释,经过调试运行我发现在行avarParams[0]=pList;这一行的时候,被以VT_BOOL参数传递.经过几次尝试,我把这一句改成:
//avarParams[0] = pList;
avarParams[0].byref = pList;
avarParams[0].vt = VT_I4;
生成后再次运行,得到正确的指针值.运行成功,但却留下一个猜想:自动生成的代码对自己定义结构类型(Program_List*)的支持很不好.这也是下文生成.Net调用的封装DLL作一下铺垫吧.
在上面运行成功后,我又用iblimp生成.Net平台下调用的封装的DLL,发现这一工具还真强大,为AtlTest.dll自动生成了OnProgramList事件.测试的代码如下:
static void Main(string[] args)
{
AtlTestLib.TestSecond testSecondClass = new AtlTestLib.TestSecond();
testSecondClass.OnProgramList += new AtlTestLib._ITestSecondEvents_OnProgramListEventHandler(testSecondClass_OnProgramList);
try
{
testSecondClass.RaiseEvent(3);
}
catch (Exception exc)
{
Console.WriteLine(exc.Message);//没有出错,也没有异常
}
Console.Read();
}
static void testSecondClass_OnProgramList(ref AtlTestLib.Program_List pList)
{//在这里设断点,发现跟本没有运行到这里
Console.WriteLine(pList.aBstr);
}
运行结果发现没有错误,也没有异常,就是没有响应事件.这里正好呼应了我前面的猜想自动生成的代码对自己定义结构类型(Program_List*)的支持很不好,所以tblimp自动生成的DLL无法建立与事件源的连接.
用Reflector.exe查看AtlTestLib的源码,很容易发现这一段代码:
[ComVisible(false), TypeLibType((short) 0x10)]
public delegate void _ITestSecondEvents_OnProgramListEventHandler([In] ref Program_List pList);
其实这一段代码也不算错,但一写AtlTest项目中的代理类CProxy_ITestSecondEvents比较就会发现问题,
//avarParams[0] = pList;
avarParams[0].byref = pList;
avarParams[0].vt = VT_I4;
这三行代码是关健的地方,我把接口的第一个参数以VT_I4方式传递,即最后应该是int32类型的.在C++的测试项目中用的是Program_List*指针做为参数,所以VT_I4还是可以直接转成Program_List*指针的.但在C#测试项目中,用ref Program_List结构做为连接点的参数,其不能直接把VT_I4直接转成Program_List结构,所以最后事件没有响应.解决方法有两个,一个是在CProxy_ITestSecondEvents的给avarParams[0]设置正解的参数值,让其能转化成Program_List.但可惜我试了很多种VT_***,但最后都没成功,也许是我自己写错了.最后选择第二种方法,用VT_I4,而写自己的封装项目,用int32作为连接参数,然后再转换成Program_List的结构.
以下是我自已写的一个用于封装AtlTest项目,与tlbimp工具自动生成的代码有好多个地方有区别,大家可以区别一下.其实原本也想直接复制自动成生AtlTestLib的装封方法的,不过后来出现几个错误不好解决,所以放弃了.大家可以用Refrector.exe来查看其源码,看看其中的封装方法,然后自己实现一下.我也想看一看.闲话少说,下面是源代码:
最后运行成功.不过到这里,已基本实现了我最初的设想.用C++实现COM,然后通过事件通知UI层更新界面,但我心理还是很不爽.因为为了实现这么一个小小的功能,得自己写这么多的代码.如果的在项目当中使用的话,那不得写多少无用功的代码呀?为什么弃tblimp.exe不用呢?所以最后想到别一个解决方法,也许不会很让大家满意,但我是觉得可以实现心中想要的结果便可.下面是简单的说明这一个实现:
STDMETHODIMP CTestSecond::RaiseEvent(LONG a)
{
// TODO: 在此添加实现代码
if(3==a)
{
Program_List *pList=NULL;
pList=(Program_List*)CoTaskMemAlloc(sizeof(Program_List));
pList->aBstr=SysAllocString(TEXT("abcdef"));
pList->check_offset2=34;
pList->check_size=23;
pList->file_size=12;
pList->file_type=1;
pList->pid=1;
LONG lptrList=(LONG)pList;//也许只写这么一点大家便明白了,直接把指针转化为int32,激发事件
this->Fire_OnProgramList(lptrList);
}
return S_OK;
}
而对应的事件接口也改成:
dispinterface _ITestSecondEvents
{
properties:
methods:
[id(1), helpstring("方法OnProgramList")] HRESULT OnProgramList([in]LONG lptrList);
};
最后通过tlbimp.exe生成的事件委托也对应为:
[ComVisible(false), TypeLibType((short) 0x10)]
public delegate void _ITestSecondEvents_OnProgramListEventHandler([In] int lptrList);
然后再通过以下代码:
IntPtr pList = (IntPtr)lptrList;
AtlTestCS.Program_List programList =(AtlTestCS.Program_List)Marshal.PtrToStructure(pList, typeof(AtlTestCS.Program_List));
Console.WriteLine(programList.aBstr);
Marshal.FreeCoTaskMem(pList);
分析:最后这一解决方案其实也差很多,因为不管如何如果让别人用的话总觉得很别扭.因为别人还得理解为什么还得把int lptrList转化成IntPtr,再通过Marshal转化得到Program_List.对于面对对象来说,这不免也让别人知道得太多了?所以还可以作一点相应的封装,让用户看到的只是Program_List的参数.而如果直接能在CProxy_ITestSecondEvents中设置好转换的参数的话,就免去很多烦麻,等别人帮我解答了...
总结:这一篇文章主要是讨论如何让传统的C++与.Net平台的交互问题.包括简单的交互.直接写C++组件,而不是通过COM相应的接口.这种情况下全部得自己写封装组件,不能用tlbimp.exe生成,因为它不是标准的接口.而其对面对对象的支持不够好.如以前两篇文章所说,最后都得在C#中转化成static函数.然后还有不知道在自己写的COM组件中实现事件.而用C++写ATL/COM项目的话,前面的几个问题都好解决.虽然不能在其中定义灵活的方法,比如可以有返回值,但其很容易实现类的生成,事件的生成,且还有方便的工具tlbimp.exe.只是有一个问题是对自定义的结构的支持不够好.
最后简单说一说这一篇文章的起因.是因为其中遇到好几个问题都不能在百度,google,codeproject中找到很好的解答.比如如何自己写对COM事件的封装程序,在ATL开发中把结构做为函数的参数等.而原本在网上讨论C++与C#的交互也比较少.所以我想把它分享出来,以让大家可以一起讨论.其实好多地方我也没有找到好的方法,只是基本可以解决,所以希望能得到指点.同时也算是一篇日记吧.
