⬆️ ⬇️

C ++ ActiveX Controls Guide with ATL

There are many textbooks on the use of ATL on the Internet, and in particular, on creating COM components with its help, including ActiveX / OLE controls, but most of them for some reason describe the process of poking the mouse at various interesting places in Visual Studio, using graphic tools of the latter, and few of them affect the inside of the code generated by the development environment in a sufficiently deep volume. In the Russian-speaking segment of the Internet, the situation is even worse - there is very little material on ATL, there is practically no (and not only for ATL, but also for creating COM components in general), so I decided to compensate for this disadvantage.



Well, you should probably start with a brief description of what ActiveX components are and what ATL is.



ActiveX is the rebranding of the abbreviations OLE, "Object Linking and Embedding", technology from Microsoft, based on COM, Component Object Model - a language independent component model, invented by MS. OLE allows you to embed individual controls, documents, or just components into different programs written in different programming languages ​​running under Windows. ActiveX controls, in particular, are known for being able to “paste” them into the Internet Explorer web browser, and one of the most well-known such components for IE is, for example, the Adobe Flash module.



The interface of the ActiveX component comes with many well-known and popular programs for Windows, both from Microsoft itself (Windows Media Player, or, for example, programs from Microsoft Office, in particular Word, Excel, etc.) and from third-party companies. (already the aforementioned flash, Adobe Reader, plus many other programs of the same Adobe - for example, Photoshop, if I remember correctly).

')

It is believed that the technology is outdated, or already outdated, but nevertheless, I personally think that it is simply being forced into the field of low-level programming and system services, and while the Windows kernel is written in C, and many components of the system use COM, and are based on COM interfaces, it certainly will not go anywhere.



Now about what ATL is. ATL, Active Template Library is a well-known library from Microsoft that simplifies working with Winapi in C ++. ATL includes classes / templates not only for working with COM / OLE / ActiveX, but also classes for, for example, building and managing GUI, etc.



ATL usually comes with full versions of Microsoft Visual Studio, but if you don’t have it, you can get this library from the Windows DDK .



So, in this article I will describe the process of creating an unpretentious component, which we will have using DirectX 11 tools to draw a spinning sphere rasterized in wireframe, and which will have two methods - Run - start the rotation of the sphere, and Stop - stop the rotation.





To begin with, we need to come up with the interface of our module, come up with a GUID for the library, interfaces, and component class, and record all of this in the 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; }; }; 




Save this file to a file, and call it MyActiveX.idl



As you can see, we recorded the definition of two interfaces, as well as the description of the COM class that implements our interfaces. The first, IMyControl, is the component's interface itself, and the second is needed to alert the outside world about events that happen to our control. We have no events recorded, and we will not do them in our example, so this interface is empty.



The interface implemented by our class belongs to the so-called dual-interfaces. This means that it can be used not only from languages ​​capable of communicating with native-code, and accordingly, capable of communicating with COM components via virtual method tables, but also from scripting languages ​​through the IDispatch interface.



Next, we need to write down the definitions of IMyControl and _IMyControlEvents in the header file for 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__ 




The macro MIDL_INTERFACE is expanded into something like “struct __ declspec (novtable) __ declspec (uuid (GUID string for interface))”. Microsoft quite conveniently integrated COM into its C ++ compiler, and this allows us (and this will be seen especially well) to work with interfaces and COM components from MSVC ++, as with more or less ordinary classes and C ++ structures.



The macro DEFINE_GUID, in turn, is expanded depending on the definition of the macro INITGUID - in the absence of the macro, it declares an extern variable of the GUID type with a specific name. In the case of INITGUID, it also initializes it.



Now we need to define the _Module variable, which belongs to the ATL CComModule class.



Write the variable declaration in a separate header file, say 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 is a class that implements the functionality of a COM module, in particular, registration of component classes, initialization of a COM server, and other such things.



To register COM servers in the registry (and COM works through the registry), we could write a reg-file, or manually create the corresponding entries in the registry, but we can also use the program regsvr32.exe, included in Windows, and allowing automatic registration, by means of the component itself. For this, it is necessary that our library export some functions, and in particular DllRegisterServer and DllUnregisterServer.



CComModule simplifies the whole process of automatic registration, and allows you to reduce it to calling the appropriate methods in the above-mentioned exported library functions, but it requires that the registration script be located in the dll resource section implementing the component. Let's name the file that we add to the resources later, MyControl.rgs, and add the following text there:



 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' } } } 




