现在的位置: 首页 > 综合 > 正文

用标准C编写COM(四)COM in plain C,Part4

2012年07月07日 ⁄ 综合 ⁄ 共 12288字 ⁄ 字号 评论关闭

原文:http://www.codeproject.com/Articles/14117/COM-in-plain-C-Part-4

用C构造多接口的COM对象

下载例程-194kb

内容

  •     在我们的对象中嵌入子对象
  •     应用程序获取基对象的方法
  •     应用程序通过基对象获取子对象的方法
  •     应用程序从一个子对象获取另一个子对象的方法
  •     应用程序获得我们集合对象的方法
  •     委托
  •     我们基对象的QueryInterface、AddRef和Release
  •     我们子对象的QueryInterface、AddRef和Release
  •     另一个添加子对象到我们对象中的方法
  •     一个多接口对象的例程
  •     使用我们对象的C应用程序例程
  •     接下来是什么?

介绍

 

     有时,一个COM对象可能有多个接口。说一个COM对象有多个接口也就是说这个对象由几个“子对象”组成。每个子对象本身是一个完整的COM对象,有自己的lpVtbl成员(指向它自己的VTable),也有自己的VTable函数集(包括它自己的QueryInterface、AddRef和Release函数)。

 

     一个对象可以有许多种类型的子对象。例如,我们有一个用于管理声卡的对象,一个声卡上面可能有几个输入输出插口。我们要给每个输入、输出插口分配一个子对象。假设我们有一个输入线插口、一个麦克输入插口和一个扬声器输出口,因此我们可以有三个子对象:ILineIn、IMicIn和ISpeakerOut。每个单独的对象可以有特定控制它的单独插口的函数。例如,可能麦克输入口有一个可以切换高低阻抗麦克风的装置,所以我们的IMicIn对象可以有一个SetImpedance函数。但ILineIn和ISpeakerOut对象不需要这样的函数,因为这个装置与那些插口无关,可能ILineIn和ISpeakerOut对象需要其他函数来控制那些插口,可能ILineIn有一个名为Mute函数静音信号,我们的ISpeakerOut有一个名为SetVolume的来设置它的声音。在这我们用微软提供的定义COM对象的宏来定义这三个对象。

   #undef  INTERFACE
   #define INTERFACE  IMicIn
   DECLARE_INTERFACE_ (INTERFACE, IUnknown)
    {
      STDMETHOD  (QueryInterface) (THIS_REFIID, void **) PURE;
      STDMETHOD_ (ULONG, AddRef)  (THIS)PURE;
      STDMETHOD_ (ULONG, Release) (THIS) PURE;
      STDMETHOD  (SetImpedance)   (THIS_ long) PURE;
   };
   #undef  INTERFACE
   #define INTERFACE  ILineIn
   DECLARE_INTERFACE_ (INTERFACE, IUnknown)
    {
      STDMETHOD  (QueryInterface) (THIS_REFIID, void **) PURE;
      STDMETHOD_ (ULONG, AddRef)  (THIS)PURE;
      STDMETHOD_ (ULONG, Release) (THIS) PURE;
      STDMETHOD  (Mute)           (THIS_ long) PURE;
   };
   #undef  INTERFACE
    #define INTERFACE  ISpeakerOut
   DECLARE_INTERFACE_ (INTERFACE, IUnknown)
    {
      STDMETHOD  (QueryInterface) (THIS_REFIID, void **) PURE;
      STDMETHOD_ (ULONG, AddRef)  (THIS)PURE;
      STDMETHOD_ (ULONG, Release) (THIS) PURE;
      STDMETHOD  (SetVolume)      (THIS_ long) PURE;
   };

     注意每个子对象的VTable像所有COM对象那样必须以它自己的QueryInterface、AddRef和Release函数开始,在这之后,我们列出一些在对象的VTable中的额外函数。IMicIn有它的SetImpedance函数、ILineIn有它的Mute函数、ISpeakerOut有它的SetVolume函数。(对于这个例子,我已经定义他们接受一个long值的参数。在SetImpedance中这个参数可以是一个阻抗值。对于Mute它可以静音时是1或者非静音是0。对于SetVolume这个参数可以是这个声音的值)

 

     注意:我没有给这些子对象添加IDispatch函数(也没有指定他们的VTable基于IDispatch)。相反,我忽略IDipatch函数,指定VTable基于IUnknowm。因此这些子对象的函数,脚本语言比如VBScript或Jscript不能直接访问。这没问题,因为反正那些脚本语言没有设计成会使用多接口对象。

 

     同时记住上面的宏也会自动定义对象本身拥有一个成员lpVtbl-指向它的VTable的指针:

   typedef struct {
      IMicInVtbl  *lpVtbl;
    }IMicIn;
   typedef struct {
      ILineInVtbl  *lpVtbl;
    }ILineIn;
   typedef struct {
       ISpeakerOutVtbl  *lpVtbl;
    }ISpeakerOut;

