ActiveX script hosting (2)

This code represents one of my current experiments with active script
hosting. I am not sure if the code will be useful for you, i am not even
sure if the code will be useful for me either. The explanation of my
uncertainty is in the readme.txt file, packed along with the project.
I have prepared this article not just to show you an example, but mostly
to ask you some questions being in hope for receiving some answers.

In this experiment i have tried to make a program that would use the
Microsoft Forms 2.0 designer, along with abilities to control the form,
created by the designer, with programs written in VBScript.

When you run the program open the file “TestForm.frm”. This is an
example of form. You can use the menu item “View|Design mode” to
edit form’s design, and the menu item “View|Edit script” to see
the VBScript source of the form.

I have tested the program under NT 4.0 and Windows 95 (4.00.950).
Under NT4 everything goes ok, but under Windows 95 i see messages
during execution in the debug window telling:

First-chance exception in MsForm.exe (GDI32.DLL): 0xC0000005: Access Violation.

When i try to exit, the program causes a GPF in OLEAUTO.DLL and dies.
I do not know so far what is wrong with my code.

I am not going to describe how to create active script hosts. There are
some articles in Internet concerning the topic. I have even tried to
describe it myself. Here i will try to explain how to add the MSForm designer
to your MFC project, and how to connect the designer to the scripting engine.

If you run the “OLE-COM Object Viewer” program and select the menu item
“File|View TypeLib…” and select the “FM20.DLL” file located
in your Windows System folder, you will see the coclass “UserForm”.
Observe that the interface to the “UserForm” coclass (_UserForm) has been
inherited form the interface to the “Frame” colass (IOptionFrame). Having
noticed this feature, i have made some steps for creating an MFC wrapper
class for MSForm designer in my project.

  1. In the Developer studio selected the menu item
    “Project|Add to project|Components and controls”.

  2. In the “Registered ActiveX Controls” folder selected the
    “Microsoft Forms 2.0 Frame” control. The MSDEV created some wrapper
    MFC classes to work with Microsoft controls.

  3. In all created files replaced the word “OptionFrame” with “UserForm”.
    I have changed the names of the “optionframe.cpp” and “optionframe.h” files
    accordingly.

  4. Modified the CUserForm::Create functions with new ones which use
    CLSID of the “UserForm” coclass.

  5. Manualy implemented the hidden “DesignMode” property of the form.

Thus i have created the MFC wrapper class for MSForm control. The name
of the wrapper class is CUserForm.

My next step was creating the four classes CMsFormDoc, CMsFormView,
CVbscriptView and CMSFormFrame.

The CMsFormDoc object almost does nothing except of serializing
and providing links between FormView and VbscriptView objects.

The CVbscriptView is a simple editor to edit a vbscript code for
a form.

The CMSFormFrame provides a way of containing two views – the MsFormView,
and the CVbscriptView together in a single frame.

The CMsFormView object carries on and controls a CUserForm object and
on user requests switches on and off the design mode of the form.

Take a look at the CMsFormView::GetUserFormUnknown() function. If you
want to obtain the IUnknown interface to an object created by
CWnd::CreateControl function, you will not be able to get it by calling
the GetIDispatch function. CWnd object incapsulates the OLE control
container features by some undocumented classes which are defined
in the header file located in the MFC source folder.


//The code to obtain the IUnknown interface to the control
IUnknown* CMsFormView::GetUserFormUnknown()
{
IUnknown* pUnk=m_UserForm.GetControlUnknown();
return pUnk;
}

The two functions – CMsFormView::WriteDesignToFile(LPCTSTR lpstrFileName)
and CMsFormView::ReadDesignFromFile(LPCTSTR lpstrFileName) implement the
form control contents writing to and reading from a temporary file.

The CMsFormDoc::Serialize(CArchive& ar) function use these functions
to store and load the design information.

Connecting the form with the scripting engine is not difficult. In this
experiment i used the CScriptEngine class provided by ….
I have made the CMsFormScriptEngine class derived form CScriptEngine to
pass the information needed from the form to the scripting engine. An
instance of the CMsFormScriptEngine is a part of CMsFormView class.