The most significant parts of the registration script are CLSID, that is, the GUID of the class of our component, ProgID, i.e. CLSID is a human-readable representation, and ThreadingModel is a multithreading model of our component, which in this case is set to Apartment, which means that we can directly access our control only from the thread in which it was created, and all external calls, including those from outside the process (or even from another computer via DCOM) will be serialized and synchronized using COM runtime.



By the way, about serialization, that is, or rather, about the marshaling. In theory, in order to serialize the calls of our component methods and pointers to its interfaces, we have to put in parallel with it a separate library, the so-called proxy-dll, which will be loaded into both the client application (if it is on another computer) and the process where our COM component is loaded.



Microsoft's MIDL compiler could generate code for the proxy library, or as it would be called proxy / stub in this case, but in our case, you can do it easier - because we have more or less standard data types, we can use marshaling runtime OLE. For this case, we need from our IDL file, again using the MIDL compiler, midl.exe (included in both the Windows SDK and VS), compile the so-called type library (type library, tlb), and put it with our component her. Moreover, we can further simplify the whole thing, and include the compiled type library in the DLL resource section, which we will do.



The header file for our module resources:

 #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 of the registration script resource.

IDB_MAIN_ICON - 16x16 icon for our component, in BMP format. I personally took the DirectX icon from the MS DirectX SDK for this file.

IDS_SHADER and SHADER_RESOURCE - the id of the resource and the type of the resource containing the shader code for drawing the sphere.



The resource file itself, MyActiveX.rc, is:

 #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 




We now turn directly to the implementation of our control.

Name the class CMyControl, and create the header file 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__ 




As you can see, the definition of the class turned out quite large.

To implement ActiveX control, our class, in fact, has to implement a very rather large number of various COM interfaces, starting from IDispatch, but since we use ATL, the process is greatly simplified, thanks to inheritance from special classes whose names end with “Impl », Which, strictly speaking, implement the functionality necessary for interfaces in a standard form.



The three most important basic class of our CMyControl - CComObjectRootEx, managing, inter alia, reference counting objects of our class, CComCoClass, implements the class factory (IClassFactory), and CComControl, in turn inherits from CWindowImpl (class, wrapping the HWND, ie the window handle) , and implementing most of the functionality necessary for embedded ActiveX controls.



The most significant macros in the class body:



DECLARE_REGISTRY_RESOURCEID - indicates the id of the resource where the component registration script is located.



BEGIN_COM_MAP + END_COM_MAP - implements the IUnknown interface's QueryInterface method (which is at the top of the COM interface hierarchy), which allows you to get references to different object interfaces (and each of the COM_INTERFACE_ENTRY indicates one of the options).



BEGIN_CONNECTION_POINT_MAP and the corresponding END are necessary to implement the interfaces associated with control event notifications.



BEGIN_MSG_MAP, MESSAGE_HANDLER and END_MSG_MAP - implement the display of Windows-window messages on the methods of C ++ classes.



The OnDraw method will be called every time the control receives a message about the need for a redraw from the system. In addition, the control redrawing will be called by timer.



All the functionality of our component, and, in particular, working with Direct3D, is implemented by the private class CMyControlImpl, according to the pimpl pattern, in the CMyControl.cpp file. I will not describe it in detail, I will only note that in the constructor of CMyControl itself, it is necessary to set the internal property m_bWindowOnly to TRUE - this will mean that our component supports embedding solely into a graphic application.



Also, it is worth noting that in the implementation of the component for managing the counting of references to COM interfaces, the template class CCOMPtr smart pointer from ATL is actively used, very similar to the intrusive_ptr of boost.



Now we will create the MyActiveX.cpp file, in which we will define the class GUIDs and interfaces, the _Module variable, the DLL entry point and the exported functions necessary for the ActiveX module:

 #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); } 




Before compiling the dll, let's determine which functions our module exports in the def file:

 LIBRARY "MyActiveX.dll" EXPORTS DllCanUnloadNow PRIVATE DllGetClassObject PRIVATE DllRegisterServer PRIVATE DllUnregisterServer PRIVATE 




The entire project source code, including the Makefile for building using the Windows SDK, is provided on github, at the link at the end of the article. But first, a few examples of embedding the component:



Embedding in HTML page



 <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> 






Embedding in application on 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(); } } } 






Source Code: github.com/Lovesan/MyActiveX

Source: https://habr.com/ru/post/149277/



All Articles