在我们的对象中嵌入子对象

     关于子对象有几个规则。其中的一个子对象被当做“基对象”,它的VTable指针必须是我们对象本身的第一个真正的成员。例如我们让我们的IMicIn子对象作为基对象,我们这样定义我们的对象(我们叫它IAudioCard),把IMicIn子对象嵌入到第一的位置,然后嵌入其他子对象:

   typedef struct {
      IMicIn       mic;     // 我们的IMicIn(基)子对象。
      ILineIn      line;    // 我们的ILineIn子对象。
      ISpeakerOut  speaker; // 我们的ISpeakerOut子对象。
    }IAudioCard;

     记住IMicIn的第一个真正的成员是它的lpVtbl(也就是它的VTable指针)。由于我们的IMicIn是直接嵌入在我们的IAudioCard对象结构的一开始位置,这意味着我们的IAudioCard对象里第一个真正的成员实际上是一个指向IMicIn的VTable的指针,这样IMicIn就成为了基对象。所以,IAudioCard其实是开始于一个指向VTable的指针(因为IMicIn以它开始),它的VTable实际上以QueryInterface、AddRef和Release这三个函数开始。

 

     另一个规则是每个子对象的VTable必须拥有它自己的GUID。所以我们需要运行GUIDGEN.EXE来为IMicIn、ILineIn和ISpeakerOut的VTable创建GUID。同时我们也要给我们的IAudioCard对象本身一个GUID。

应用程序获得基对象的方法

 

     通常,应用程序可以通过传入IAudioCard的GUID调用我们的IClassFactory的CreateInstance来获取基对象(在这里是IMicIn)。(或者为了得到我们的基对象,或许应用程序会调用我们添加在我们对象中的另一个额外的函数,像这个系列的前一部分中我们返回我们的集合对象或者IEnumVARIANT对象那样)。我们的IClassFactory的CreateInstance会返回一个指向我们基对象的指针。

   // 获得IAudioCard的基对象的方法。忽略错误检查!
   // 包含定义了我们IAudioCard子对象VTable和所有需要的GUID的.H文件
   #include "IAudioCard.h"
   // 首先,我们需要获得IAudioCard的基对象。假设我们通过调用CoCreateInstance
   // 来获得。我们必须传入包含IAudioCard对象的DLL的GUID。假设这个GUID在
   // IAudioCard.h中已经命名为CLSID_IAudioCard。我们也必须传入IAudioCard
   // 对象的GUID,在这我们假设它叫IID_IAudioCard。CoCreateInstance返回一个
   // 指向IAudioCard的基对象的指针。我们把它存贮到我们的audioCard变量中。
   // 注意我们把audionCard变量申明为IUknown*类型,这是因为我们不知道基对象是哪种
   // 类型的对象。(好,它会是一个IMicIn对象。但如果这个DLL是其他人写的,
   // 我们可能不知道基对象真正的类型)。我们所知道就是它肯定有QueryInterface函数,
   // 因为所有的COM对象都以它开始。
   IUnknown    *audioCard;
   CoCreateInstance(&CLSID_IAudioCard, 0, CLSCTX_ALL,&IID_IAudioCard,
        (void **)&audioCard);
   // “audioCard”现在包含了一个指向基对象的指针(它是什么-在这,它是一个IMicIn,
   // 但我们通常接着调用QueryInterface,如果我们明确我们想要的是IMicIn,
   // 传入IID_IMicIn)。

