Руководство по созданию ActiveX-контролов на C++ с помощью ATL

    В интернете существует множество учебников по использованию ATL, и в частности, по созданию COM-компонентов с его помощью, в том числе и ActiveX/OLE контролов, но большинство из них почему-то описывают процесс тыкания мышкой в разные интересные места Visual Studio, использование графических инструментов последней, и мало какой из них затрагивает нутро сгенерированного средой разработки кода в достаточно глубоком объеме. В русскоязычном сегменте интернета ситуация и того хуже — материалов по ATL крайне мало, практически нет(да и не только по ATL, а и по созданию COM-компонентов вообще), поэтому я решил этот недостаток компенсировать.

    Ну, начать следует, наверное, с краткого описания того, что из себя представляют ActiveX-компоненты и что такое ATL.

    ActiveX это ребрендинг аббревиатуры OLE, «Object Linking and Embedding», технологии от Microsoft, основанной на COM, Component Object Model — языконезависимой компонентной модели, придуманной MS. OLE позволяет встраивать отдельные контролы, документы, да или просто компоненты в разные программы, написанные на разных языках программирования, работающие под Windows. ActiveX-контролы, в частности, известны тем, что их можно «вклеивать» в веб-браузер Internet Explorer, и одним из самых известных таких компонентов для IE является, к примеру, модуль Adobe Flash.

    Интерфейс ActiveX-компонента поставляют с собой многие известные и популярные программы для Windows, как от самой Microsoft(Windows Media Player, или, например, программы из Microsoft Office, в частности Word, Excel и т.п.), так и от сторонних компаний(уже вышеупомянутый флеш, Adobe Reader, плюс многие другие программы того же Adobe — например Photoshop, если я правильно помню).

    Есть мнение, что технология устаревает, или уже устарела, но тем не менее, я лично считаю, что она просто вытесняется в область низкоуровневого программирования и системных сервисов, и пока ядро Windows пишется на Си, а многие компоненты системы используют COM, и основаны на COM-интерфейсах, она уж точно никуда не денется.

    Теперь про то, что такое ATL. ATL, Active Template Library — известная библиотека от Microsoft, которая упрощает работу с Winapi на C++. В ATL входят классы/шаблоны не только для работы с COM/OLE/ActiveX, но и классы для, к примеру, построения и управления GUI и т.п.

    ATL обычно поставляется с полноценными версиями Microsoft Visual Studio, но если у вас ее нет, то можете взять эту библиотеку из Windows DDK.

    Так вот, в данной статье я опишу процесс создания простенького компонента, который будет у нас средствами DirectX 11 рисовать крутящуюся растеризованную в wireframe сферу, и у которого будет два метода — Run — запустить вращение сферы, и Stop — остановить вращение.


    Для начала нам нужно придумать интерфейс нашего модуля, придумать GUID для библиотеки, интерфейсов и класса компонента, и записать все это, как водится, в MIDL, Microsoft Interface Definition Language.

    [
      uuid(1E4E47F3-21AF-407C-9544-59C34C81F3FA),
      version(1.0),
      helpstring("MyActiveX 1.0 Type Library")
    ]
    library MyActiveXLib
    {
      importlib("stdole32.tlb");
      importlib("stdole2.tlb");
    
      [
        object,
        uuid(2B26D028-4DA6-4D69-9513-D0CA550949D1),
        dual,
        helpstring("IMyControl Interface"),
        pointer_default(unique)
      ]
      interface IMyControl : IDispatch
      {
        [id(1), helpstring("method Run")] HRESULT Run();
        [id(2), helpstring("method Stop")] HRESULT Stop();
      };
    
      [
        uuid(E7D13B5A-0A09-440A-81EA-C9E3B0105DB0),
        helpstring("_IMyControlEvents Interface")
      ]
      dispinterface _IMyControlEvents
      {
        properties:
        methods:
      };
    
      [
        uuid(5747094E-84FB-47B4-BC0C-F89FB583895F),
        helpstring("MyControl Class")
      ]
      coclass MyControl
      {
        [default] interface IMyControl;
        [default, source] dispinterface _IMyControlEvents;
      };
    };
    
    


    Сохраним это дело в файл, и назовем MyActiveX.idl

    Как видно, мы записали определение двух интерфейсов, а также описание COM-класса, который реализует наши интерфейсы. Первый, IMyControl, у нас представляет собой собственно интерфейс компонента, а второй нужен для оповещения внешнего мира о событиях, которые случаются с нашим контролом. Событий у нас не записано, и мы их делать в нашем примере не будем, поэтому этот интерфейс у нас пустой.

    Интерфейс, реализуемый нашим классом, относится к так называемым dual-интерфейсам. Это значит, что его можно использовать не только из языков, способных на общение с native-кодом, а соответственно, и способных общаться с COM-компонентами через таблицы виртуальных методов, но и из скриптовых языков, посредством интерфейса IDispatch.

    Далее нам нужно записать определения IMyControl и _IMyControlEvents в заголовочном файле для C++ — MyControl.hpp
    #ifndef __MY_CONTROL_HPP__
    #define __MY_CONTROL_HPP__
    
    #include <windows.h>
    
    typedef interface IMyControl IMyControl;
    
    MIDL_INTERFACE("2B26D028-4DA6-4D69-9513-D0CA550949D1")
    IMyControl : IDispatch
    {
    public:
      virtual HRESULT STDMETHODCALLTYPE Run() = 0;
      virtual HRESULT STDMETHODCALLTYPE Stop() = 0;
    };
    
    MIDL_INTERFACE("E7D13B5A-0A09-440A-81EA-C9E3B0105DB0")
    _IMyControlEvents : public IDispatch
    {
    };
    
    DEFINE_GUID(IID_IMyControl,0x2B26D028,0x4DA6,0x4D69,0x95,0x13,0xD0,0xCA,0x55,0x09,0x49,0xD1);
    
    DEFINE_GUID(LIBID_MyActiveXLib,0x1E4E47F3,0x21AF,0x407C,0x95,0x44,0x59,0xC3,0x4C,0x81,0xF3,0xFA);
    
    DEFINE_GUID(DIID__IMyControlEvents,0xE7D13B5A,0x0A09,0x440A,0x81,0xEA,0xC9,0xE3,0xB0,0x10,0x5D,0xB0);
    
    DEFINE_GUID(CLSID_MyControl,0x5747094E,0x84FB,0x47B4,0xBC,0x0C,0xF8,0x9F,0xB5,0x83,0x89,0x5F);
    
    #endif __MY_CONTROL_HPP__
    


    Макрос MIDL_INTERFACE раскрывается во что-то вроде «struct __declspec(novtable) __declspec(uuid(строка GUID для интерфейса))». Microsoft довольно удобно интегрировали COM в свой компилятор C++, и это позволяет(и дальше это будет видно особенно хорошо) нам работать с интерфейсами и компонентами COM из MSVC++, как с более-менее обычными классами и структурами C++.

    Макрос DEFINE_GUID же, в свою очередь, раскрывается в зависимости от определения макроса INITGUID — в случае отсутствия оного, он декларирует extern-переменную типа GUID с определенным названием. В случае INITGUID, он ее еще и инициализирует.

    Теперь следует определить переменную _Module, которая принадлежит классу CComModule из ATL.

    Запишем декларацию переменной в отдельный заголовочный файл, скажем MyActiveX.hpp
    #ifndef __MY_ACTIVE_X_HPP__
    #define __MY_ACTIVE_X_HPP__
    
    #include <windows.h>
    #include <atlbase.h>
    
    extern CComModule _Module;
    
    #endif // __MY_ACTIVE_X_HPP__
    


    CComModule это класс, реализующий функциональность COM-модуля, в частности регистрацию классов компонентов, инициализацию COM-сервера и прочие подобные вещи.

    Для регистрации COM-серверов в реестре(а COM работает именно через реестр) мы могли бы написать reg-файл, или вручную создать в реестре соответствующие записи, но также мы можем использовать программу regsvr32.exe, входящую в состав Windows, и позволяющую проводить автоматическую регистрацию, средствами самого компонента. Для этого необходимо, чтобы наша библиотека экспортировала некоторые функции, и в частности DllRegisterServer и DllUnregisterServer.

    CComModule упрощает весь процесс автоматической регистрации, и позволяет сводить ее к вызову соответствующих методов в вышеупомянутых экспортируемых функциях библиотеки, но ему требуется, чтобы в секции ресурсов dll, реализующего компонент, находился скрипт регистрации. Назовем файл, который мы позже добавим в ресурсы, MyControl.rgs, и добавим туда такой текст:

    HKCR
    {
      MyActiveX.MyControl.1 = s 'MyControl Class'
      {
        CLSID = s '{5747094E-84FB-47B4-BC0C-F89FB583895F}'
      }
      MyActiveX.MyControl = s 'MyControl Class'
      {
        CLSID = s '{5747094E-84FB-47B4-BC0C-F89FB583895F}'
        CurVer = s 'MyActiveX.MyControl.1'
      }
      NoRemove CLSID
      {
        ForceRemove {5747094E-84FB-47B4-BC0C-F89FB583895F} = s 'MyControl Class'
        {
          ProgID = s 'MyActiveX.MyControl.1'
          VersionIndependentProgID = s 'MyActiveX.MyControl'
          ForceRemove 'Programmable'
          InprocServer32 = s '%MODULE%'
          {
            val ThreadingModel = s 'Apartment'
          }
          ForceRemove 'Control'
          ForceRemove 'Insertable'
          ForceRemove 'ToolboxBitmap32' = s '%MODULE%, 101'
          'MiscStatus' = s '0'
          {
              '1' = s '139665'
          }
          'TypeLib' = s '{1E4E47F3-21AF-407C-9544-59C34C81F3FA}'
          'Version' = s '1.0'
        }
      }
    }
    


    Наиболее значимые части скрипта регистрации — CLSID, то есть GUID класса нашего компонента, ProgID, т.е. человекочитаемое представление CLSID, и ThreadingModel — модель многопоточности нашего компонента, которая в данном случае устанавливается в Apartment, что значит что напрямую к нашему контролу можно обращаться только из того потока, в котором он был создан, а все внешние вызовы, в том числе извне процесса(или даже с другого компьютера — через DCOM) будут сериализоваться и синхронизироваться средствами рантайма COM.

    Кстати, о сериализации, то есть, вернее сказать, о маршалинге. В теории, для того чтобы сериализовать вызовы методов нашего компонента и указатели на его интерфейсы, мы должны поставить параллельно с ним отдельную библиотеку, так называемую прокси-dll, которая будет подгружаться как в клиентское приложение(если оно на другом компьютере), так и к процессу, куда загружен наш COM-компонент.

    Компилятор MIDL от Microsoft мог бы генерировать код для прокси-библиотеки, или как она в данном случае называлась бы, proxy/stub, но в нашем случае можно поступить проще — так как типы данных у нас более-менее стандартные, мы может использовать встроенный маршалинг рантайма OLE. Для данного дела нам нужно из нашего IDL-файла, опять же средствами компилятора MIDL, midl.exe(входит в состав как Windows SDK, так и VS), скомпилировать так называемую библиотеку типов(type library, tlb), и поставить с нашим компонентом ее. Более того, мы можем еще дальше упростить все это дело, и включить скомпилированную библиотеку типов в секцию ресурсов DLL, что мы и сделаем.

    Заголовочный файл для ресурсов нашего модуля:
    #ifndef __RESOURCE_H__
    #define __RESOURCE_H__
    
    #define IDB_MAIN_ICON 101
    #define IDR_MYCONTROL 102
    #define IDS_SHADER 103
    #define SHADER_RESOURCE 256
    
    #endif // __RESOURCE_H__
    


    IDR_MYCONTROL — id ресурса скрипта регистрации.
    IDB_MAIN_ICON — иконка 16x16 для нашего компонента, в формате BMP. Я лично для этого файла взял иконку DirectX из MS DirectX SDK.
    IDS_SHADER и SHADER_RESOURCE — id ресурса и типа ресурса, содержащего код шейдеров для отрисовки сферы.

    Сам файл ресурсов, MyActiveX.rc, такой:
    #include <windows.h>
    #include "Resource.h"
    
    LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US
    
    1 TYPELIB "MyActiveX.tlb"
    
    IDR_MYCONTROL REGISTRY "MyControl.rgs"
    
    IDB_MAIN_ICON BITMAP "DirectX.bmp"
    
    IDS_SHADER SHADER_RESOURCE "MyActiveX.fx"
    
    VS_VERSION_INFO VERSIONINFO
     FILEVERSION 1,0,0,0
     PRODUCTVERSION 1,0,0,0
     FILEFLAGSMASK 0x3fL
    #ifdef _DEBUG
     FILEFLAGS 0x1L
    #else
     FILEFLAGS 0x0L
    #endif
     FILEOS 0x4L
     FILETYPE 0x2L
     FILESUBTYPE 0x0L
    BEGIN
        BLOCK "StringFileInfo"
        BEGIN
            BLOCK "040904B0"
            BEGIN
                VALUE "CompanyName", "\0"
                VALUE "FileDescription", "MyActiveX component module\0"
                VALUE "FileVersion", "1, 0, 0, 0\0"
                VALUE "InternalName", "MyActiveX\0"
                VALUE "LegalCopyright", "Copyright 2012 (C) Dmitry Ignatiev <lovesan.ru at gmail.com>\0"
                VALUE "OriginalFilename", "MyActiveX.dll\0"
                VALUE "ProductName", "MyActiveX component module\0"
                VALUE "ProductVersion", "1, 0, 0, 0\0"
                VALUE "OLESelfRegister", "\0"
            END
        END
        BLOCK "VarFileInfo"
        BEGIN
            VALUE "Translation", 0x409, 1200
        END
    END
    


    Теперь перейдем непосредственно к реализации нашего контрола.
    Назовем класс CMyControl, и создадим заголовочный файл CMyControl.hpp
    #ifndef __CMY_CONTROL_HPP__
    #define __CMY_CONTROL_HPP__
    
    #include <atlbase.h>
    #include <atlcom.h>
    #include <atlctl.h>
    #include "Resource.h"
    #include "MyActiveX.hpp"
    #include "MyControl.hpp"
    
    class DECLSPEC_UUID("5747094E-84FB-47B4-BC0C-F89FB583895F") CMyControl :
      public CComObjectRootEx<CComSingleThreadModel>,
      public CStockPropImpl<CMyControl, IMyControl, &IID_IMyControl, &LIBID_MyActiveXLib>,
      public CComControl<CMyControl>,
      public IPersistStreamInitImpl<CMyControl>,
      public IPersistPropertyBagImpl<CMyControl>,
      public IObjectSafetyImpl<CMyControl, INTERFACESAFE_FOR_UNTRUSTED_CALLER | INTERFACESAFE_FOR_UNTRUSTED_DATA>,
      public IOleControlImpl<CMyControl>,
      public IOleObjectImpl<CMyControl>,
      public IOleInPlaceActiveObjectImpl<CMyControl>,
      public IViewObjectExImpl<CMyControl>,
      public IOleInPlaceObjectWindowlessImpl<CMyControl>,
      public IConnectionPointContainerImpl<CMyControl>,
      public IPersistStorageImpl<CMyControl>,
      public ISpecifyPropertyPagesImpl<CMyControl>,
      public IQuickActivateImpl<CMyControl>,
      public IDataObjectImpl<CMyControl>,
      public IProvideClassInfo2Impl<&CLSID_MyControl, &DIID__IMyControlEvents, &LIBID_MyActiveXLib>,
      public IPropertyNotifySinkCP<CMyControl>,
      public CComCoClass<CMyControl, &CLSID_MyControl>
    {
    public:
    DECLARE_REGISTRY_RESOURCEID(IDR_MYCONTROL)
    
    DECLARE_PROTECT_FINAL_CONSTRUCT()
    
    BEGIN_COM_MAP(CMyControl)
      COM_INTERFACE_ENTRY(IMyControl)
      COM_INTERFACE_ENTRY(IDispatch)
      COM_INTERFACE_ENTRY(IViewObjectEx)
      COM_INTERFACE_ENTRY(IViewObject2)
      COM_INTERFACE_ENTRY(IViewObject)
      COM_INTERFACE_ENTRY(IOleInPlaceObjectWindowless)
      COM_INTERFACE_ENTRY(IOleInPlaceObject)
      COM_INTERFACE_ENTRY2(IOleWindow, IOleInPlaceObjectWindowless)
      COM_INTERFACE_ENTRY(IOleInPlaceActiveObject)
      COM_INTERFACE_ENTRY(IOleControl)
      COM_INTERFACE_ENTRY(IOleObject)
      COM_INTERFACE_ENTRY(IPersistStreamInit)
      COM_INTERFACE_ENTRY(IPersistPropertyBag)
      COM_INTERFACE_ENTRY(IObjectSafety)
      COM_INTERFACE_ENTRY2(IPersist, IPersistStreamInit)
      COM_INTERFACE_ENTRY(IConnectionPointContainer)
      COM_INTERFACE_ENTRY(ISpecifyPropertyPages)
      COM_INTERFACE_ENTRY(IQuickActivate)
      COM_INTERFACE_ENTRY(IPersistStorage)
      COM_INTERFACE_ENTRY(IDataObject)
      COM_INTERFACE_ENTRY(IProvideClassInfo)
      COM_INTERFACE_ENTRY(IProvideClassInfo2)
    END_COM_MAP()
    
    BEGIN_PROP_MAP(CMyControl)
    END_PROP_MAP()
    
    BEGIN_CONNECTION_POINT_MAP(CMyControl)
      CONNECTION_POINT_ENTRY(IID_IPropertyNotifySink)
    END_CONNECTION_POINT_MAP()
    
    BEGIN_MSG_MAP(CMyControl)  
      MESSAGE_HANDLER(WM_SETFOCUS, OnSetFocus)
      MESSAGE_HANDLER(WM_CREATE, OnCreate)
      MESSAGE_HANDLER(WM_DESTROY, OnDestroy)
      MESSAGE_HANDLER(WM_SIZE, OnSize)
      MESSAGE_HANDLER(WM_TIMER, OnTimer)
      CHAIN_MSG_MAP(CComControl<CMyControl>)
    END_MSG_MAP()
    
    DECLARE_VIEW_STATUS(VIEWSTATUS_SOLIDBKGND | VIEWSTATUS_OPAQUE)
    
      CMyControl();
      ~CMyControl();
      STDMETHOD(Run)();
      STDMETHOD(Stop)();
      HRESULT OnDraw(ATL_DRAWINFO& di);
    
    private:
      LRESULT OnCreate(UINT /*uMsg*/, WPARAM /*wParam*/, LPARAM /*lParam*/, BOOL& /*bHandled*/);
      LRESULT OnDestroy(UINT /*uMsg*/, WPARAM /*wParam*/, LPARAM /*lParam*/, BOOL& /*bHandled*/);
      LRESULT OnSize(UINT /*uMsg*/, WPARAM /*wParam*/, LPARAM /*lParam*/, BOOL& /*bHandled*/);
      LRESULT OnTimer(UINT /*uMsg*/, WPARAM /*wParam*/, LPARAM /*lParam*/, BOOL& /*bHandled*/);
      CMyControl(const CMyControl& copy);
      class CMyControlImpl;
      CMyControlImpl *_impl;
    };
    
    #endif // __CMY_CONTROL_HPP__
    


    Как видно, определение класса получилось довольно большим.
    Для реализации ActiveX-контрола наш класс, на самом деле, должен реализовать крайне немаленькое количество разнообразных COM-интерфейсов, начиная от IDispatch, но так как мы используем ATL, процесс очень сильно упрощается, благодаря наследованию от специальных классов, названия которых оканчиваются на «Impl», которые собственно говоря, и реализуют необходимую для интерфейсов функциональность в стандартном виде.

    Три наиболее важных базовых класса нашего CMyControl — CComObjectRootEx, управляющий, в частности, подсчетом ссылок объектов нашего класса, CComCoClass, реализующий фабрику класса(IClassFactory), и CComControl, в свою очередь наследующийся от CWindowImpl(класс, обертывающий HWND, то есть дескриптор окна), и реализующий большую часть необходимой для встроенных ActiveX-контролов фунциональности.

    Наиболее значимые макросы в теле класса:

    DECLARE_REGISTRY_RESOURCEID — указывает id ресурса, в котором находится скрипт регистрации компонента.

    BEGIN_COM_MAP + END_COM_MAP — реализуют метод QueryInterface интерфейса IUnknown(который является вершиной иерархии интерфейсов COM), который позволяет получать ссылки на разные интерфейсы объекта(и каждая из COM_INTERFACE_ENTRY указывает один из вариантов).

    BEGIN_CONNECTION_POINT_MAP и соответствующий END — необходимы для реализации интерфейсов, связанных с оповещениями о событиях контрола.

    BEGIN_MSG_MAP, MESSAGE_HANDLER и END_MSG_MAP — реализуют отображение сообщений Windows-окна на методы C++ классов.

    Метод OnDraw у нас будет вызываться каждый раз, когда контрол будет получать от системы сообщение о необходимости перерисовки. Кроме этого, перерисовка контрола у нас будет вызываться по таймеру.

    Вся функциональность нашего компонента, и, в частности, работа с Direct3D, реализуется приватным классом CMyControlImpl, согласно паттерну pimpl, в файле CMyControl.cpp. Я не буду в деталях его описывать, отмечу только то, что в конструкторе самого CMyControl необходимо выставить внутреннее свойство m_bWindowOnly в TRUE — это будет означать, что наш компонент поддерживает встраивание исключительно в графические приложение.

    Также, стоит отметить что в реализации компонента для управления подсчетом ссылок на COM-интерфейсы активно используется шаблонный класс умного указателя CComPtr из ATL, очень похожий на intrusive_ptr из boost.

    Теперь создадим файл MyActiveX.cpp, в котором у нас будет определены GUID класса и интерфейсов, переменная _Module, а также реализована точка входа DLL и необходимые для ActiveX-модуля экспортируемые функции:
    #include <windows.h>
    #include <atlbase.h>
    #include "MyActiveX.hpp"
    #include "CMyControl.hpp"
    
    const IID IID_IMyControl = {0x2B26D028,0x4DA6,0x4D69,{0x95,0x13,0xD0,0xCA,0x55,0x09,0x49,0xD1}};
    const IID LIBID_MyActiveXLib = {0x1E4E47F3,0x21AF,0x407C,{0x95,0x44,0x59,0xC3,0x4C,0x81,0xF3,0xFA}};
    const IID  DIID__IMyControlEvents= {0xE7D13B5A,0x0A09,0x440A,{0x81,0xEA,0xC9,0xE3,0xB0,0x10,0x5D,0xB0}};
    const CLSID CLSID_MyControl= {0x5747094E,0x84FB,0x47B4,{0xBC,0x0C,0xF8,0x9F,0xB5,0x83,0x89,0x5F}};
    
    CComModule _Module;
    
    BEGIN_OBJECT_MAP(ObjectMap)
      OBJECT_ENTRY(CLSID_MyControl, CMyControl)
    END_OBJECT_MAP()
    
    extern "C" BOOL WINAPI DllMain(HINSTANCE hInstance, DWORD dwReason, LPVOID lpReserved)
    {
      if(DLL_PROCESS_ATTACH == dwReason)
      {
        _Module.Init(ObjectMap, hInstance, &LIBID_MyActiveXLib);
        DisableThreadLibraryCalls(hInstance);
      }
      else if(DLL_PROCESS_DETACH == dwReason)
        _Module.Term();
      return TRUE;
    }
    
    STDAPI DllCanUnloadNow(void)
    {
        return (_Module.GetLockCount()==0) ? S_OK : S_FALSE;
    }
    
    STDAPI DllGetClassObject(REFCLSID rclsid, REFIID riid, LPVOID* ppv)
    {
        return _Module.GetClassObject(rclsid, riid, ppv);
    }
    
    STDAPI DllRegisterServer(void)
    {
        return _Module.RegisterServer(TRUE);
    }
    
    STDAPI DllUnregisterServer(void)
    {
        return _Module.UnregisterServer(TRUE);
    }
    
    


    Прежде чем скомпилировать dll, определим, какие функции наш модуль экспортирует, в def-файле:
    LIBRARY      "MyActiveX.dll"
    
    EXPORTS
      DllCanUnloadNow     PRIVATE
      DllGetClassObject   PRIVATE
      DllRegisterServer   PRIVATE
      DllUnregisterServer PRIVATE
    


    Весь исходный код проекта, включая Makefile для сборки с помощью Windows SDK, приведен на github, по ссылке в конце статьи. Но сначала несколько примеров встраивания компонента:

    Встраивание в HTML-страницу


    <html>
    <head>
      <title>Test page for MyControl ActiveX object</title>
      <script type="text/javascript">
    var running = false;
    function OnClick()
    {
      var ctl = document.getElementById("MyControl");
      var btn = document.getElementById("btn");
      if(running)
      {
        ctl.Stop();
        running = false;
        btn.value = "Run";
      }
      else
      {
        ctl.Run();
        running = true;
        btn.value = "Stop";
      }
    }
    </script>
    </head>
    <body>
      <center>
        <input type=button value="Run" id="btn"
               style="display:block; padding: 3px 20px;"
               onclick="OnClick();"/>
        <object id="MyControl"
                style="width:500px; height:500px;"
                classid="CLSID:5747094E-84FB-47B4-BC0C-F89FB583895F">
        </object>
      </center>
    </body>
    </html>
    



    Встраивание в приложение на Windows.Forms


    using System;
    using System.Windows.Forms;
    
    namespace MyControl
    {
        class MyControl : AxHost
        {
            public MyControl()
                : base("5747094E-84FB-47B4-BC0C-F89FB583895F")
            {
            }
    
            public void Run()
            {
                dynamic ax = GetOcx();
                ax.Run();
            }
    
            public void Stop()
            {
                dynamic ax = GetOcx();
                ax.Stop();
            }
        }
    
        class Program
        {
            [STAThread]
            static void Main()
            {
                Application.EnableVisualStyles();
    
                Form f = new Form();
                f.Text = "My control";
                f.StartPosition = FormStartPosition.CenterScreen;
                f.Width = 640;
                f.Height = 480;
    
                MyControl c = new MyControl();
                c.Dock = DockStyle.Fill;
                c.BeginInit();
    
                Button b = new Button();            
                b.Dock = DockStyle.Top;
                b.Text = "Run/Stop";
    
                bool running = false;
                b.Click += (s, e) =>
                {
                    if (running)
                    {
                        c.Stop();
                        running = false;
                    }
                    else
                    {
                        c.Run();
                        running = true;
                    }
                };
    
                f.Controls.Add(b);
                f.Controls.Add(c);
                f.ShowDialog();
            }
        }
    }
    



    Исходный код: github.com/Lovesan/MyActiveX
    Share post

    Comments 43

      +9
      COM и тем более на ATL разве кто-то еще пишет?
        +6
        пишет :)
          +3
          COM-интерфейсы имеют LzmaSdk (aka 7z api), .NET Hosting API, в виде COM-компонента оформлен DirectX…

          Кроме того, COM был и остается способом «по умолчанию» для универсального взаимодействия между исполнимыми модулями, написанными на разных языках, в тех случаях, когда простых экспортируемых функций становится недостаточно.
            +1
            Интересно, в каких случаях может быть недостаточно простых экспортируемых функций? Они же позволяют передавать между модулями любые данные и даже код…
            ИМХО, COM — классический пример оверинжениринга, заставляющий для простейших действий писать тонны маловразумительного кода. Абсолютно везде COM-архитектуру можно было бы заменить обычными DLL, вместо того чтобы городить огород с регистрацией в реестре, GUID'ами, моникерами и COM-серверами.
            Если так уж хотелось классовой обёртки, чтобы через "->" можно было к функциям обращаться, то можно было бы сделать классовую обёртку непосредственно в хеадерах, тем более что там и так вся метаинформация ручками набита (спрашивается, на кой вообще придумали IDL, если всё равно инфа дублируется в хеадерах?).
            Похоже на то, что COM исходно задумывался для потенциально опасного (недоверенного) кода — например, кода Internet-апплетов. Все эти подписи, GUID'ы, регистрация — очень уж напоминают движение в сторону restricted-кода, который выполняется в песочнице с жёстким контролем доступа. Но в итоге продукт получился весьма сырым и недоработанным.
              +6
              COM это в первую очередь бинарный стандарт для межплатформенного(или если выражаться прямее — межъязыкового, в плане языков программирования), межпроцессного и даже сетевого(DCOM) взаимодействия.

              В JS и VB нет «хеадеров», в C# их тоже нет, и уж точно их нет в каком-нибудь Common Lisp. Модульность во всех трех реализована совершенно по-разному. Зато и там и там и в третьем, и даже в четвертом, есть более-менее похожее ООП.

              Кроме того, COM это некоторая такая попытка пофиксить недостатки C++ — а именно: отсутствие ABI, управление памятью, модель ООП и отсутствие reflection.
                +1
                Простых экспортируемых функций недостаточно, когда продукт постоянно развивается. Через некоторое время возникнет нехороший эффект версионности. Короче — dll hell.
                  +4
                  Интересно, в каких случаях может быть недостаточно простых экспортируемых функций? Они же позволяют передавать между модулями любые данные и даже код…


                  В случае сложного объектно-ориентированного API, разумеется. Я привел три примера, которые в виде простых экспортируемых функций стали бы абсолютным кошмаром.

                  ИМХО, COM — классический пример оверинжениринга, заставляющий для простейших действий писать тонны маловразумительного кода. Абсолютно везде COM-архитектуру можно было бы заменить обычными DLL, вместо того чтобы городить огород с регистрацией в реестре, GUID'ами, моникерами и COM-серверами.


                  Обязательный элемент среди вами перечисленных — GUID'ы. Возможно, вы удивитесь, но все остальное глубоко опционально.

                  Если так уж хотелось классовой обёртки, чтобы через "->" можно было к функциям обращаться, то можно было бы сделать классовую обёртку непосредственно в хеадерах, тем более что там и так вся метаинформация ручками набита (спрашивается, на кой вообще придумали IDL, если всё равно инфа дублируется в хеадерах?).

                  Затем, что C++ — это далеко не единственной язык программирования! Мне вот другое не понятно, зачем TLB придумали…
                    +4
                    Судя по всему, вы юзаете COM. Удивительно, что вы не знаете о TLB. Впрочем, наверное, просто никогда особо не интересовались. TLB хранит информацию о типах (метаинформацию). Она нужна, например, для автоматического маршалинга, когда на основе TLB генерятся proxy & stub. TLB нужна для загрузки COM-компонента в среде, которая не имеет никакой информации о самом компоненте. Например, директива #import, которая «кушает» dll и генерит tli/tlh хидеры со всеми интерфейсами, гуидами и прочими необходимыми вещами на самом деле работает с type library, вкомпиленной в DLL. Другие языки (не С++) тоже таким образом работают с бинарными компонентами.
                      0
                      Все тоже самое хранится и в IDL-файле. TLB по сути — всего лишь его двоичное представление.
                        +2
                        Да, так и есть. Я не совсем понимаю суть вашего возражения. Или это просто констатация факта? В idl вы описываете типы, потом использвуется midl, компилятор IDL, который переводит все в бинарную форму и это добро вкомпиливается в длл. Или вы считаете, что все должно храниться в текстовом виде?
                          0
                          Да, это машиночитаемое представление, которое в runtime используется средой исполнения COM, например, для автоматического маршалинга.
                        0
                        Вы глубоко удивитесь, но GUID — тоже опционально. А вот QueryInterface — обязательно :-)
                          0
                          QueryInterface принимает тот самый GUID обязательным параметром…
                            0
                            :-) тут уж не поспоришь
                        +1
                        Для нас COM в первую очередь — компонентная архитектура. Она позволяет достичь модульности и независимости компонентов друг от друга.
                        COM на самом деле тяжеловесен и громоздок, но при работе над крупным многокомпонентным проектом открывается, что это всё нужно.
                        Я думаю, что если бы у нас не было COM, то мы бы придумали какой-нибудь его недоработанный аналог.
                        CORBA в принципе тоже подошла, но тогда бы был отдельный геморрой интеграции с .NET.
                      0
                      Windows Runtime — даже не само новое API, а новый подход к API от Microsoft — это как раз таки COM (с небольшой подтяжкой морщин типа неймспейсов, отсутствия множественного наследования и пр..).

                      Что до ATL — вполне нормальная библиотека (на чем же еще COM писать?). Ее связь с WRL (новой темплейтной библиотекой для работы с COM/WinRT для тех, кто не хочет использовать C++/CX) значительно менее очевидна, чем между парой COM/WinRT, но все равно чувствуется значительное влияние ATL на WRL.
                      +14
                      Десять лет, десять долгих лет я ждал этой статьи…
                        +8
                        Бррр, хочу это развидеть! Код с комом вызвал ужасные воспоминания (не как MFC конечно, но тоже мало приятного). К счастью, уже много лет не приходилось возиться с com-ом и надеюсь, что больше не придется.
                          0
                          Почему?
                            +1
                            Почему что?
                              –2
                              Почему надеетесь. Помимо того, что «computers suck» какие Ваши претензии к COM?
                                –5
                                Уродливая технология, которая портит архитектуру даже клиентов не говоря уже о провайдерах com-компонентов. Уродливый синтаксис и конвенции, ужасная система регистраций, гуйдов и прочей фигни, плохо соответствует и ложится на принципы ООП, полностью платформо-зависимая и вообщем-то совершенно ненужная при наличии тонны других способов взаимодействия между компонентами и процессами. Да, иногда не остается выбора работая со сторонними компонентами (в основном майкрософтовскими), но внутри своей собственной архитектуры я бы ни за что в жизни эту дрянь не стал бы брать.
                                  +5
                                  Другими словами, Вы сами не знаете что Вам не нравится — лучше уж «тонна других способов» (aka «зоопарк велосипедов»), чем одна стандартная, невероятная гибкая, крайне логичная (если не залазить в дебри, но подобные дебри остались бы дебрями независимо от того, на какой компонентной модели они построены), оттестированная на миллионах сценариев использования и на сотнях миллиардов пользователей (кумулятивно за последние 20 лет). Ах, да, COM кроме масштабирования «вверх» (всякие связывания-с-моникерами-на-удаленной-машине-используя-кастомные-маршалеры-и-кастомные-транстпортные-протоколы), очень хорошо масштабируется «вниз» (graceful degradation): можно сделать
                                  class MyObject: public IUnknown {
                                  //...
                                  }
                                  

                                  с пустой реализацией AddRef и Release и это уже COM объект, который можно передавать куда угодно. Никакой регистрации. GUID — это просто 128-битное случайное число, которое гарантированно глобально-уникально. Все. Что Вам не нравится в глобально уникальных случайных идентификаторах? Синтаксис и конвенции — это где? В IDL? Расскажите больше. «Плохо соотетствует и ложится на принципы ООП» — это глупость. А «полностью платформо-зависимая» — так и вообще идиотизм.

                                  А вообще, немного почитав Ваши комментарии/посты, «что-то» мне подсказывает, что дело вовсе даже и не в технологии (у меня есть ОГРОМНЫЕ подозрения, что Вы в ней даже и не пытались разбираться) — дело в производителе. Все, конечно, не так запущено как в случае amarao, но тенденция тоже отлично просматривается.
                                    +4
                                    На всякий случай поправлюсь, чтоб не придирались к словам.
                                    … это уже COM объект...

                                    Это конечно же класс, а не объект. Объект можно инстанцировать («активировать» в терминах COM) любым понравившимся способом: хоть на стеке, хоть в куче, хоть в data секции и управлять его временем жизни хоть вручную, хоть отдельным самопальным счетчиком ссылок, хоть настоящим сборщиком мусора.

                                    COM — это прежде всего QueryInterface + жесткий ABI, а весь COM рантайм с маршалерами, апартментами, моникерами, регистрацией/активацией — это просто хелперы. Они призваны УПРОЩАТЬ жизнь, если эта функциональность нужна, а не усложнять требованием обязательно реализовывать все известные человечеству интерфейсы. Простейший пример: «классический» COM, Windows Runtime и registration-free COM имеет три совершенно разных способа активации компонентов, но при это все три между собой бинарно совместмы после того, как активация произведена.
                                      –3
                                      Я действительно сильно глубоко в com не копал, но работая в windows тоже волей не волей приходилось сталкиваться, и уж точно не могу сказать что это «гибкая и логичная» система, и уж точно не «стандартная» так как опять-таки привязана к конкретной платформе (потрудитесь объяснить, почему это «идиотизм»?).

                                      >«Плохо соотетствует и ложится на принципы ООП» — это глупость
                                      Глупость? Хехе. Видимо вы никогда не пытались объектно ориентированную архитектуру экспортировать в ком :) Наследование ком-интерфейсов — это нечто странное, но не как не стандартное «is-a» отношение. COM не умеет многих стандартных фич из ООП языков как множественное наследование от классов или интерфейсов, полиморфизм, перегрузка и т.д. Да, на ком можно легко наложить какой-нибудь простенький недокласс (данные+функции) но никак не не более — все остальное это жуткая головная боль и тонны костылей.

                                      >дело в производителе
                                      И у меня нет предвзятости к майкрософту, но есть предвзятость к закрытым платформо-зависимым решениям вроде COM. Просто я много писал (и пишу) кросспатформенный код и занимаюсь портированием, и не раз сталкивался, когда с ситуацией, когда портирование многократно усложнялось, из за наличия вот такой вот хрени. Единственная претензия к Майкрософту, что они любят проталкивать свои собственные костыли под видом «стандарта», после чего толпы виндузятных программистов понятия не имеют, где заканчивается стандартные фичи языков, стандартные библиотеки и начинается платформо зависимая пропраятщина, что особенно заметно в С++.
                                        +3
                                        так как опять-таки привязана к конкретной платформе (потрудитесь объяснить, почему это «идиотизм»?)

                                        Ну даже если не принимать во внимание тот факт что существуют сторонние реализации COM рантайма под не-Windows платформы, человек должен совершенно ничего не понимать в COM, чтобы говорить, что технология как таковая привязана к конкретной платформе. Присмотритесь к эппловскому CoreFoundation или к мозиловскому nsISupports. Ничего не напоминает? Мне все еще необходимо объяcнять «почему это идиотизм»?

                                        Видимо вы никогда не пытались объектно ориентированную архитектуру экспортировать в ком :)

                                        Вы заблуждаетесь в двух вещах: в том, что считаете, что понимаете что такое «объектно-ориентированная архитектура» и в том, что считаете, догадываетесь что я пытался и чего не пытался делать. «Наследование» интерфейсов — неудачный термин (и деталь реализации в некоторых языках), интерфейсы РЕАЛИЗУЮТСЯ классами. Причем (сюрприз!!!), далеко не только в COM. Еще для меня загадка, с каких пор множественное наследование стало стандартной фичей ООП языков, и с каких пор COM перестал поддерживать реализацию множества интерфейсов (Вы же это имеете в виду?). Полиморфизм — э-э-э Вы точно понимаете, что такое полиморфизм? Что же до перегрузки — действительно перегрузка не поддерживается напрямую (потому что ABI остается одним и тем же и для языков, которые поддерживают перегрузку и для тех, которые не поддерживают — да-да, перегрузка — тоже не такая уж «стандартная фича ООП языков»), с другой стороны поддерживается аннотация [overload] для языков, которые перегрузку поддерживают и хотят спроецировать данный метод под перегруженным именем.

                                        … Единственная претензия к Майкрософту, что они любят проталкивать свои собственные костыли под видом «стандарта»...

                                        Каждый раз когда я вижу подобные утверждения мне хочется научиться бить людей по TCP/IP. Вы, похоже, сами не осознаете, но данным абзацем Вы ПОЛНОСТЬЮ подтвердили мое утверждение о том, что у Вас ABM (пусть даже и в начальной стадии).
                                          –3
                                          Не вижу смысла продолжать дискуссию при ваших постоянных попытках перехода на личности, вы даже ничего не ответили по сути, кроме обвинений в том, что «я ничего не понимаю». Кстати, забавно, но все, что вы пишите, указывает на то, что вы сами являетесь ярым фанбоем, в чем почему то пытаетесь обвинить меня, хотя я я просто написал почему мне не нравится одна устаревшая кривоватая технология, а вовсе не «наезжал» на вашу любимую компанию.
                                            +3
                                            Хм, я ничего не ответил? А ссылочки там для чего? По первому абзацу остались вопросы?

                                            По множественному наследованию как стандартной фиче ООП языков. Возьмем два самых популярных ООП языка — Java и C#.
                                            java.lang.Class содержит два аксессора:
                                            public Class getSuperclass()
                                            public Class[] getInterfaces()

                                            Прошу отметить, что базовый класс всегда один, интерфейсов — несколько, и обе сущности отделены друг от друга.

                                            System.Type содержит проперти
                                            public abstract Type BaseType { get; }
                                            и аксессор
                                            public abstract Type[] GetInterfaces()

                                            Прошу отметить, что имеем ОДИН базовый класс, несколько интерфейсов и то и другое является отдельными сущностями.

                                            Возьмем еще один популярный OOP язык — JavaScript. Ой, там вообще классов нет, а вместо них прототипы.

                                            Продолжать?

                                            Реализация множества интерфейсов коклассами. Даже не знаю, зачем я должен это писать — это нужно или знать, или не рассуждать о том, чем COM является и чем не является.

                                            Полиморфизм. QueryInterface это полиморфизм и есть. Концентрированный. Вы работаете с интерфейсом, а coclass-ы, реализующие этот интерфейс уже сами решают чего им делать.

                                            «Проталкивание собственных костылей под видом стандартов»
                                            Оставим в стороне Вашу личную оценку качества предлагаемых технических решений («костыль» или не «костыль» решать тем, кто хотя бы в принципе разбирается в том, что предлагается). Но ведь, как и предполагалось, Вы не понимаете, в чем Ваша ошибка. Хорошо, попробую подтолкнуть Вас к тому, чтоб Вы сами осознали. Критикуя — предлагай. Какая альтернатива: КТО не «проталкивает собственные костыли под видом стандартов» (из тех, кто делает хоть что-то)? И откуда вообще берутся эти самые «несобственный костыли»?

                                            Ах да, Вы наезжали не на «мою любимую компанию» и даже не на мою любимую технологию — Вы просто необоснованно критиковали (и упорствуете, хотя уже сами же признали, что Вы даже и не пытались в ней толком разобраться) технологию. Мне не нравится, когда критика высказывается людьми, которые не понимают о чем говорят (вон взгляните там ниже amarao выступил — стоило оставить этот выпад неотвеченным просто для того, чтоб не быть уличенным в «любви к компании»?). В общем, я никого не хотел обидеть (и прошу прощения, если таки обидел), но стереотипы вокруг Microsoft уже настолько надоели, что начали раздражать.
                            –2
                            ATL? COM? Закопайте обратно. :)
                              +3
                              Опоздали с топиком лет эдак на 10-15. Ну а че, давайте писать гайды по кодингу для DOS например =)
                                0
                                Ну раз уж поперли довольно глупые штампы, давайте и я: «А Вы разве не знали, что Windows это всего лишь оболочка над DOS и есть, следовательно любое программирование под Windows это и есть программирование под DOS».
                                  +2
                                  Вы опоздали с этим заявлением на 10 лет :)
                                –5
                                ActiveX? Это технология, которая работает ТОЛЬКО на Windows, ТОЛЬКО в IE и ТОЛЬКО в 32-битах?

                                ActiveX? Это технология, которая позволяет запускать программы с сайтов без согласия пользователя и даёт этим программам 100% доступ к всем документам и настройкам пользователя, включая пароли, куки и прочие приватные вещи?

                                Да вы рехнулись, батенька.
                                  +6
                                  Красноглазик, отаку, гик, нерд, фанат линукса, недоброжелатель майкрософта, оракла, и т.д.
                                    –5
                                    Сейчас вы мне расскажите, какое благо есть быть ActiveX, и миллион сисадминов, которые настраивают всякие уродливые банк-клиенты с «IE6 only» это подтвердят, да?
                                    +6
                                    Красноглазые иксперты такие искперты.
                                    В первом же абзаце из трех утверждений — 2 фактических ошибки («ТОЛЬКО в IE и ТОЛЬКО в 32-битах») и одно утверждение верно с большими натяжками («ТОЛЬКО на Windows»). Во втором абзаце неверно вообще все.

                                    Почему бы Вам не перестать позориться и выскакивать с заявлениями о Windows? И ведь это не первый раз, когда Вам демонстрируют, что Ваши знания о Windows… как бы это помягче сказать… далеко не исчерпывающи. Либо Вы в принципе неспособны воспринимать информацию и рефлексировать, либо Вы неспособны воспринимать информацию и рефлексировать только если дело касается Microsoft (а также Apple, Oracle и пр.).

                                    Нет, я серьезно, либо уже перестаньте влазить в посты, посвященные Windows либо таки сделайте усилие и РАЗБЕРИТЕСЬ в вопросах, по которым у Вас уже есть Мнение. Спасибо-пожалуйста-у-меня-все.
                                      –5
                                      Жду примера ActiveX под Debian/Hurd с ARM-архитектурой. В качестве браузера… Ну, допустим, opera.
                                        +4
                                        Мдя, говорю ж — не лечится.

                                        Давайте по порядку.
                                        «ТОЛЬКО в IE» — читаем статью: «Встраивание в приложение на Windows.Forms».
                                        «ТОЛЬКО в 32 битах» —
                                        «ТОЛЬКО на Windows» — здесь, как я уже сказал, с некоторыми натяжками можно согласиться. Во первых этих Windows — было три штуки разных Windows NT, Windows 9x и Windows CE (Embedded Compact). На всех трех работал ActiveX — сам по себе он от платформы не зависит. А вот и пример вполне себе ActiveX под «Debian/Hurd с ARM». Вот еще один

                                        Что же до Вашего второго абзаца, у меня есть встречное предложение. Вы создаете простенький ActiveX и даете мне ссылку. Я по ней кликаю, далее происходит следующее: Ваша «программа» запускается без моего согласия, Вы «получаете 100% доступ ко всем моим документам, настройкам, включая пароли куки и прочие приватные вещи», думаю похищения хабракуки и комментария от моего имени «pwned, Windows suxx» будет более чем достаточно, чтоб подвердить Вашу позицию (только, пожалуйста, без мошенничества вроде неизвестной XSS на хабре или типа того — нужно провести все исключительно через ActiveX, устанавливаемый без моего согласия).
                                          +3
                                          Хм, вполне осознаю, что «встречное предложение» потребует неоправданно много усилий, так что давайте хотя бы так: Вы СЛОВАМИ обрисовываете план «атаки» и попутно рассказываете, почему дыра такой величины НИ РАЗУ в истории не использовалась злоумышленниками (они, лентяи, все больше на соц инженерию нажимают)?

                                          И, пожалуйста, попробуйте не опозориться. В очередной раз.
                                    0
                                    А почему вы хидер с интерфейсами вручную делали? Удобнее будет взять хидеры, которые midl генерирует.
                                      0
                                      да ну просто чтобы читабельнее и понятнее вышло
                                        0
                                        Да, то что генерит midl лучше не читать :) Наверное, все же, стоит указать, что так в реальной жизни так обычно не делают. Еще одну штуку нашел:

                                        >>> свойство m_bWindowOnly в TRUE — это будет означать, что наш компонент поддерживает встраивание исключительно в графические приложение.

                                        Не совсем понятно, что вы имели в виду. Это свойство говорит о том, может ли контрол работать в windowless ркжиме, т.е. без выделенного для себя HWND: отрисовываться в контексте, который предоставляется контейнером и получать от него информацию о необходимых событиях.
                                          0
                                          касательно m_bWindowOnly — согласен, выразился как-то совсем кривовато. Имел ввиду именно что контролу нужно «свое окошко».

                                    Only users with full accounts can post comments. Log in, please.