class CMsFormView : public CView
{
…..
CUserForm m_UserForm;
CMsFormScriptEngine m_ScriptEngine;
};

The CMsFormView::Run function feeds the scripting engine with named
items, script code, and switches it to the “connected” state. Here
there is a problem. A scripting engine can process events from
objects, referenced by the AddNamedItem function, and their properties
– objects which can source events by themselves. The information about
object’s properties the scripting engine obtains from the type library.
Since form designer has undefined set of properties, at the
moment of compilation of the project, the type library must be
created dynamicaly. Moreover, the IDispatch interface to such an object
must support dynamic properties. The example of creating such an object
and such a typelib is the “Axscript” project, located in the ActiveX SDK
examples folder. I am not familiar with the ICreateTypeLib, ICreateTypeInfo
and IProvideMultipleInfo interfaces. I failed to find any documentation
to these interfaces. When i tried to repeat the code, shown in the example,
whith MFC, i had not achieved the results i wanted.

I have solved the problem by adding references to all the controls in the
form to the scripting engine. Observe that the reference to the “Form”
named item is added with the SCRIPTITEM_GLOBALMEMBERS. All named items are
added with the SCRIPTITEM_ISVISIBLE|SCRIPTITEM_ISSOURCE flags.


void CMsFormView::Run()
{
if(!m_ScriptEngine.IsCreated())
m_ScriptEngine.Create(CLSID_VBScript);
else
{
SCRIPTSTATE ss=m_ScriptEngine.GetScriptState();
if(ss==SCRIPTSTATE_CONNECTED)
return;
}

CString strScript=GetDocument()->GetScriptText();
if(!strScript.IsEmpty() && m_UserForm.GetDesignMode()!=-1)
{
HRESULT hr;

USES_CONVERSION;
LPCOLESTR pstrItemName = T2COLE(_T(“Form”));
LPCOLESTR pstrCode = T2COLE(strScript);
EXCEPINFO ei;

hr=m_ScriptEngine.AddNamedItem(pstrItemName, SCRIPTITEM_ISVISIBLE|SCRIPTITEM_ISSOURCE|SCRIPTITEM_GLOBALMEMBERS);
if(FAILED(hr))
{
AfxMessageBox(_T(“AddNamedItem failed. Item name: <Form>.”));
return;
}

//Adding form controls
for(long i=0; i<m_UserForm.GetControls().GetCount(); i++)
{
CString strControlName=m_UserForm.GetControls()._GetItemByIndex(i).GetName();

LPCOLESTR pstrControlName;
pstrControlName = T2COLE(strControlName);
hr=m_ScriptEngine.AddNamedItem(pstrControlName,SCRIPTITEM_ISVISIBLE | SCRIPTITEM_ISSOURCE);
if(FAILED(hr))
{
CString strError=_T(“AddNamedItem failed. Item name: <“);
strError+=strControlName+_T(“>”);
AfxMessageBox(strError);
return;
}
}

if(SUCCEEDED(m_ScriptEngine.ParseScriptText(pstrCode, pstrItemName, NULL, NULL, 0, 0, 0L, NULL, &ei)))
m_ScriptEngine.Run();
else
AfxMessageBox(_T(“Can’t parse script text”));
}
}

The interfaces to the named items are obtained by the scripting engine in the
CMsFormScriptEngine::OnGetItemInfo function. The CMsFormScriptEngine object
has a back pointer (m_pView) to the CMsFormView object in which it is
contained. The technique of obtaining the class information of an object
i have seen in several examples. I just do not understand why the
IProvideClassInfo interface to the object is not released. When i tried
to release the interface, my program caused a GPF.


IProvideClassInfo* pProvideClassInfo = NULL;
hr = pUnk->QueryInterface(IID_IProvideClassInfo, (void**)&pProvideClassInfo);

// if the object supports IprovideClassInfo …
if (SUCCEEDED(hr) && pProvideClassInfo != NULL)
{
hr = pProvideClassInfo->GetClassInfo(ppTypeInfo);

//The pProvideClassInfo->Release() should not be called. Why?
}