应用程序通过基对象获取子对象的方法

     应用程序获取我们的IAudioCard的一个子对象的方法是调用基对象(IMicIn)的QueryInterface函数,传入它需要的子对象的VTable的GUID。(现在,你可能知道QueryInterface函数除了允许应用程序检查它是什么类型的对象外还有其他目的)。这意味着当应用程序传入其他子对象的VTable的GUID,基对象的QueryInterface函数应该能识别这个GUID,定位子对象并返回(给应用程序)一个指向子对象的指针。

 

     所以,如果一个应用程序想要得到我们的ISpeakerOut对象,它必须首先获取我们的IAudioCard的基对象。然后,应用程序必须调用基对象的QueryInterface,传入我们的ISpeakerOut的VTable的GUID,QueryInterface会返回指向我们的ISpeakerOut对象的指针。

   // 应用程序得到IAudioCard的ISpeakerOut子对象的方法。忽略了错误检查!
   IUnknown    *audioCard;
   ISpeakerOut *speakerOut;
   // 获取IAudioCard的基对象。
   CoCreateInstance(&CLSID_IAudioCard, 0, CLSCTX_ALL,&IID_IAudioCard,
                     (void**)&audioCard);
   // 现在我们有了基对象“audioCard”(不管它是什么)。调用它的QueryInterface,
   // 请求它的ISpeakerOut子对象。我们调用基对象的QueryInterface,传入ISpeakerOut
   // 的VTable的GUID,我们假设它的名字是IID_ISpeakerOut。QuryInterface
   // 会把这个指针存储在“speakerOut”中。
   audioCard->lpVtbl->QueryInterface(audioCard, &IID_ISpeakerOut,
                                        &speakerOut);
   // 现在我们已经得到了ISpeakerOut,我们可以Release这个基对象了。
   // 注意:如果基对象碰巧是这个ISpeakerOut,那么“speakerOut”和“audioCard”
   // 是同一个指针(对象)。但是CoCreateInstance已经替我们对它做了一次AddRef。
   // 同时QueryInterface也替我们对它做了一次AddRef。所以这次Release操作只撤销了
   // 一次AddRef操作,我们ISpeakerOut没有失效,因此我们还需要对它做再做一个Release。
    audioCard->lpVtbl->Release(audioCard);

从一个子对象获取另一个子对象的方法

     另一个规则是在所有其他子对象没有被应用程序Release前不能删除基对象。如果应用程序获取了子对象的指针,基对象必须保留(即使应用程序Release了它获取的基对象)。为什么?好,这么做是为了与下一条规则保持一致。

 

     通常,每个子对象的QueryInterface函数必须能识别出入的自己的GUID。(也就是说,IMicIn的QueryInterface必须识别它自己的VTable的GUID、ISpeakerOut必须识别它自己的VTable的GUID、ILineIn必须识别它自己的VTable的GUID)。但是,每个子对象的QueryInterface也必须识别它的基对象的GUID,同时能返回指向基对象的指针。

 

     例如,应用程序可能传入我们的IAudionCard的VTable的GUID给ISperakerOut的QueryInterface函数,ISpeakerOut的QueryInterface必须识别它,并返回指向基对象的指针。这表明每个子对象要能定位基对象。当然,这意味着当子对象存在时基对象必须存在。

   // 应用程序从IAudioCard的ISpeakerOut子对象获取它的基对象的方法。忽略错误检查。
   // 假设我们已经获得了指向ISpeakerOut子对象指针,放在我们的“speakerOut”中。
   // 为了得到基对象,我们调用ISpeakerOut的QueryInterface,传入IAudioCard的
   // VTable的GUID。在这,QueryInterface把指针存储在我们的“audioCard”变量中。
   IUnknown    *audioCard;
   speakerOut->lpVtbl->QueryInterface(speakerOut,&IID_IAudioCard,
                                       &audioCard);
   // 注意:当我们使用完基对象时必须要audioCard->lpVtbl->Release(audioCard)。
    事实上,每个子对象必须能识别其他子对象的VTable的GUID,同时能定位、返回指向其他子对象的指针。
    例如,应用程序可能传我们的ILineIn的VTable的GUID给ISpeakerOut的QueryInterface函数。ISpeakerOut的QueryInerface必须识别它,同时返回指向ILineIn子对象的指针。
   // 应用程序从ISpeakerOut子对象获取ILineIn子对象的方法。忽略了错误检查!
   // 假设我们已经获取了指向ISpeakerOut子对象的指针到我们的“speakerOut”变量中。
   // 为了得到ILineIn子对象,我们调用ISpeakerOut的QueryInterface,传入ILineIn的
   // VTable的GUID。在这,QueryInterface存储指针到我们的“lineIn”变量中。
   ILineIn    *lineIn;
   speakerOut->lpVtbl->QueryInterface(speakerOut, &IID_ILineIn,&lineIn);
   // 当我们使用完ILineIn时必须要lineIn->lpVtbl->Release(lineIn)。

