Create an ActiveX control using ATL that you can use from Fox, Excel, VB6, VB.Net

Creating an ActiveX control is a good exercise in understanding how one works. It also helps to have full control over its source code for learning and testing purposes. A customer asked about migrating legacy ActiveX controls over to .Net. Many controls can be used without any changes in .Net. (See Using ActiveX Controls with Windows Forms in Visual Studio .NET)

Here are some steps to create an ActiveX control using C++ ATL (Active Template Library) and a few wizards. We’ll add a method and an event, and if you’re really ambitious, we’ll add some GDIPlus calls to draw text and a picture.

Start Visual Studio 2005 (2003 works too, but the steps may be a little different). Now create a new project: choose File->New Project->C++->ATL->ATL Project. Call it “MyCtrl”

In the ATL Project Wizard’s Application Settings, choose “Attributed” and Server Type: DLL. Then choose Finish.

Now we’ll add a control to the project: Choose Project->Add Class->ATL->ATL Control to start the ATL Control Wizard.

Enter the short name "TestCtrl". Note that the ProgId is "MyCtrl.TestCtrl". In Options, choose Connection Points for event support, then Finish the wizard.

Now you can host the control in a client, even though it has no functionality yet. Hit F5 to go, allow it to build. When it asks for an Executable to run for debugging, point it to a host like “c:\program files\....\VFP9.EXE”, VB6.exe, or Excel.exe

Note that the output is MyCtrl.DLL. The extension doesn’t matter: if you want to use the traditional OCX extension, change Project->Properties->Configuration->Linker->General->Output file.

To use the control in VB6, right click on the toolbox and select Components->Controls. Add "MyCtrl 1.0 Type Library"

To use the control in Fox, create a new form, put on an ActiveX Control, then choose "CTestCtrl Object"

In VB.Net 2005, right click on the ToolBox Common Controls, choose "Add Toolbox Items", COM, "CTestCtrl Object"

Depending on which client you choose, you can have VS automatically start the client with F5 and load the control on a form by loading a prior project or executing some code. Choose Project->Properties, Configuration Properties->Debugging. Set the Command Arguments to point to some code to execute or a project.

For Fox, I have command arguments set to "-t t" which means to suppress the Log screen (run VFP9 /? to see other options) and run a program called "t" which contains these lines

CLEAR ALL

MODIFY FORM t nowait

RETURN

You can also view the control on a web page. Right click on TestCtrl.htm in Solution explorer and choose to view in a browser. Note all the security warnings!

Now that you see how easy it is to make a control that does nothing, let’s add some functionality.

Let’s add a method: In Class View (View->Class View), expand MyCtrl and right click on ITestCtrl, choose Add->New Method to start the "Add Method Wizard"

We'll add a method that takes a string parameter and returns an integer: a signature like this:

            Function Foobar(bstrString as string) as Integer

In the wizard, set the Method Name to "Foobar"

Now we'll add the first parameter: it's an IN parameter, so check "In". Set the Parameter type to "BSTR" and the name to "bstrString", then choose "Add" to add that parameter.

All COM method calls return HRESULTs, with S_OK (zero) being success. Thus, to get a return value for a method call, we need to add another parameter that's passed by reference, and it's marked by "retval" in the dialog. IOW, if the method has N parameters, the COM method signature will have N+1, with the last parameter being the return value.

However, you'll see the "retval" checkbox disabled until you choose a parameter type that's by reference, such as "LONG *". The "*" indicates it's a pointer, so it's by reference. So Choose "LONG *" and then click on the newly enabled "retval" checkbox. Give it a name "nRetval", then choose "Add".

Now you have 2 parameters for the method.

Similarly, right click on ITestCtrl, choose Add->New Property called "MyString" (this actually adds 2 methods: a get and a set)

In TestCtrl.CPP, put some code in the Method Foobar. To open TestCtrl.CPP, click on CTestCtrl in Class View, and in the bottom pane of Class View, dbl-click on Foobar.

      ::MessageBox(0,bstrString,L"From Foobar",0);

      *nRetval=10;

      return S_OK;

Set a breakpoint on the MessageBox line and on the CTestCtrl::get_MyString and CTestCtrl::set_MyString methods.

Hit F5 to go, or test your control in a client. Call the Foobar method, play with the properties of the control. At breakpoints, examine the call stacks and expand the “this” pointer in the CTestCtrl object to see all the COM Interfaces CTestCtrl implements (I copied them from the debug window, pasted into a new VS C++ file, then used the VS Editor Column Select with Ctrl-Alt drag to select columns):

ATL::CStockPropImpl<CTestCtrl,ITestCtrl,&_GUID_4d088eed_554d_4df0_a5e9_

ATL::IPersistStreamInitImpl<CTestCtrl> {...} ATL::IPersistStreamInit

ATL::IOleControlImpl<CTestCtrl> {...} ATL::IOleControlImpl<CTestCtrl>

ATL::IOleObjectImpl<CTestCtrl> {...} ATL::IOleObjectImpl<CTestCtrl>

ATL::IOleInPlaceActiveObjectImpl<CTestCtrl> {...} ATL::IOleInPlaceAct

ATL::IViewObjectExImpl<CTestCtrl> {...} ATL::IViewObjectExImpl<CTes

ATL::IOleInPlaceObjectWindowlessImpl<CTestCtrl> {...} ATL::IOleInPlac

ATL::IPersistStorageImpl<CTestCtrl> {...} ATL::IPersistStorageImpl<CT

ATL::ISpecifyPropertyPagesImpl<CTestCtrl> {...} ATL::ISpecifyProper

ATL::IQuickActivateImpl<CTestCtrl> {...} ATL::IQuickActivateImpl<CTe

