Originally published for
CodeProject
Using Active Scripting Interface in ATL Application
Since I already provided some details about how the Script Host works, I will go straight with the details that you need to integrate it into your application.
Hopefully, the steps provided here should work with any ATL/WTL GUI application. Also, I will consider for this article that the reader has some knowledges about ATL/WTL and COM or ActiveX control. There are various articles
in learning COM available here at CodeProject or you can also search on MSDN for a few more. I recommend to try this procedure in a separate project before putting it into an existing application. This will help you to get familiar
with the steps provided here. I believe they are straightforward but may seem complicate for some of us. Give it a try separately before attempting it in your existing application. Also, I didn't try this procedure with Visual Studio
.NET and I hope it should not be too painful.
Procedures
1. Adding IDL file to your project
This step is necessary if your application is not a server (one that was created with COM server option in ATL/WTL wizard), a non-server application (only GUI)
doesn't have any IDL since it doesn't need to expose any interface. Now this is problem for us since later, we will need to register our component in the system. If your application is already an automation server, just skip this
step. If not, let's continue! We will create an IDL file and make it ready for use. First thing first, you will have to create an IDL file with the name of your applicaton (YourAppName) and use GUIDGen.exe (available in tools folder of
Visual Studio) to generate a new GUID for your class library. A typical .IDL file would look like this
// YourAppName.idl : IDL source for YourAppName.exe
//
// This file will be processed by the MIDL tool to
// produce the type library (YourAppName.tlb) and marshalling code.
import "oaidl.idl";
import "ocidl.idl";
[
uuid(XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX),
version(1.0),
helpstring("YourAppName 1.0 Type Library")
]
library YourAppNameLib
{
importlib("stdole32.tlb");
importlib("stdole2.tlb");
};
Now that you have the new .IDL file, you will notice that building your project doesn't create any definitions interfaces files. To do so, you need to
configure your project. Under "Project Settings", you can specify the names of the files by selecting the .IDL file, options will be available under MIDL tab.
Figure 1: IDL file Configuration tab
If you rebuild your project, you will notice that the new generated files
("YourAppName.h" and "YourAppName_i.c") contain informations only about library
and no interfaces. If you use different names, just keep in mind to replace
them when using this procedure. We will then move to the next step and create
our Host object.
2. Creating Active Scripting Site Object
This step shows how to create the Active Scripting Site object. Now if you try to create a new COM ATL class by using "Insert->New Class" command menu, notice
that only "generic c++ class" is available. But what we need is a COM ATL class! First thing to do to get it work, you need to define an object map using the ATL object map macros. You will have to insert the following after the module declaration.
// This is to be included in "YourAppName.cpp"
CAppModule _Module;
BEGIN_OBJECT_MAP(ObjectMap)
END_OBJECT_MAP()
After inserting these lines, now you can create your ATL based-COM object. We will call it "CScriptHost". If you want to use a different, it is up to you!
Figure 2: New COM ATL object class
If you build your project, you may have errors because ATL class-wizard put the object definition outside of the library. One easy way to fix this is to move the entire object definition inside of the library. You will also have a new
set of errors because the wizard didn't add include directives automatically for you. Don't worry, we just need to include the appropriate files in correct places.
// Insert in your "YourAppName.cpp"
#include "ScriptHost.h"
#include "YourAppName_i.c"
// Insert this in "ScriptHost.h"
#include "YourAppName.h"
That's it, now no error right? If you still have error verify that you didn't
miss something in the previous step. We will now customize our host to do
really job for us!
3. Active Scripting Host implementation
Congratulations, if you got to that step, then the rest is straightforward! Most complications that you may encounter are due to the fact
that your application was a simple GUI. As most advanced ATL developers would probably guess, now we only need to include the header file
(AtlActiveScriptSite.h) and implement
IActiveScriptHostImpl interface for our host object. We will also expose methods and properties for our object. In summary the end result should look like this:
class CScriptHost :
public CComObjectRoot,
public CComCoClass<CScriptHost,&CLSID_ScriptHost>,
public ISupportErrorInfo,
public IConnectionPointContainerImpl<CScriptHost>,
public IActiveScriptHostImpl<CScriptHost>,
public IDispatchImpl<IScriptHost, &IID_IScriptHost, &LIBID_YOURAPPNAMELib>
{
public:
CScriptHost() {}
BEGIN_COM_MAP(CScriptHost)
COM_INTERFACE_ENTRY(IScriptHost)
COM_INTERFACE_ENTRY(IActiveScriptSite)
COM_INTERFACE_ENTRY(IActiveScriptSiteWindow)
COM_INTERFACE_ENTRY(ISupportErrorInfo)
COM_INTERFACE_ENTRY(IConnectionPointContainer)
COM_INTERFACE_ENTRY(IDispatch)
END_COM_MAP()
// .... other lines
// IScriptHost
public:
STDMETHOD(CreateEngine)(BSTR pstrProgID);
STDMETHOD(CreateObject)(/*[in]*/BSTR strProgID,
/*[out,retval]*/LPDISPATCH* ppObject);
STDMETHOD(AddScriptItem)(/*[in]*/BSTR pstrNamedItem,
/*[in]*/LPUNKNOWN lpUnknown);
STDMETHOD(AddScriptCode)(/*[in]*/BSTR pstrScriptCode);
STDMETHOD(AddScriptlet)(/*[in]*/BSTR pstrDefaultName,
/*[in]*/BSTR pstrCode,
/*[in]*/BSTR pstrItemName,
/*[in]*/BSTR pstrEventName);
};
I highly recommend to use ATL wizard to add methods and properties for your object. Adding these stuff manually won't save you time, so go ahead and use it to save some time.
Figure 3: Adding Methods and properties to your host object
Now you can call actual implementation code for your host object. For examples:
STDMETHODIMP CScriptHost::CreateEngine(BSTR pstrProgID)
{
BOOL bRet
= IActiveScriptHostImpl<CScriptHost>::CreateEngine( pstrProgID );
return (bRet? S_OK : E_FAIL);
}
STDMETHODIMP CScriptHost::CreateObject(BSTR strProgID, LPDISPATCH* ppObject)
{
LPDISPATCH lpDispatch = IActiveScriptHostImpl
<CScriptHost>::CreateObjectHelper( strProgID );
*ppObject = lpDispatch;
return ((lpDispatch!=NULL)? S_OK : E_FAIL);
}
Now that your object is ready to be used, the next step will show you how you can register it and create instance of it
4. Prepare Script Hosting services
Before we can use any COM object, as you may know, it must be registered in your system. The cleaner way to do this is to follow current convention in ATL code, which means we will give the user option to register/unregister the
typelib for our application. We will also create a registry resource. This step is necessary for GUI application to become server. If your application is already a server, you may need to skip this step.
// FindOneOf function
LPCTSTR FindOneOf(LPCTSTR p1, LPCTSTR p2)
{
while (p1 != NULL && *p1 != NULL)
{
LPCTSTR p = p2;
while (p != NULL && *p != NULL)
{
if (*p1 == *p)
return CharNext(p1);
p = CharNext(p);
}
p1 = CharNext(p1);
}
return NULL;
}
// WinMain codes
// Modify your WinMain to have these lines
hRes = _Module.Init(ObjectMap, hInstance, &LIBID_YOURAPPNAMELib);
ATLASSERT(SUCCEEDED(hRes));
TCHAR szTokens[] = _T("-/");
int nRet = 0;
BOOL bRun = TRUE;
LPCTSTR lpszToken = FindOneOf(lpstrCmdLine, szTokens);
while (lpszToken != NULL)
{
if (lstrcmpi(lpszToken, _T("UnregServer"))==0)
{
_Module.UpdateRegistryFromResource(IDR_SCRIPTHostServer, FALSE);
nRet = _Module.UnregisterServer(TRUE);
bRun = FALSE;
break;
}
if (lstrcmpi(lpszToken, _T("RegServer"))==0)
{
_Module.UpdateRegistryFromResource(IDR_SCRIPTHostServer, TRUE);
nRet = _Module.RegisterServer(TRUE);
ATLASSERT( SUCCEEDED(nRet) );
bRun = FALSE;
break;
}
lpszToken = FindOneOf(lpszToken, szTokens);
}
if (bRun)
{
hRes = _Module.RegisterClassObjects(CLSCTX_LOCAL_SERVER,
REGCLS_MULTIPLEUSE);
int nRet = 0;
// BLOCK: Run application
{
CMainDlg dlgMain;
nRet = dlgMain.DoModal();
}
_Module.RevokeClassObjects();
}
For Single/Multi document view application, you need to call
RegisterClassObjects and
RevokeClassObjects before you call the
Run function
that creates the GUI main window. Also you have to create a new resource to register your application. Let's say the name of the "REGISTRY" resource is
IDR_SCRIPTHost_Server, it should look like this (use GUIDGen to create new GUID):
HKCR
{
NoRemove AppID
{
ForceRemove {11111111-1111-1111-1111-111111111111} = s 'YourAppName'
'YourAppName.exe'
{
val AppID = s {11111111-1111-1111-1111-111111111111}
}
}
}
Use Resource editor to create a new "REGISTRY" resource and save it as a ".rgs" file. You will also have to use the Resource Includes to define the typelib
resource. Insert following text:
#ifdef _DEBUG
1 TYPELIB "Debug\\YourAppName.tlb"
#else
1 TYPELIB "Release\\YourAppName.tlb"
#endif
Figure 4: Resource Includes dialog
5. Using Script Hosting services
Now that everything is in place, the last thing to do is to create an instance of the host object. Add any named item that you want to expose from scripting
language and you are set! Typical initialization code will look like this.
// Create
CComPtr<IScriptHost> m_pScriptHost;
HRESULT hr = m_pScriptHost.CoCreateInstance( L"YourAppName.ScriptHost");
if (SUCCEEDED(hr))
{
m_pScriptHost->CreateEngine( L"JScript" );
// Adding named-item other than the "ScriptHost"
// Web Browser
IWebBrowser* pWebBrowser = NULL;
GetDlgControl(IDC_WEBBROWSER, __uuidof(IWebBrowser), (void**)&pWebBrowser);
m_pScriptHost->AddScriptItem(L"webBrowser", pWebBrowser);
pWebBrowser->GoHome();
}
else
{
MessageBox(_T("Class not registered!!!"));
}
You will then add the macro text by calling
AddScriptCode method:
CComBSTR bstrText;
GetDlgItemText(IDC_TXT_SCRIPT, bstrText.m_str);
if (m_pScriptHost)
m_pScriptHost->AddScriptCode( bstrText );
This is it, the article may seem long but the steps are pretty much what you
need to follow to get the work done. Step 1 and 4 are easier if your
application already exposes some interfaces. I put together a simple demo that
shows how it can be used. I would like to hear from you as I plan to provide
more advanced solutions to integrate full macro support for existing
application (ATL and/or MFC). Enjoy!
References
View my
previous solution for MFC application.
Download WTL7