委托

 

     因此我们必须确保每个子对象的QueryInterface能识别其他子对象的VTable和IAudioCard的VTable的GUID,同时返回指向相应子对象的指针。

     我们怎样才能做的这一点呢?

     由于我们已经规定一个基对象必须识别所有不同的VTable的GUID(包括它自身的),同时返回指向合适子对象的指针,我们的基对象的QueryInterface已经做了所有定位、返回子对象的工作,因此所有其他子对象的QueryInterface需要做的是调用它的基对象的QueryInterface。换句话说,子对象的QueryInterface“推卸责任”给基对象。毕竟,如果子对象可以得到指向基对象的指针,它就可以像应用程序那样调用它的基对象的QueryInteface。

 

     同时为了保持基对象在所有其他子对象释放前不消失,只要基对象返回了一个子对象,我们就必须把它的引用计数增加一次。这个引用计数会在应用程序每次Release子对象时做相应的减小操作。这样,基对象的引用计数在指向基对象和其他子对象未完指针(outstanding pointer)没有全部Release前不会到零。但注意如果应用程序调用子对象的AddRef函数,我们也必须增加基对象的引用计数,必须保持基对象的引用计数与我们认为应用程序Release子对象的次数一致。所以,子对象的AddRef和Release函数也应该分别调用基对象的AddRef和Release函数。这样,基对象的引用计数在每个子对象每次AddRef后加1,在每个子对象每次Release后减1。

     当子对象调用它的基对象的QueryInterface、AddRef和Release时,我们称这为委托(delegation)。子对象把自己该做的工作委托出去(给它的基对象)。

我们基对象的QueryInterface、AddRef和Release

 

     像前面那样,我们定义我们的IAudioCard,在它里面嵌入三个子对象。因为我们已经选择IMicIn作为基对象,它必须是第一个。我们还必须在我们的对象中增加一个引用计数成员。

   typedef struct {
      IMicIn      mic;     // 我们的IMicIn(基)子对象。
      ILineIn     line;    // 我们的ILineIn子对象。
      ISpeakerOut speaker; // 我们的ISpeakerOut子对象。
      long        count;   // 引用计数。
    }IAudioCard;

     一旦我们分配了IAudioCard对象(可能通过IClassFactory的CreateInstance),那么三个子对象同样也就分配了(因为它们直接存在于IAudioCard的内部)。所以,我们可以在初始化子对象时,用它们的VTable来填充它们各自的lpVtbl成员。

 

     例如,我们的IClassFactory的CreateInstance可以这样做:

   IAudioCard  *thisObj
   // 分配IAudioCard(和它的三个嵌入子对象)
   thisobj = (IAudioCard *)GlobalAlloc(GMEM_FIXED, sizeof(IAudioCard));
   // 把IMicIn的VTable存储到它的lpVtbl成员中
   thisobj->mic.lpVtbl = &IMicIn_Vtbl;
   // 把ILineIn的VTable存储到它的lpVtbl成员中
   thisobj->line.lpVtbl = &ILineIn_Vtbl;
   // 把ISpeakOut的VTable存储到它的lpVtbl成员中
   thisobj->speaker.lpVtbl = &ISpeakerOut_Vtbl;

     当这个CreateInstance调用基对象(IMicIn)的QueryInterface检查应用程序传入的我们的IID_IAudioCard的GUID之后,会用基对象(IMicIn)来填充应用程序的指针。

 

     当应用程序向我们基对象的QueryInterface请求一个子对象时,我们可以直接用指针计算来在IAudioCard中定位它。例如,这是IMicIn的QueryInterface定位ISpeakerOut子对象的方法(假设“this”是指向IMicIn的指针):

   // 应用程序请求的是ISpeakerOut子对象嘛?假设它传入ISpeakerOut的VTable的GUID,应该是这样的。
   if (IsEqualIID(vTableGuid, &IID_ISpeakerOut))
      // 直接定位嵌在IAudioCard中的ISpeakerOut对象
      *ppv = ((unsigned char *)this + offsetof(IAudioCard, speaker));

 