ATL::IDataObjectImpl<CTestCtrl> {...} ATL::IDataObjectImpl<CTestCtrl>

ATL::CComControl<CTestCtrl,ATL::CWindowImpl<CTestCtrl,ATL::CWindow,ATL:

ATL::IConnectionPointContainerImpl<CTestCtrl> {...} ATL::IConnectio

ATL::IPropertyNotifySinkCP<CTestCtrl,ATL::CComDynamicUnkArray> {...}

ATL::CComCoClass<CTestCtrl,&_GUID_b2ab8776_ec21_46a2_acf8_fcb9bd1e97bc>

ATL::CComObjectRootEx<ATL::CComSingleThreadModel> {...} ATL::CComOb

ISupportErrorInfo {...} ISupportErrorInfo

ATL::IConnectionPointImpl<CTestCtrl,&_GUID_6ea2a8a2_c809_4dbd_a938_c792

ATL::IProvideClassInfo2Impl<&_GUID_b2ab8776_ec21_46a2_acf8_fcb9bd1e97bc

Also, you'll see that without any implementation on the property get and set, setting and getting the property from the client does nothing.

Let's add some code to respond to mouse clicks on the control. First we need to write a click handler. From Class View, click on CTestCtrl. Near the top of the Properties window (not the Class View window), click on the Messages icon. Choose WM_LBUTTONDOWN, add code. This creates an OnLButtonDown method for you to fill out. I want the code to raise an event in the client. That means we need to modify the ITestCtrlEvents interface, add the definition of a method that the client will optionally implement.

From Class View, click on the ITestCtrlEvents interface (which was added because we chose Connection Point support above) and add a method as we did for the ITestCtrl interface.

Let's call it MyEvent with a single BSTR parameter bstrEventString and no return value. This will be implemented by the client (Fox or Excel) and will be called from our control.

Now add some code to fire the event in the client in your OnLButtonDown handler in TestCtrl.cpp:

LRESULT CTestCtrl::OnLButtonDown(UINT /*uMsg*/, WPARAM /*wParam*/, LPARAM /*lParam*/, BOOL& /*bHandled*/)

{

      CComBSTR bstr=L"it was clicked";

      MyEvent(bstr); // Fire the event: call the client code, if it exists

      return 0;

}

Now hit F5 and click on the control to see the event get fired. In the Fox client, add code to the Olecontrol1.MyEvent snippet.

*** ActiveX Control Event ***

MESSAGEBOX(bstreventstring+" From Fox event code")

?PROGRAM(),bstreventstring

For more fun, let’s override the OnDraw method and add some GDIPlus calls.

Add these 2 lines after the other #includes as the top of testctrl.h:

#include "gdiplus.h"

using namespace Gdiplus;

Then rename the OnDraw method (in the same file: TestCtrl.h) to be OnDrawOld, and add this code. Be sure to point to a jpg file on your machine and note that backslash is an escape character and needs to be doubled.

(For production code, we’d call GdiplusShutdown and cache the Image.)

Notice how the ActiveX control determines the difference between design time and run time.

HRESULT OnDraw(ATL_DRAWINFO& di)

{

      BOOL fRunMode=0;

      GetAmbientUserMode(fRunMode);

      static Gdiplus::GdiplusStartupInput gdiplusStartupInput;

      static ULONG_PTR gdiplusToken=0;

      if (gdiplusToken == 0) {

            Gdiplus::GdiplusStartup(&gdiplusToken, &gdiplusStartupInput,NULL);// note: call GdiplusShutdown!!!

      }

      Gdiplus::Graphics MyGraphics(di.hdcDraw);

      Gdiplus::Image MyImage(L"d:\\kids.jpg",0);// some jpg: backslash needs to be doubled. Cache this!

      RectF rect(

            (REAL)di.prcBounds->left,

            (REAL)di.prcBounds->top,

            (REAL)(di.prcBounds->right - di.prcBounds->left),

            (REAL)(di.prcBounds->bottom - di.prcBounds->top));

      MyGraphics.DrawImage(&MyImage,rect);

      if (!fRunMode) // if design mode, draw some text

      {

            Gdiplus::Font MyFont(L"Arial",16);

            Gdiplus::StringFormat sf;

            PointF pf(rect.X,rect.Y);

            SolidBrush MyBrush(Color(128,0,128,255));

            MyGraphics.DrawString(L"Design Time!",12,&MyFont,pf,&MyBrush);

      }

      return S_OK;

}

Now we’ll have to link with the GDIPlus library, so go to Project->Properties, Configuration Properties->Linker->input->Additional Dependencies and add Gdiplus.lib.

Hit F5 and have fun!

Below is sample Fox client code that doesn’t use the Fox form designer, so it will not be in design mode:

PUBLIC ox

ox=CREATEOBJECT("MyForm")

ox.visible=1

DEFINE CLASS MyForm as Form

          ADD OBJECT OC as olecontrol WITH ;

                   oleClass="MyCtrl.TestCtrl",;

                   height=200,width=300

          left=200

          AllowOutput=.f.

          PROCEDURE oc.MyEvent(bstreventstring as String)

                   MESSAGEBOX(bstreventstring+" From Fox event code")

                   ?PROGRAM(),bstreventstring

                   ?this.foobar("Call the Foobar method")

ENDDEFINE

When you try it in a VB.Net application, try this code:

    Private Sub AxCTestCtrl1_MyEvent(ByVal sender As System.Object, ByVal e As AxMyCtrl._ITestCtrlEvents_MyEventEvent) Handles AxCTestCtrl1.MyEvent

        MsgBox(e.bstrEventString)

    End Sub

See also ATL Tutorial

You can also create a .Net Usercontrol as an ActiveX control: see: Create a .Net UserControl that calls a web service that acts as an ActiveX control to use in Excel, VB6, Foxpro