深入分析基于VCL派生的ActiveX控件的实现原理及应用
Aweay
你可转载,拷贝,但必须加入作者署名Aweay,如果用于商业目的,必须经过作者同意。
前言
这篇文章虽然是以VCL为题,但却是基于BCB的,也就是说是在VCL基础上使用ATL实现的ActiveX的原理分析,如果你是Delphi程序员,这篇文章可能不适合你,不过作者如果有时间会再写一篇“Delphi版的深入分析”,本篇文章比较深入的分析了VCL实现ActiveX控件的原理、事件机制、属性页和ActiveX控件编写的相关知识。希望大家已经掌握了VCL编写元件(Component)的知识,COM原理及ATL/模板的相关知识,因为作者不会对文章中探讨的相关知识做详细介绍,所以你可能因为缺乏相应知识而遇到一些困难,不过作者会尽量用简单的语言来阐述一切。另,作者本来水平有限,在写这篇文章的时候仅是参考了帮助文档、分析了VCL源代码再加上一些猜测,如果有任何理解错误敬请大家指教,好我们开始吧!
从向导开始
为了有分析的对象,我们就从最基本的开始,由向导从TButton派生一个TButtonX的ActiveX控件(以下出现TButtonX都是指ActiveX控件),注意在向导中选中“产生关于对话框”的选项,这样生成的TButtonX会有一个关于对话框。现在我们得到了ButtonImpl,ButtonXContrl_TLB这两个主要的单元,所有的奥秘也在这里,我们进入看看。
在ButtonImpl.h文件中,有这样的申明:
class ATL_NO_VTABLE TButtonXImpl:
VCLCONTROL_IMPL(TButtonXImpl, ButtonX, TButton, IButtonX, DIID_IButtonXEvents)
{
…
}
这个TButtonXImpl实现了TButtonX,一般情况下针对ActiveX的工作都可以在这个单元内完成,不过这个类看起来还是很奇怪:首先ATL_NO_VTABLE是个什么东西?这是一个宏,经过豫编译处理后,这个宏最终替换为__declspec(novtable),而这又是一个编译器指示字,用于指示编译器不要产生vtable(准确的说,是不初始化vtable指针,这样连接器可以排除那些需要vtable才能调用的函数,比如虚函数),这又为什么?
我们知道COM是语言无关的,但是vtable的机制不是所有的语言都有,比如VB等,而我们编写ActiveX控件肯定是要拿到这些开发工具上使用的,所以为了使得C++开发的COM元件可以在VB下使用,我们不能使用vtable机制,而且ActiveX还要是自动化对象,这在后面再讨论。
还有一个宏VCLCONTROL_IMPL,这个宏是关键,它隐藏了VCL实现ActiveX控件的全部奥秘,看来必须分析它才能拨开全部迷雾:
VCLCONTROL_IMPL 宏,封装了ActiveX控件继承的基类,展开后:
#define VCLCONTROL_IMPL(cppClass, CoClass, VclClass, intf, EventID) public TVclControlImpl<cppClass, VclClass, &CLSID_##CoClass, &IID_##intf, &EventID, LIBID_OF_##CoClass>,public IDispatchImpl<intf, &IID_##intf, LIBID_OF_##CoClass>, public TEvents_##CoClass<cppClass>
cppClass是C++实现的类的名字,
CoClass是ActiveX的类名,
VclClass是ActiveX控件继承的VCL基类的名字,
intf是ActiveX控件实现的IDispatch接口,
EventID是ActiveX控件事件接口的标示符。
TVclControlImpl它封装了VclClass指定的VCL类,通过它的窗口管理和消息处理机制,使得它可以工作在ActiveX的宿主环境下。TVclControlImpl实现了标准的ActiveX控件需要的接口,因为它间接继承自CComObjectRootEx和CComCoClass,使得它可以工作在C++ Builder的基与ATL的COM应用程序中。
IDispatchImpl,它实现了IDispatch接口,所以从这个类派生的子类的属性和方法可以被自动化(Automation)操作。上面说了ActiveX必须是自动化对象,而自动化对象必须继承自IDispatch接口,这里正好说明这一点。
TEvents_##CoClass(或者TEvents_CoClassName),提供了VCL控件的事件触发机制,IDE会根据创建的VCL控件自动产生这个类,通过这个类,ActiveX控件可以在事件触发的时候调用相应的方法来处理这些事件。
注意上面的##的编译器指示字,它是用来连接2个宏参数的,比如TEvents_##CoClass会被替换为TEvents_ButtonX,这也是一个类,不过是用IDE自动产生的,用于支持事件机制。
而上面展开代码的关键在于TVclControlImpl这个类,我们再看看它:
template <class T, // User class implementing Control
class TVCL, // Underlying VCL type used in One-Step Conversion
const CLSID* pclsid, // Class ID of Control
const IID* piid, // Primary interface of Control
const IID* peventsid, // Event (outgoing) interface of Control
const GUID* plibid> // GUID of TypeLibrary
class ATL_NO_VTABLE TVclControlImpl:
public CComObjectRootEx<CComObjectThreadModel>,
public CComCoClass<T, pclsid>,
public TVclComControl<T, TVCL>,
public IProvideClassInfo2Impl<pclsid, peventsid, plibid>,
public IPersistStorageImpl<T>,
public IPersistStreamInitImpl<T>,
public IQuickActivateImpl<T>,
public IOleControlImpl<T>,
public IOleObjectImpl<T>,
public IOleInPlaceActiveObjectImpl<T>,
public IViewObjectExImpl<T>,
public IOleInPlaceObjectWindowlessImpl<T>,
public IDataObjectImpl<T>,
public ISpecifyPropertyPagesImpl<T>,
public IConnectionPointContainerImpl<T>,
public IPropertyNotifySinkCP<T, CComDynamicUnkArray>,
public ISupportErrorInfo,
public ISimpleFrameSiteImpl<T>
{
…
}
可以看到,这个类实现了所有ActiveX控件必要实现的接口,除此之外,这个类也是VCL和ATL转换的关键,他有很多关键的方法,比如:
HRESULT OnDraw(ATL_DRAWINFO& di)
{
try
{
if (m_VclCtl)
m_VclCtl->PaintTo(di.hdcDraw, di.prcBounds->left, di.prcBounds->top);
}
catch (Exception& e)
{
return (static_cast<T*>(this))->Error(e.Message.c_str());
}
return S_OK;
}
这个方法可以把VCL元件的界面画在ActiveX宿主窗体上。
经过层层拨丝,我们现在终于搞明白了TButtonXImpl的实现框架,但ActiveX运作的原理和如何同VCL交互的还是不清楚,好,我们现在再来看看,TButtonXImpl的实现代码:
void __fastcall ClickEvent(TObject *Sender);
void __fastcall KeyPressEvent(TObject *Sender, char &Key);
public:
TVclControlImpl
void InitializeControl()
{
m_VclCtl->OnClick = ClickEvent;
m_VclCtl->OnKeyPress = KeyPressEvent;
}
BEGIN_COM_MAP(TButtonXImpl)
VCL_CONTROL_COM_INTERFACE_ENTRIES(IButtonX)
END_COM_MAP()
DECLARE_VCL_CONTROL_PERSISTENCE(TButtonXImpl, TButton);
DECLARE_ACTIVEXCONTROL_REGISTRY("ButtonXControl.ButtonX", 1);
protected:
STDMETHOD(_set_Font(IFontDisp** Value));
STDMETHOD(AboutBox());
STDMETHOD(DrawTextBiDiModeFlagsReadingOnly(long* Value));
...
又是很多的宏,不过作者不打算介绍了,他们在BCB生成的代码注释(这里被删除)里解释的很清楚了,大家可以自己看,我就提示一点,很多朋友问:如何改变ActiveX控件的图标?更改这个
DECLARE_ACTIVEXCONTROL_REGISTRY(“ButtonXControl.ButtonX”, 1);
宏的参数就可以了,比如你已经将图标资源(Bitmap),加入工程,并且这个资源ID为2,则你可以这样更改:
DECLARE_ACTIVEXCONTROL_REGISTRY(“ButtonXControl.ButtonX”, 2);
再看,在protected下有很多属性、方法的申明,在cpp文件中,这些申明也得到了实现,但问题在于为什么是保护类型的?这样ActiveX控件岂不是访问不到这些属性、方法?申明了又有什么用?
是否还记得TButtonXImpl继承了IButtonX接口呢?我们现在要到那里去看看,为此我们要分析一下ButtonXContrl_TLB单元,这个单元文件是由IDE维护的,一般情况是不需要理会这个文件的内容,Borland也不建议你更改这个文件,不过今天我们必须要跨入禁区了,于是就有了IButtonX的实现代码:
interface IButtonX : public IDispatch
{
public:
virtual HRESULT STDMETHODCALLTYPE get_Cancel(VARIANT_BOOL* Value/*[out,retval]*/) = 0; // [1]
virtual HRESULT STDMETHODCALLTYPE set_Cancel(VARIANT_BOOL Value/*[in]*/) = 0; // [1]
virtual HRESULT STDMETHODCALLTYPE get_Caption(BSTR* Value/*[out,retval]*/) = 0; // [-518]
...
#if !defined(__TLB_NO_INTERFACE_WRAPPERS)
VARIANT_BOOL __fastcall get_Cancel(void)
{
VARIANT_BOOL Value;
OLECHECK(this->get_Cancel((VARIANT_BOOL*)&Value));
return Value;
}
...
__property VARIANT_BOOL Cancel = {read = get_Cancel, write = set_Cancel};
__property BSTR Caption = {read = get_Caption};
__property VARIANT_BOOL Default = {read = get_Default, write = set_Default};
__property short DragCursor = {read = get_DragCursor, write = set_DragCursor};
...
}
可以看到IButtonX同样继承自IDispatch接口,所以这也是一个自动化的接口,而且终于有了public,所以那些接口方法和属性被公开了,我们不难得出这样一张类的布局图:
另外请大家注意:
VARIANT_BOOL __fastcall get_Default(void)
{
VARIANT_BOOL Value;
OLECHECK(this->get_Default((VARIANT_BOOL*)&Value));
return Value;
}
上面的实现代码,OLECHECK用于检查函数的执行结果,如果有错误,那么还有一个机会去处理错误。
方法和属性都有了,对于一个ActiveX控件还差事件,没有事件支持的ActiveX控件就像一个没有发条的钟是不会动的,下面,我们再来看一下VCL是如何实现ActiveX控件的事件机制的。
事件机制
还是上面的代码:
void __fastcall ClickEvent(TObject *Sender);
void __fastcall KeyPressEvent(TObject *Sender, char &Key);
public:
TVclControlImpl
void InitializeControl()
{
m_VclCtl->OnClick = ClickEvent;
m_VclCtl->OnKeyPress = KeyPressEvent;
}
...
看起来像是事件的处理代码,啊?好像?有没有搞错?没有搞错,确实是好像,而且是表面的
:
m_VclCtl->OnClick = ClickEvent;
m_VclCtl->OnKeyPress = KeyPressEvent;
是标准的VCL消息处理函数机制,m_VclCtl通过模板参数最终对应于相应的VCL原类,这样m_VclCtl的OnClick事件的处理就会转交给ClickEvent函数,而OnKeyPress事件的处理也就交给了KeyPressEvent函数处理,有VCL经验的人,都能猜到ClickEvent和KeyPressEvent函数是如何实现的,例如:
void __fastcall TButtonXImpl::KeyPressEvent(TObject *Sender, char &Key)
{
short TempKey;
TempKey = (short)Key;
Fire_OnKeyPress(&TempKey);
Key = (short)TempKey;
}
又跳转了,忽略Sender参数,然后把Key又传递给了Fire_OnKeyPress函数处理,为了保证VCL KeyPress事件的结构,Key参数先被保存到TempKey,然后传递,最后返回Key参数,注意:TempKey参数可能在ActiveX事件处理中被修改,这也符合Visual Basic的KeyPress事件结构。
不过问题在于Fire_OnKeyPress函数是从哪里来的?要搞清楚这个问题,我们还要看看前面那个复杂的宏定义:
#define VCLCONTROL_IMPL(cppClass, CoClass, VclClass, intf, EventID) public TVclControlImpl<cppClass, VclClass, &CLSID_##CoClass, &IID_##intf, &EventID, LIBID_OF_##CoClass>,public IDispatchImpl<intf, &IID_##intf, LIBID_OF_##CoClass>, public TEvents_##CoClass<cppClass>
其中,TEvents_##CoClass(或者TEvents_CoClassName),提供了VCL控件的事件触发机制,IDE会根据创建的VCL控件自动产生这个类,通过这个类,ActiveX控件可以在事件触发的时候调用相应的方法来处理这些事件。
注意上面的##的编译器指示字,它是用来连接2个宏参数的,比如TEvents_##CoClass会被替换为TEvents_ButtonX,这也是一个类,不过是用IDE自动产生的,用于支持事件机制。
所以所有的事件奥秘都应该隐藏在这个TEvents_ButtonX类里,如果你够大胆的话,你可以猜测那个Fire_OnKeyPress函数就在这个类里?再次跨越禁区,我们得到代码:
template <class T>
class TEvents_ButtonX : public IConnectionPointImpl<T,
&DIID_IButtonXEvents,
CComUnkArray<CONNECTIONPOINT_ARRAY_SIZE> >
{
public:
void Fire_OnClick(void);
void Fire_OnKeyPress(short* Key);
void Fire_OnMouseMove(int Button, int X, int Y);
protected:
IButtonXEventsDisp m_EventIntfObj;
};
看到了确实如此,我们再来看看Fire_OnKeyPress是如何实现的?
template <class T> void
TEvents_ButtonX<T>::Fire_OnKeyPress(short* Key)
{
T * pT = (T*)this;
pT->Lock();
IUnknown ** pp = m_vec.begin();
while (pp < m_vec.end())
{
if (*pp != NULL)
{
m_EventIntfObj.Attach(*pp);
m_EventIntfObj.OnKeyPress(Key);
m_EventIntfObj.Attach(0);
}
pp++;
}
pT->Unlock();
}
剔除不必要的多线程访问互斥代码、对控件数组事件的支持代码,关键在于:
m_EventIntfObj.OnKeyPress(Key);
看来我们又要跳转了,最后来到:
template <class T> void __fastcall
IButtonXEventsDispT<T>::OnKeyPress(short* Key/*[in,out]*/)
{
_TDispID _dispid(/* OnKeyPress */ DISPID(8));
TAutoArgs<1> _args;
_args[1] = Key /*[VT_I2:1]*/;
OleProcedure(_dispid, _args);
}
至此,如果有ATL/COM知识的人,都可以看出来这是一套标准的OLE方法调用机制,如果你还想跟踪下去,你会发现它就是调用IDispatch接口的Invoke方法来负责方法、属性的调用的,不过这里还可以注意一下:
_TDispID _dispid(/* OnKeyPress */ DISPID(8));
这里DISPID(8)是接口方法的标识符,这个值来自于你设计IButtonXEvents接口时定义的ID号,所以Invoke方法会唯一的定位到这个方法来完成事件机制的最后一步:调用客户代码-也就是你在VB中提供的事件的代码。
于是我们又得到了这样一幅消息、事件流向图:
VCL就是这样一步一步实现ActiveX控件的事件机制的,可以看出来,他的实现还是挺麻烦的,不过考虑到COM原理本来的复杂性,这样的实现复杂度还是可以接受的。
简单的测试
由控件向导生成的TButtonX代码,不需要任何改动,直接编译就会产生一个TButtonX ActiveX控件,我们现在测试一下,点Register Active Server菜单,注册这个控件,然后在开启Visual Basic开发环境,加入刚才注册的控件,发现它确实是按照我们的设计工作的,注意:如果你在开始创建这个控件的时候,选择了生成About对话框的选项,那么还有一个About属性用于显示关于对话框。
那么这个关于对话框又是怎么回事?代码为我们展示这点:
void ShowButtonXAbout(void)
{
TButtonXAbout* Form;
Form = new TButtonXAbout(NULL);
try
{
Form->ShowModal();
}
catch(...)
{
Form->Free();
return;
}
Form->Free();
}
在TButtonImpl的AboutBox函数中调用了上面的函数来显示对话框,不过有一点作者也不太清楚,就是:
Form->Free();
本来按照Borland的说法,在BCB中不推荐使用Free来释放内存,而应该使用delete这个关键字,但这里为什么这样使用?不过作者做了测试,使用delete也是可以的,没有发现什么问题,所以作者猜测,这里可能是Borland没有更新那个代码向导以适应BCB的开发(可能本来是为Delphi设计的,而Borland只是简单的做了一下Delphi到BCB的转换)。
最后需要说明的是AboutBox函数的DISPID必须是-552,这样ActiveX会把这个函数作为About来对待,其实DISPID的设置还是有强制性,很多标准属性必须是特定的DISPID,这些DISPID都是负值,有兴趣的朋友可以看看MSDN或者COM原理的书籍。
重用 继承?
上面的TButtonX控件简单的通过继承VCL的TButton的实现了一个按钮的AcitiveX控件,如果是其他的VCL能不能同样这样简单的继承就可以方便的生成ActiveX控件呢?在问答前,我们先做一个试验,把刚才这个TButtonX放在Visual Basic开发环境中,然后使用Spy++这样工具(这里作者使用作者自己开发的MySpy,可以到http://siney.yeah.net下载)看看他的类名:
发现它的类名并不是ActiveX的控件的TButtonX,而是原来VCL的TButton,这能说明什么?这从一个侧面说明了,在BCB下我们开发ActiveX控件其实就是设计相应的VCL控件,而在最后把他再封装成为ActiveX控件,那么到底什么样的VCL控件都可以封装成为ActiveX控件呢?通常来说只要从TWinControl继承的VCL元件都可以封装成为ActiveX。而这样VCL元件具有如下特征:
? 可以获得焦点
? 可以包含其他控件(仅是具有这样能力,不代表一定具备)
? 拥有窗口句柄
还记得这段代码吗:
HRESULT OnDraw(ATL_DRAWINFO& di)
{
try
{
if (m_VclCtl)
m_VclCtl->PaintTo(di.hdcDraw, di.prcBounds->left, di.prcBounds->top);
}
catch (Exception& e)
{
return (static_cast<T*>(this))->Error(e.Message.c_str());
}
return S_OK;
}
前面说这是BCB实现ActiveX机制的关键类TVclControlImpl所具有的代码,用于把控件画在ActiveX宿主窗体上,而这个方法就是来源于TWinControl,所以ActiveX控件的必须继承自这个类(当然如果是自己实现了,就另当别论),这样,很容易想到的是像TLabel这样的VCL控件是无法实现为ActiveX控件的。
那么是不是从TWinControl继承的VCL元件都可以被封装为ActiveX呢?这也不一定,如果你已经把相应的的unit添加进入工程或者已经把它安装了,则这个VCL元件可能不会出现在那个DropDown 列表里,还有就是这个类没有用RegisterNonActiveX函数注册,这个函数专门用来设置那些类不能被封装为ActiveX,这个函数很复杂:
extern PACKAGE void __fastcall RegisterNonActiveX(System::TMetaClass*,
const * ComponentClasses,
const int ComponentClasses_Size,
TActiveXRegType AxRegType);
具体的使用方法可以参考帮助。在CSDN上和Borland新闻组作者也看到过网友询问“为什么我的控件不能出现在AcitveX的生成向导里”,希望下次看过这篇文章的朋友下次可以解决这个问题。
再完善一些
再回到刚才Visual Basic的开发环境,我们来看看到底那些属性和方法被表露了:
而同样的TButton在BCB中却具有非常多的属性,为什么呢?开始作者认为所有的VCL元件都是表露这些基本的属性和方法,但后来作者又做了一个试验,就是简单的封装了TEdit后发现他表露其他的更多属性和方法,参考了一下帮助文档,发现原来有如下规则:
数据感知属性不表露。
任何与自动化不兼容的类型定义不表露。
可以表露在VCL中未发布的属性,但这样做不保证持久性(persist)。
如果VCL的属性或方法不符合上述规则,我们就需要自己实现相应ActiveX代码来表露他们;相反如果符合上述规定,而你又不想表露给最终用户,你可以在Type Library Editor中删除它们,刷新代码后再删除单元文件中相应的生成代码。
这里需要强调的是:在BCB中设计ActiveX控件在很大程度上是先设计对应的VCL后再封装为ActiveX,而不是像VC那样直接开发ActiveX(其实BCB也可以像VC那样开发ActiveX,毕竟都是使用ATL嘛),这样设计不管是从难易程度还是调试都是非常轻松的。
知道了原理,我们现在要做的是在这个半成品的TButtonX中加入新的事件和属性页,使它看起来更像一个专业的ActiveX控件,对于属性和方法,因为编写这些代码非常简单,为了节省篇幅,这里就不实际添加属性和方法了。
从TButton封装得到的AcitiveX缺少了一个重要的事件,OnMouseMove,下面我们就写代码来表露这个事件,根据我们上面讲述的原理,完成这部分很容易,首先就是在Type Library Editor里的事件支持接口添加相应方法,如图:
刷新后IDE自动产生相应代码,在Impl单元文件中,加入VCL的OnMouseMove消息处理的转移代码,如下黑体部分:
class ATL_NO_VTABLE TButtonXImpl:
VCLCONTROL_IMPL(TButtonXImpl, ButtonX, TButton, IButtonX, DIID_IButtonXEvents)
{
void __fastcall ClickEvent(TObject *Sender);
void __fastcall KeyPressEvent(TObject *Sender, char &Key);
void __fastcall MouseMoveEvent(TObject *Sender, TShiftState Shift, int X,
int Y);
public:
void InitializeControl()
{
m_VclCtl->OnClick = ClickEvent;
m_VclCtl->OnKeyPress = KeyPressEvent;
m_VclCtl->OnMouseMove = MouseMoveEvent;
}
这里的m_VclCtl其实就是TButton,他是通过模板参数替换的,所以通过上面的代码TButton的OnMouseMove消息的处理过程转向了MouseMoveEvent,现在我们最后的工作就是编写MouseMoveEvent这个函数处理OnMouseMove消息:
void __fastcall TButtonXImpl::MouseMoveEvent(TObject *Sender,
TShiftState Shift, int X, int Y)
{
int ss=0;
if(Shift.Contains(ssLeft))
ss=1;
else if(Shift.Contains(ssRight))
ss=2;
Fire_OnMouseMove(ss,X,Y);
}
忽略Sender参数后,再次转发消息流,Fire_OnMouseMove这个函数是由IDE自动产生的,我们直接调用就可以了,它的代码如下:
template <class T> void
TEvents_ButtonX<T>::Fire_OnMouseMove(int Button, int X, int Y)
{
T * pT = (T*)this;
pT->Lock();
IUnknown ** pp = m_vec.begin();
while (pp < m_vec.end())
{
if (*pp != NULL)
{
m_EventIntfObj.Attach(*pp);
m_EventIntfObj.OnMouseMove(Button, X, Y);
m_EventIntfObj.Attach(0);
}
pp++;
}
pT->Unlock();
}
可以看到这部分代码与缺省的Fire_OnClick是一致的,这样添加OnMouseMove事件支持的代码就完成了。
接下来是添加一个属性页,由于VCL的封装,使得开发设计属性页变得非常简单,首先生成一个新的Property Page,这样BCB会为我们产生一个Form,这个Form与普通的Win32开发中Form的最大区别在于它继承自TPropertyPage,所以它有一些独有的方法是我们在设计需要注意的,为了简单起见,属性页里只简单地更改、反馈Caption属性,在实际开发中复杂的属性页是类似的。
属性页与ActiveX交互就是通过2个函数来进行的,而这两个函数都是来自于TPropertyPage类,在缺省生成的Property Page Form里已经加入了这个两个函数的缺省代码,我们要做就是完成这2个函数:
UpdatePropertyPage(void),在打开属性页时,系统会调用这个函数,你可以在这个函数里添加代码用以反映ActiveX的属性在属性页里的显示。
UpdateObject(void),在应用属性时,系统会调用这个函数,你可以把属性页中改变的数据应用到实际的ActiveX控件中。
按照BCB的帮助这两个函数实现起来非常简单,比如UpdateObject(void),就只需要如下代码:
void __fastcall TPropertyPage1::UpdateObject(void)
{
OleObject.OlePropertySet<WideString>("EditMask", WideString(InputMast->Text).Copy());
}
就可以把ActiveX控件的属性更改为属性页中设置的数据,但在作者的计算机上(BCB6+sp4),上面的代码怎么都无法通过编译,无奈之下,作者只好使用原始的方法,代码如下:
void __fastcall TpageNormal::UpdatePropertyPage(void)
{
// Update your controls from OleObjects
IDispatch* ctrl=OleObject;
CComPtr<IButtonX> btnctrl;
ctrl->QueryInterface<IButtonX>(&btnctrl);
edtcaption->Text=String(btnctrl->get_Caption());
}
//---------------------------------------------------------------------------
void __fastcall TpageNormal::UpdateObject(void)
{
// Update OleObjects from your controls
IDispatch* ctrl=OleObject;
CComPtr<IButtonX> btnctrl;
ctrl->QueryInterface<IButtonX>(&btnctrl);
btnctrl->set_Caption(WideString(edtcaption->Text));
}
对于没有ATL、COM知识的读者上面的代码可能比较难懂,建议找些相关书籍熟悉一下。
通过上面的代码,ActiveX控件和属性页之间就可以完美的交互了,最后在ButtonImpl.h文件中加入如下宏映:
BEGIN_PROPERTY_MAP(TButtonXImpl)
PROP_PAGE(CLSID_pageNormal)
END_PROPERTY_MAP()
关于这些宏的说明和原理因篇幅关系这里就不讨论了,BCB的帮助和源码注释里都写的很清楚,感兴趣的朋友可以自己研究一下。至于上面的CLSID_pageNormal是生成属性页时,IDE自动为改属性页生成的ClassID。
这样一个比较完善的ActiveX控件就写完了,是不是非常简单,详细代码可以到作者网站下载。
写在最后
由于篇幅关系,本来很多内容可以展开详细讨论,但作者都省略了,本文就当抛砖引玉,感兴趣的读者可以再深入研究。作者想再次强调的是,不管开发ActiveX控件还是Active Form,最好的方法都是封装(或者转换)为ActiveX,而不是一切从头来,比如你有一个工程,想以ActiveForm的形式用在Web上,那么最好的方法是拿出单独的Form然后转换为ActiveForm,这不需要多复杂的代码,这也是Borland新闻组上专家给的建议。
由于编写ActiveX控件的调试复杂性,所以保证代码质量非常重要,除此之外就是善于利用一些工具来帮助调试,比如Visual Basic、ActiveX Control Test Container等工具,如果有机会作者会写一些实际开发中可能遇到的调试问题和经验。
参考文献:
Borland C++ Builder5 Help Document
Borland VCL Source and Comment