我们子对象的QueryInterface、AddRef和Release

 

     因为每个子对象也直接存在与我们IAudioCard内部,子对象也可以用指针计算来定位基对象(IMicIn)。例如,ISpeakerOut的QueryInterface可以这样调用IMicIn的QueryInerface(假设“this”是ISpeakerOut):

   IMicIn  *base;
   base = (IMicIn *)((unsigned char *)this - offsetof(IAudioCard,speaker));
   base->lpVtbl->QueryInterface(base, tableGuid, ppv);

 

添加子对象到我们对象中的另一种方法

 

     我们可选择的添加子对象到我们对象中的另一种方法是,当应用程序第一次请求子对象时,可以让我们的基对象的QueryInterface来分配和初始化其他子对象。在应用程序请求前子对象实际上不存在。

 

     注意:基对象不能用这种方式实现,它必须是内嵌的。

 

     为了方便,我们在IAudioCard中为每个子对象添加一个额外的成员。这个成员是一个指向子对象的指针。例如,IAudioCard对象可能是这样:

   typedef struct {
      IMicIn      mic;      // 我们的基对象(IMicIn)。它必须是内嵌的。
      ILineIn     *line;    // 指向ILineIn对象的指针。
      ISpeakerOut *speaker; // 指向ISpeakerOut对象的指针。
       long        count;    // 引用计数。
    }IAudioCard;

     当IAudioCard本身被分配时,我们要把这些指针置成0,因为子对象这时还没有创建。在IMicIn的QueryInterface中,当应用程序请求一个子对象时,我们调用GlobalAlloc的并初始化它,然后把指向子对象的指针填充到它的IAudioCard各自成员中。然后,我们把这个指针返回给应用程序。在应用程序下一次调用QueryInterface请求这个对象时,我们只返回已存储的同一个指针。

 

     但由于子对象要能找到基对象,我们需要给每个子对象添加一个额外的指针成员。这个额外的成员用于存储它的基对象的指针。所以,我们的ISpeakerOut对象看起来可以是这样:

   typedef struct {
      ISpeakerOutVtbl  *lpVtbl;   // 我们的ISpeakerOunt的VTable。必须在第一位。
      // 注意:子对象本身不需要引用计数。而是,它们增加、减小基对象的count。
      IMicIn           *base;     // 指向基对象的指针。
    }ISpeakerOut;

     当IMicIn的QueryInterface分配完ISpeakerOut对象后,IMicIn会立即把指向它自己的指针填充到ISpeakerOut基对象的base成员中。

     例如,IMicIn的QueryInterface可能这样返回一个ISpeakerOut子对象:

   // 应用程序请求的是ISpeakerOut子对象嘛?假设它传入ISpeakerOut的
   // VTable的GUID,应该是这样的。
   if (IsEqualIID(vTableGuid, &IID_ISpeakerOut))
    {
      IAudioCard   *myObj;
      // 因为IMicIn是基对象。“this”实际上也指向IAudioCard。
       myObj = (IAudioCard *)this;
      // 如果我们已经分配了ISpeakerOut,那么我们的IAudioCard->speaker成员指向它。
      // 我们只需要返回这个指针。
      if (!myObj->speaker)
      {
         // 我们还没有分配ISpeakerOut。我们现在分配它,把指针存储在
         // IAudioCard->speaker成员中。
         if (!(myObj->speaker = (ISpeakerOut *)GlobaAlloc(GMEM_FIXED,
                                                   sizeof(ISpeakerOut))))
             return(E_OUTOFMEMORY);
         // 设置base成员。
         myObj->speaker->base =this;
         // 设置ISpeakerOut的VTable到它的lpVtbl成员。
         myObj->speaker->lpVtbl = &ISpeakerOut_Vtbl;
      }
      // 返回ISpeakerOut子对象。
      *ppv = myObj->speaker;
    }

     当应用程序最后ReleaseIAudioCard和所有它的子对象时,我们需要检查这些指针成员(在IAudioCard的Release中)和GlobalFree存在的子对象(在我们GlobalFree我们的IAudioCard前)。

     现在,当ISpeakerOut的QueryInterface需要调用它的基对象的QueryInerface时,ISpeakerOut需要做的是(假设“this”是指向ISpeakerOut的指针):

   this->base->lpVtbl->QueryInterface(this->base, tableGuid,ppv);

     把子对象直接嵌入你的对象中,或者把指向子对象的指针放在对象中然后单独分配子对象,两者你可以任意选择。单独分配子对象意味着在应用程序真正需要大的对象前它是不存在的,所以如果一个特定的子对象有许多私有数据成员,这不会有不必要的内存消耗。另一方面,嵌入子对象方式节省了指向每个子对象的额外指针成员,同时无论应用程序何时请求这些子对象,只要他们的父对象一创建他们就是存在的。

     事实上,你甚至可以混合使用这些技术,让一些子对象嵌入,另外子对象单独分配。

