Adding Macro Scripting language support to existing ATL/WTL Application
Adding Macro Scripting language support to existing ATL/WTL Application
ScreenShots
The ATLScriptHost application must register its component before first used. Run "ATLScriptHost /RegServer" to register them.
Introduction
In my previous article: MFCScriptHost, I was promising to release ATL version of the same class. Now there it is! This article shows how you can integrate Active Scripting technology in your own application. Later, if enough developers are interested, I may provide complete solution to have macro editor/recorder for existing application. Again, this will be provide to you freely!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.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!
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 should be 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 provide files (AtlActiveScriptSite.h) and implement the IActiveScriptHostImpl
for our host object. We will also expose functions (methods) and properties that we want to use with 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 recommend you to use ATL wizard to add methods (and/or properties) to your host object. Adding these stuff manually won't save you time, so go ahead and use it to save some time.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
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
License
This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)