January 26, 2023, 09:43:16 PM

News:

Own IWBasic 2.x ? -----> Get your free upgrade to 3.x now.........


Extending browser control

Started by sapero, April 16, 2006, 01:40:29 PM

Previous topic - Next topic

0 Members and 1 Guest are viewing this topic.

sapero

Hello community
Since we have a hosted browser control, why not extend it?
We can add:
- support for drag-drop local files and anchors
- remove the ugly error message that is shown on any HTML script error
- extensions to scripts - document.external.blah(1,2,3,4)
- snap OnNewWindow event and open your window instead new iexplore instance
- ...

Existing browser container we can "subclass" by changing its IDocHostUIHandler class. If you like to create own container... I have it already done :)
To subclass the class we need to obtain ICustomDoc interface from IWebBrowser2 -> IDispatch and call SetUIHandler().
But first we need custom IDocHostUIHandler implementation

1. includes (incomplete alpha: http://www.sendspace.com/file/yw20il )#define __NO_WARNINGS__
#include "com.inc" // custom; temporary
#include "objidl.inc"
#include "OleIdl.inc"
#include "MsHtmHst.inc"
#include "ExDisp.inc"
#include "winerror.inc"
#include "shlobj.inc"


2. The always existing class IUnknown
class IUnknownImpl : IUnknown
{
declare IUnknownImpl();
declare virtual QueryInterface(GUID *riid, void *ppv),HRESULT;
declare virtual AddRef(),ULONG;
declare virtual Release(),ULONG;
int m_refs;
}

IUnknownImpl :: IUnknownImpl()
{
m_refs = 1; return;
}

IUnknownImpl :: QueryInterface(GUID *riid, void *ppv),HRESULT
{
if IsEqualGUID(riid, _IID_IUnknown)
{
*(pointer)ppv = this; AddRef(); return 0;
}
*(pointer)ppv = null;
return E_NOINTERFACE;
}

IUnknownImpl :: AddRef(),ULONG
{
m_refs++; return m_refs;
}
IUnknownImpl :: Release(),ULONG
{
m_refs--;
if (!m_refs) {delete this; return 0;}
return m_refs;
}


3. Our new browser handler IDocHostUIHandler - all methods are transparent
// this is only a template

class IDocHostUIHandlerImpl : IUnknownImpl
{
// IUnknown
declare virtual QueryInterface(GUID *riid, void *ppv),HRESULT;
// IDocHostUIHandler
declare virtual ShowContextMenu(DWORD dwID, POINT *ppt, IUnknown *pcmdtReserved, IDispatch *pdispReserved),HRESULT;
declare virtual GetHostInfo(DOCHOSTUIINFO *pInfo),HRESULT;
declare virtual ShowUI(DWORD dwID, IOleInPlaceActiveObject *pActiveObject,
IOleCommandTarget *pCommandTarget, IOleInPlaceFrame *pFrame, IOleInPlaceUIWindow *pDoc),HRESULT;
declare virtual HideUI(),HRESULT;
declare virtual UpdateUI(),HRESULT;
declare virtual EnableModeless(BOOL fEnable),HRESULT;
declare virtual OnDocWindowActivate(BOOL fActivate),HRESULT;
declare virtual OnFrameWindowActivate(BOOL fActivate),HRESULT;
declare virtual ResizeBorder(RECT *prcBorder, IOleInPlaceUIWindow *pUIWindow, BOOL fRameWindow),HRESULT;
declare virtual TranslateAccelerator(MSG *lpMsg, GUID *pguidCmdGroup, DWORD nCmdID),HRESULT;
declare virtual GetOptionKeyPath(LPOLESTR *pchKey, DWORD dw),HRESULT;
declare virtual GetDropTarget(IDropTarget *pDropTarget, IDropTarget *ppDropTarget),HRESULT;
declare virtual GetExternal(IDispatch *ppDispatch),HRESULT;
declare virtual TranslateUrl(DWORD dwTranslate, OLECHAR *pchURLIn, OLECHAR *ppchURLOut),HRESULT;
declare virtual FilterDataObject(IDataObject *pDO, IDataObject *ppDORet),HRESULT;
IDocHostUIHandlerImpl *original;
}

and here we go, in IDocHostUIHandlerImpl class we can add drag-drop, document.external and other extensions.
First we add drag-drop extension:
class IDropTargetImpl : IUnknownImpl
{
// IUnknown
declare virtual QueryInterface(GUID *riid, void *ppv),ULONG;
// IDropTarget
declare virtual DragEnter(IDataObject *pDataObj,DWORD grfKeyState,POINT *pt,DWORD *pdwEffect),HRESULT;
declare virtual DragOver(DWORD grfKeyState,POINT *pt,DWORD *pdwEffect),HRESULT;
declare virtual DragLeave(),HRESULT;
declare virtual Drop(IDataObject *pDataObj,DWORD grfKeyState,POINT *pt,DWORD *pdwEffect),HRESULT;
HWND m_hwnd; // send here WM_DROPFILES
}


IDropTargetImpl :: QueryInterface(GUID *riid, void *ppv),HRESULT
{
if IsEqualGUID(riid, _IID_IDropTarget)
{
*(pointer)ppv = this; AddRef(); return S_OK;
}
return IUnknownImpl!!QueryInterface(riid, ppv);
}

IDropTargetImpl :: DragEnter(IDataObject *pDataObj,DWORD grfKeyState,POINT *pt,DWORD *pdwEffect),HRESULT
{
STGMEDIUM medium;

if FindFormat(pDataObj, medium)
{
ReleaseStgMedium(medium);
return S_OK;
}
pdwEffect = DROPEFFECT_NONE;

return S_FALSE;
}

IDropTargetImpl :: DragOver(DWORD grfKeyState,POINT *pt,DWORD *pdwEffect),HRESULT
{
return S_OK;
}

IDropTargetImpl :: DragLeave(),HRESULT
{
return S_OK;
}

IDropTargetImpl :: Drop(IDataObject *pDataObj,DWORD grfKeyState,POINT *pt,DWORD *pdwEffect),HRESULT
{
LRESULT not_processed = 1;
STGMEDIUM medium;

if FindFormat(pDataObj, medium)
{
not_processed = SendMessage(m_hwnd, WM_DROPFILES, medium.hGlobal, 0);
if (not_processed) ReleaseStgMedium(medium);
}
return S_OK;
}


/////////////////////////////////////////////////
// enumerates all data types in IDataObject
/////////////////////////////////////////////////


sub FindFormat(IDataObject *pDataObj, /*out*/STGMEDIUM pmedium),BOOL
{
IEnumFORMATETC *penumFormatetc;
FORMATETC etc;
STGMEDIUM medium;

if(!pDataObj->EnumFormatEtc(DATADIR_GET, &penumFormatetc))
{
ULONG celtFetched;
while (!penumFormatetc->Next(1, etc, &celtFetched))
{
if (etc.cfFormat = CF_HDROP)
{
// a local file
if (!pDataObj->GetData(etc, pmedium)) return true;
}
dstring cf_name[128];
GetClipboardFormatName(etc.cfFormat, cf_name, 128);

if (cf_name = "UniformResourceLocator")
{
// a anchor
if (!pDataObj->GetData(etc, medium))
{
string *data = GlobalLock(medium.hGlobal);
void *hdf = GlobalAlloc(GMEM_MOVEABLE, len(*data) + 1 + len(DROPFILES));

if (hdf)
{
DROPFILES *dfÂÃ,  = GlobalLock(hdf);
if (df)
{
pmedium.tymed = TYMED_HGLOBAL;
pmedium.hGlobal = hdf|0;ÂÃ,  ÂÃ,  ÂÃ,  ÂÃ, // a bug?
pmedium.pUnkForRelease = null;
df->pFiles = len(DROPFILES);
df->fWideÂÃ,  = false;
*(string)(df+len(DROPFILES)) = *data;
GlobalUnlock(hdf);
GlobalUnlock(medium.hGlobal);
return true;
}
else GlobalFree(hdf);
}
GlobalUnlock(medium.hGlobal);
ReleaseStgMedium(medium);
}
}
}
penumFormatetc->Release();
}
return false;
}


back to IDocHostUIHandlerImpl: add initialization and pointer to drag-dropo class:
class IDocHostUIHandlerImpl : IUnknownImpl
{
declare IDocHostUIHandlerImpl();
// IUnknown
declare virtual QueryInterface(GUID *riid, void *ppv),HRESULT;
// need to release IDropTarget class
declare virtual Release(),ULONG;
// IDocHostUIHandler
declare virtual ShowContextMenu(DWORD dwID, POINT *ppt, IUnknown *pcmdtReserved, IDispatch *pdispReserved),HRESULT;
declare virtual GetHostInfo(DOCHOSTUIINFO *pInfo),HRESULT;
declare virtual ShowUI(DWORD dwID, IOleInPlaceActiveObject *pActiveObject,
IOleCommandTarget *pCommandTarget, IOleInPlaceFrame *pFrame, IOleInPlaceUIWindow *pDoc),HRESULT;
declare virtual HideUI(),HRESULT;
declare virtual UpdateUI(),HRESULT;
declare virtual EnableModeless(BOOL fEnable),HRESULT;
declare virtual OnDocWindowActivate(BOOL fActivate),HRESULT;
declare virtual OnFrameWindowActivate(BOOL fActivate),HRESULT;
declare virtual ResizeBorder(RECT *prcBorder, IOleInPlaceUIWindow *pUIWindow, BOOL fRameWindow),HRESULT;
declare virtual TranslateAccelerator(MSG *lpMsg, GUID *pguidCmdGroup, DWORD nCmdID),HRESULT;
declare virtual GetOptionKeyPath(LPOLESTR *pchKey, DWORD dw),HRESULT;
declare virtual GetDropTarget(IDropTarget *pDropTarget, IDropTarget *ppDropTarget),HRESULT;
declare virtual GetExternal(IDispatch *ppDispatch),HRESULT;
declare virtual TranslateUrl(DWORD dwTranslate, OLECHAR *pchURLIn, OLECHAR *ppchURLOut),HRESULT;
declare virtual FilterDataObject(IDataObject *pDO, IDataObject *ppDORet),HRESULT;
IDocHostUIHandlerImpl *original;
IDropTargetImpl *m_DropTargetImpl;
}

all methods but GetDropTarget() are transparent; every transparent method calls back to original bwoeser container:
IDocHostUIHandlerImpl :: IDocHostUIHandlerImpl()
{
m_DropTargetImpl = new(IDropTargetImpl, 1);
original = null;
return;
}

IDocHostUIHandlerImpl :: QueryInterface(GUID *riid, void *ppv),HRESULT
{
if IsEqualGUID(riid, _IID_IDocHostUIHandler)
{
*(pointer)ppv = this; AddRef(); return 0;
}
return IUnknownImpl!!QueryInterface(riid, ppv);
}

IDocHostUIHandlerImpl :: Release(),ULONG
{
if (m_refs = 1)
{
if (original) original->Release();
m_DropTargetImpl->Release();
}
return IUnknownImpl!!Release();
}
// transparent methods
IDocHostUIHandlerImpl :: ShowContextMenu(DWORD dwID, POINT *ppt,
IUnknown *pcmdtReserved, IDispatch *pdispReserved),HRESULT
{return original->ShowContextMenu(dwID, ppt, pcmdtReserved, pdispReserved);}
IDocHostUIHandlerImpl :: GetHostInfo(DOCHOSTUIINFO *pInfo),HRESULT
{return original->GetHostInfo(pInfo);}
IDocHostUIHandlerImpl :: ShowUI(DWORD dwID, IOleInPlaceActiveObject *pActiveObject,
IOleCommandTarget *pCommandTarget, IOleInPlaceFrame *pFrame, IOleInPlaceUIWindow *pDoc),HRESULT
{return original->ShowUI(dwID,pActiveObject,pCommandTarget,pFrame,pDoc);}
IDocHostUIHandlerImpl :: HideUI(),HRESULT
{return original->HideUI();}
IDocHostUIHandlerImpl :: UpdateUI(),HRESULT
{return original->UpdateUI();}
IDocHostUIHandlerImpl :: EnableModeless(BOOL fEnable),HRESULT
{return original->EnableModeless(fEnable);}
IDocHostUIHandlerImpl :: OnDocWindowActivate(BOOL fActivate),HRESULT
{return original->OnDocWindowActivate(fActivate);}
IDocHostUIHandlerImpl :: OnFrameWindowActivate(BOOL fActivate),HRESULT
{return original->OnFrameWindowActivate(fActivate);}
IDocHostUIHandlerImpl :: ResizeBorder(RECT *prcBorder, IOleInPlaceUIWindow *pUIWindow, BOOL fRameWindow),HRESULT
{return original->ResizeBorder(prcBorder,pUIWindow,fRameWindow);}
IDocHostUIHandlerImpl :: TranslateAccelerator(MSG *lpMsg, GUID *pguidCmdGroup, DWORD nCmdID),HRESULT
{return original->TranslateAccelerator(lpMsg,pguidCmdGroup,nCmdID);}
IDocHostUIHandlerImpl :: GetOptionKeyPath(LPOLESTR *pchKey, DWORD dw),HRESULT
{return original->GetOptionKeyPath(pchKey,dw);}
IDocHostUIHandlerImpl :: GetExternal(IDispatch *ppDispatch),HRESULT
{return original->GetExternal(ppDispatch);}
IDocHostUIHandlerImpl :: TranslateUrl(DWORD dwTranslate, OLECHAR *pchURLIn, OLECHAR *ppchURLOut),HRESULT
{return original->TranslateUrl(dwTranslate,pchURLIn,ppchURLOut);}
IDocHostUIHandlerImpl :: FilterDataObject(IDataObject *pDO, IDataObject *ppDORet),HRESULT
{return original->FilterDataObject(pDO,ppDORet);}

// nontransparent
IDocHostUIHandlerImpl :: GetDropTarget(IDropTarget *pDropTarget, IDropTarget *ppDropTarget),HRESULT
{
*(pointer)ppDropTarget = m_DropTargetImpl;
return S_OK;
}


let's install the drag-drop extension!
// data stored in GetWindowLong(hwnd, GWL_USERDATA) (browser.c)
// or property "BROWSER"
struct BROWSERCONTAINERDATA {
IUnknown *browserObject; // _IID_IWebBrowser2 + _IID_IOleObject
// IOleClientSite jumptable (_iOleClientSiteEx struct)
// ... more
}


// hwndBrowser - CWebBrowser.m_hwnd
// hwndParent - handle to window where to send WM_DROPFILES

global sub EnableBrowserDrop(HWND hwndBrowser, HWND hwndParent),BOOL
{
BOOL bRet = false;
// found a "BROWSER" string while playing with EnumProps() api :)
BROWSERCONTAINERDATA *cd = GetProp(hwndBrowser, "BROWSER");
if (cd)
{
IWebBrowser2 *unk;

// to replace IDocHostUIHandler we need ICustomDoc class from browser
// ICustomDoc is implemented by MSHTML, not by browser
// IWebBrowser2 -> IDispatch -> ICustomDoc -> SetUIHandler(new(IDocHostUIHandlerImpl))

// check if we have IWebBrowser2 object here
if (!cd->browserObject->QueryInterface(_IID_IWebBrowser2, &unk))
{
// yes! go to document->ICustomDoc
IDispatch *dispdoc = null;
if (!unk->get_Document(&dispdoc) and (dispdoc))
{
ICustomDoc *customdoc;
if (!dispdoc->QueryInterface(_IID_ICustomDoc, &customdoc))
{
// create new IDocHostUIHandler object
IDocHostUIHandlerImpl *ui = new(IDocHostUIHandlerImpl, 1);
if (ui)
{
ui->m_DropTargetImpl->m_hwnd = hwndParent;

// we need the original IDocHostUIHandler from aurora browser container
// we have pointer to IOleClientSite jumptable
// to call a method we need an object, not only jumptable
// also we create a faked THIS (pointer to pointer to jumptable)
IUnknown *faked_this = cd+4; // IOleClientSite*

// request the original IDocHostUIHandler (and add a reference to it)
if (!faked_this->QueryInterface(_IID_IDocHostUIHandler, &ui->original))
{
// got it! Now replace the original IDocHostUIHandler
if (!customdoc->SetUIHandler(ui)) bRet = true;
}
// if something failed delete new class
if (!bRet) ui->Release();
}
customdoc->Release();
}
dispdoc->Release();
}
unk->Release();
}
}

return bRet;
}


that's all for dropping files or anchors into browser window. Now we need to handle the WM_DROPFILES message:
(modified browser_test example)
#include "winapi.inc"
#include "shellapi.inc"
#include "webbrowser.inc"

class MyBrowser : CWebBrowser
{
declare MyBrowser();
declare virtual WndProc(unsigned int message, unsigned int wparam, unsigned int lparam),int;
declare virtual OnDropFile(string path);
declare virtual OnNavComplete(string url),int;

BOOL bExtensionInstalled;
BOOL bBlankDone;
}


MyBrowser :: MyBrowser()
{
// initialize variables
bExtensionInstalled = false;
bBlankDone = false;
return;
}


MyBrowser :: WndProc(unsigned int message, unsigned int wparam, unsigned int lparam),int
{
// handle WM_DROPFILES message
if (message = WM_DROPFILES)
{
dstring path[MAX_PATH];
int nFiles = DragQueryFile(wparam, -1, null, 0);
for (int nFile=0; nFile<nFiles; nFile++)
{
if DragQueryFile(wparam, nFile, path, MAX_PATH) OnDropFile(path);
}
DragFinish(wparam);
return 0;
}
return CWebBrowser!!WndProc(message,wparam,lparam);
}


MyBrowser :: OnDropFile(string path)
{
Stop();
Navigate(path);
return;
}


MyBrowser :: OnNavComplete(string url),int
{
bBlankDone = true;
// install drag-drop extension. Browser has no parent
if (!bExtensionInstalled) bExtensionInstalled = EnableBrowserDrop(m_hwnd, m_hwnd);
return true;
}



global sub main()
{
MyBrowser browse;
browse.Create(0,0,500,500,AWS_VISIBLE|AWS_SIZE|AWS_SYSMENU|AWS_MINIMIZEBOX|AWS_MAXIMIZEBOX,0,"Browser Test",0);
// create browser full instance
browse.Navigate("about:blank");
// wait for blank page and install extension
while(!browse.bBlankDone) {browserwait();}

browse.Navigate("http://www.ionicwind.com");
do{browserwait(); }until browse.m_hwnd = 0;
}


Compile and run, then try to drop a file onto browser window.
In another browser open any page and drad a anchor (link) to aurora browser window.
I don't know why, but drag-drop does not work if you do it from same process (source+target). Maybe another thread will help, not tried.

Coming soon - kill error messages

sapero

next extension: document.external

As a standard in ole automation, we need an IDispatch implementation. Also do create a universal dispatch class:
class IDispatchImpl : IUnknownImpl
{
declare virtual QueryInterface(REFIID *riid, void *ppv),HRESULT;

declare virtual GetTypeInfoCount(UINT *pctinfo),HRESULT;
declare virtual GetTypeInfo(UINT iTInfo, LCID lcid, ITypeInfo *ppTInfo),HRESULT;
declare virtual GetIDsOfNames(REFIID *riid, LPOLESTR *rgszNames, UINT cNames, LCID lcid, DISPID *rgDispId),HRESULT;
declare virtual Invoke(DISPID dispIdMember, REFIID *riid, LCID lcid, unsigned word wFlags,
DISPPARAMS *pDispParams, VARIANT *pVarResult, EXCEPINFO *pExcepInfo, UINT *puArgErr),HRESULT;
}

IDispatchImpl :: QueryInterface(REFIID *riid, void *ppv),HRESULT
{
if IsEqualGUID(riid, _IID_IDispatch)
{
*(pointer)ppv = this;
AddRef();
return S_OK;
}
return IUnknownImpl!!QueryInterface(riid, ppv);
}

IDispatchImpl :: GetTypeInfoCount(UINT *pctinfo),HRESULT
{
*pctinfo = 0;
return S_OK;
}

IDispatchImpl :: GetTypeInfo(UINT iTInfo,LCID lcid,ITypeInfo *ppTInfo),HRESULT
{
return E_NOTIMPL;
}

IDispatchImpl :: GetIDsOfNames(REFIID *riid,LPOLESTR *rgszNames, UINT cNames,LCID lcid,DISPID *rgDispId),HRESULT
{
return E_NOTIMPL;
}

IDispatchImpl :: Invoke(DISPID dispIdMember, REFIID *riid, LCID lcid, unsigned word wFlags,
DISPPARAMS *pDispParams, VARIANT *pVarResult, EXCEPINFO *pExcepInfo, UINT *puArgErr),HRESULT
{
return E_NOTIMPL;
}


To handle HTML external calls we must implement GetIDsOfNames() and Invoke() methods:
class CExternal : IDispatchImpl
{
declare virtual GetIDsOfNames(REFIID *riid, LPOLESTR *rgszNames, UINT cNames, LCID lcid, DISPID *rgDispId),HRESULT;
declare virtual Invoke(DISPID dispIdMember, REFIID *riid, LCID lcid, unsigned word wFlags,DISPPARAMS *pDispParams, VARIANT *pVarResult, EXCEPINFO *pExcepInfo, UINT *puArgErr),HRESULT;
}

The first is called every time the html parser parses method name; we should return a ID for given method if it is supported.
In this example we accept all methods:
CExternal :: GetIDsOfNames(REFIID *riid, LPOLESTR *rgszNames, UINT cNames, LCID lcid, DISPID *rgDispId),HRESULT
{
for(int a=0; a<cNames; a++)
{
print("GetIDsOfNames: - ", w2a(*(pointer)rgszNames[a])); // method name
*(int)rgDispId[a] = 0x1000+a; // id for this method
}
return S_OK;
}

w2a() converts unicode string to ansi (unicode_a2w_w2a.inc)
rgszNames - pointer to array of string pointers, here it is one pointer to method name from html document.
cNames - number of string pointers in rgszNames array, here always one.
rgDispId - (output) pointer to integer array where we should put id's for strings from rgszNames array.
You can always accept only some names..
if w2a(*(pointer)rgszNames[a])) = "SayHello"
   *(int)rgDispId[a] = dispid_SayHello