多接口对象例程

 

     我们创建一个多接口对象。我们叫这个对象IMultInterface,同时创建一个包含我们对象的名为IMultInterface.DLL的DLL。我们的对象包含叫IBase的基对象,两个额外子对象分别叫ISub1和ISub2。为了说的更明白,我们把ISub1直接嵌入到IMultInterface中(当然Ibase必须也嵌在开始部分),而单独分配ISub2。

     你可以在IMultInerface目录下找到源文件。

     我们不是把三个子对象的函数放在一个源文件中,而是把他们放在三个单独的文件IBase.C、Sub1.C和Sub2.C中,这样做会更好一点。 MultInterface.h包含了三个子对象的定义和所有的GUID。

     为了说的更明白,我在IBase中添加一个叫Sum的额外的函数。

     我在ISub1中添加了一个叫ShowMessage的额外函数。应用程序可以传入一个字符串给这个函数,它会显示一个消息框。

 

     我在ISub2中添加三个名为Increment、Decrement和GetValue额外函数。第一个函数用于增加ISub2的一个long型成员。第二个函数用于减小这个long型成员。最后的函数用于获取这个成员的当前值。

 

     我已经把IMultInterface对象定义在一个单独名为IMultInterface2.h的包含文件。为什么?因为应用程序会#include我们的IMultiInterface.h文件,而我们不想让应用程序知道IMultInterface的真正的内部结构。所以我们把后面信息放在一个单独的.H文件中,它只会被我们的IBase.c、ISub1.c和ISub2.c文件#include。

 

     我把我们的IClassFactory放在IBase.c中。如果你仔细看IBase.c,你会注意到这个基对象和我们第一章中的IExample对象非常像。事实上,大量代码是从IExample.c中逐句拷贝过来的,删除了复制代码中的注释。在IBase.c中剩余注释是把ISub1和ISub2作为子对象添加到IMultInterface中部分。在这里其实没有新东西。最大的变化是基对象的QueryInterface和Release函数。IBase的QueryInterface、AddRef和Release函数被重命名增加了IBase_前缀,同时删除了static。由于ISub1和ISub2的代码是在分开的文件中,他们需要调用基对象的QueryInterface、AddRef和Release函数,我们需要把这些函数做成全局的,避免名字冲突。

     我们IClassFactory的CreateInstance也需要做些小的调整。

     在你把代码编译成一个IMultInterface.dll后,你可以通过修改我们在第一章中的安装程序(源代码在RegIExample目录下)来注册它。用IMultInterface简单替换所有的IExample。你可以通过对UnregIExample进行同样操作来创建一个卸载程序。

使用我们对象的C应用程序例程

 

     IMultInterfaceApp目录中是一个使用我们IMutiInterface对象(也就是它的三个子对象)的C应用程序例程。仔细阅读代码中的注释你会明白应用程序获取基对象和通过另一个子对象来得到其他子对象对象的方法。

 

接下来是什么?

 

     像早些时候提到的,多接口对象不能直接被脚本语言(例如VBScript和JScript)直接使用。为什么?因为为了得到一个子对象,脚本必须能调用QueryInterface函数。为了调用QueryInterface,脚本必须涉及到指向VTable的指针(我们对象的lpVtbl成员)。但是VBScript和Jscript没有如何访问一个指针的概念。

 

     那么这意味着多接口对象对于VBScript或JScript来说根本无用嘛?当然不是。VBScript和JScript引擎本身可以替脚本调用我们对象的QueryInterface。当引擎这么做时有特殊要求嘛?是的-那是需要挂接“事件接收器”当,这些内容将是下一节的话题。

抱歉!评论已关闭.