深入分析基于VCL派生的ActiveX控件的实现原理及应用(一)

翻译|其它|编辑:郝浩|2006-04-29 16:28:00.000|阅读 2981 次

概述:

# 界面/图表报表/文档/IDE等千款热门软控件火热销售中 >>


前言

这篇文章虽然是以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原理本来的复杂性,这样的实现复杂度还是可以接受的。


标签:

本站文章除注明转载外,均为本站原创或翻译。欢迎任何形式的转载,但请务必注明出处、不得修改原文相关链接,如果存在内容上的异议请邮件反馈至chenjj@evget.com


为你推荐

  • 推荐视频
  • 推荐活动
  • 推荐产品
  • 推荐文章
  • 慧都慧问
扫码咨询


添加微信 立即咨询

电话咨询

客服热线
023-68661681

TOP