if one or more of the names you do not support, set *(int)rgDispId[a] to DISPID_UNKNOWN and return DISP_E_UNKNOWNNAME.

execute method:
CExternal :: Invoke(DISPID dispIdMember, REFIID *riid, LCID lcid, unsigned word wFlags,
DISPPARAMS *pDispParams, VARIANT *pVarResult, EXCEPINFO *pExcepInfo, UINT *puArgErr),HRESULT
{
MessageBox(0, using("CExternal :: Invoke() with # argument(s)", pDispParams->cArgs), "");
// parse arguments backwards
int idx = 0;
int cnt = pDispParams->cArgs-1;
for (int a=cnt; a>=0; a--)
{
idx++;
VARIANT *vArg = *pDispParams.*(VARIANT)rgvarg[a];
VARIANT vLocal;
vLocal.vt = VT_EMPTY; // faster than VariantInit()
// Change argument type to string
if (!VariantChangeType(vLocal, vArg, 0, VT_BSTR))
{
string *s = "st"; if (idx=2) s = "nd";
if (idx=3) s = "rd"; if (idx>3) s = "th";
MessageBox(0, using("the #& argument is &", idx, *s, w2a(vLocal.bstrVal)), "");
VariantClear(vLocal);
}
}
return S_OK;
}


dispIdMember - id of method that is executed, it was assigned in GetIDsOfNames
wFlags - can be one of DISPATCH_METHOD, DISPATCH_PROPERTYGET, DISPATCH_PROPERTYPUT or DISPATCH_PROPERTYPUTREF.
calling <SCRIPT>
external.blah(1,2,3,4,5);
</SCRIPT>
it will be always DISPATCH_METHOD
pDispParams - pointer to DISPPARAMS structure, where you can find how many arguments was passed to current method, and the arguments located in array of VARIANT structures. A VARIANT is universal data type where you have value and type of value. It is mostly used in js and vb, where variable declaration without type (Dim x) defines the variable as variant (anytype). It can be a string, integer, array, interface...
Note: the first argument pDispParams->rgvarg[0] is the rightmost argument from function


Ok, time to install:
Change one method in IDocHostUIHandlerImpl
IDocHostUIHandlerImpl :: GetExternal(IDispatch *ppDispatch),HRESULT
{
pointer x = new(CExternal, 1);
*(pointer)ppDispatch = x;
if x return S_OK else return E_OUTOFMEMORY;

}


Create a html document and navigate to it:<HTML><BODY>

<SCRIPT>
external.blah("hello ole!", 1,2,3,4,5, "bye bye");
</SCRIPT>

</BODY></HTML>


if you want to return a value... and pVarResult is not null
pVarResult->vt = VT_I4; // return a integer
pVarResult->intVal = ... // your value

// returning a string

pVarResult->vt = VT_BSTR;
pVarResult->bstrVal = SysAllocString(a2w("any string here"));

// float: VT_R4 / fltVal
// double: VT_R8 / dblVal

Jerry Muelver

Oh-oh. Now I'm going to have to buy Aurora to see what this is all about. A little extended Browser stuff, the new associative array stuff, and pretty soon we're talking about an ebook/help authoring and viewer system....  ::)