/////////////////////////////////////////////////////////////////////////////
// CMsFormScriptEngine message handlers
HRESULT CMsFormScriptEngine::OnGetItemInfo(
/* [in] */ LPCOLESTR pstrName,
/* [in] */ DWORD dwReturnMask,
/* [out]*/ IUnknown** ppUnknownItem,
/* [out]*/ ITypeInfo** ppTypeInfo)
{
USES_CONVERSION;

HRESULT hr=CScriptEngine::OnGetItemInfo(pstrName,dwReturnMask,ppUnknownItem,ppTypeInfo);
if(!m_pView)
return hr;

hr=TYPE_E_ELEMENTNOTFOUND;
if (dwReturnMask & SCRIPTINFO_ITYPEINFO)
{
if (!ppTypeInfo)
return E_INVALIDARG;
*ppTypeInfo = NULL;
}

if (dwReturnMask & SCRIPTINFO_IUNKNOWN)
{
if (!ppUnknownItem)
return E_INVALIDARG;
*ppUnknownItem = NULL;
}

TCHAR* lpstrItemName=OLE2T(pstrName);
IUnknown* pUnk=0;

if(!_tcscmp(_T(“Form”), lpstrItemName))
{
pUnk=m_pView->GetUserFormUnknown();
}
else
{
CControls Controls=m_pView->m_UserForm.GetControls();
for(long i=0; i<Controls.GetCount(); i++)
{
CControl C=Controls._GetItemByIndex(i);
CString strControlName=C.GetName();

if(strControlName==lpstrItemName)
{
pUnk=C.m_lpDispatch;
break;
}

}
}

if(pUnk)
{
if (dwReturnMask & SCRIPTINFO_ITYPEINFO)
{
IProvideClassInfo* pProvideClassInfo = NULL;
hr = pUnk->QueryInterface(IID_IProvideClassInfo, (void**)&pProvideClassInfo);

// if the object supports IprovideClassInfo …
if (SUCCEEDED(hr) && pProvideClassInfo != NULL)
{
hr = pProvideClassInfo->GetClassInfo(ppTypeInfo);
}
}

if (dwReturnMask & SCRIPTINFO_IUNKNOWN)
{
pUnk->AddRef();
*ppUnknownItem=pUnk;
hr=S_OK;
}
}

return hr;
}

To enable standard keyboard handling by forms, i have implemented the
CMsForm::PreTranslateMessage function. It is alomost the same as
CFormView::PreTranslateMessage, but i had some problems with typing
in text boxes, so i had to make some alterations.


BOOL CMsFormView::PreTranslateMessage(MSG* pMsg)
{
// TODO: Add your specialized code here and/or call the base class
// allow tooltip messages to be filtered
if (CView::PreTranslateMessage(pMsg))
return TRUE;

// don’t translate dialog messages when in Shift+F1 help mode
CFrameWnd* pFrameWnd = GetTopLevelFrame();
if (pFrameWnd != NULL && pFrameWnd->m_bHelpMode)
return FALSE;

// since ‘IsDialogMessage’ will eat frame window accelerators,
// we call all frame windows’ PreTranslateMessage first
pFrameWnd = GetParentFrame(); // start with first parent frame
while (pFrameWnd != NULL)
{
// allow owner & frames to translate before IsDialogMessage does
if (pFrameWnd->PreTranslateMessage(pMsg))
return TRUE;

// try parent frames until there are no parent frames
pFrameWnd = pFrameWnd->GetParentFrame();
}

//To allow typing in text boxes
if (pMsg->message == WM_CHAR)
return FALSE;

// filter both messages to dialog and from children
return PreTranslateInput(pMsg);
}

That’s all. If you have any information concerning the ActiveX script
hosting technology or dynamic properties support, please send me a message.

Useful References :-

  1. Microsoft Support – ActiveX Scripting Download Site
  2. Visual C++ Developers’s Journal May 97 – Steve Wampler’s article on Active X Script Hosting

More by Author

Must Read