Microsoft Live Excel Application:
Microsoft Live Calendar Application:
Yahoo! Mail Application:
Contents
With the advent of modern sophisticated web applications, web browser became one of the most frequently used applications on our desktop computers. However, most of the modern browsers still maintain the traditional browser look: address bar, navigation buttons, web page tabs, search box, etc. None of these GUI elements are required while using many advanced web applications which replace the existing desktop counterparts. Typically, a user never navigates back and forth with navigation buttons, never uses the browser's main menu, and would prefer to have a separate window for an application. In short, most users want to have web applications as similar to their desktop big brethren as possible.
Unfortunately, currently only one browser somewhat supports a web application mimicking a desktop application: it is the Google Chrome browser. With Google Chrome, a user can take an arbitrary URL and create a Windows shortcut, which can be placed on Desktop, Start menu, Quick Launch bar, etc. Launching the shortcut will open a new streamlined browser window without the usual browser GUI elements. Here is the description of the Chrome Application shortcuts: Tabs and windows: Application shortcuts. The single drawback of the Chrome application is that clicking on links within an application will open them in a Google Chrome browser exclusively. A true desktop application should open web links in the default browser, whatever it is.
Microsoft Windows provides a wonderful undeservedly little-known technology: HTML Application, or HTA. HTA allows to run a regular HTML page with the security rights of a full fledged desktop application. With the help of HTA, it is possible to create desktop applications as cheap as creating web pages and writing JavaScript. No binary code or compilation is required. Here is an example of an HTML editor written solely with HTML and script: How to Create an HTML Editor Application.
In order to make HTA the host of an arbitrary URL, we need to create an HTML page with a single IFRAME
element within it, which occupies all the available space on the page. In order to prevent a frame's content accessing an outside system, we need to specify an HTA-specific attribute: application="no"
for the IFRAME
element. Here is the HTML part of the WebApp HTA (the script part is omitted for clarity):
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html>
<head>
<style>
html, body
{
height: 100%
}
</style>
<title>Web Application</title>
<hta:application
id="oHTA"
icon="http://www.microsoft.com/windows/Framework/images/favicon.ico"
innerBorder="no"
scroll="no"
/>
<script language="vbscript" type="text/vbscript">
...
</script>
</head>
<body leftmargin="0" topmargin="0"
rightmargin="0" bottommargin="0">
<iframe
id="content"
height="100%"
width="100%"
frameborder="0"
application="no"
/>
</body>
</html>
The full source code of the HTA WebApp can be found at the link above.
The HTA web application provides a sufficient solution for the majority of users. However, it has several disadvantages, as well:
- A new window is always opened in the new standalone Internet Explorer browser.
- It is impossible to change the application icon at runtime.
- It is impossible to catch title changes of the contents frame, so we need to poll the title from an outer document script.
Here is a summary of existing solutions, including the WebApp native client, for comparison:
|
Google Chrome |
HTA |
WebApp |
New Window |
Opened in an existing instance of Google Chrome browser. |
Opened in a new instance of Internet Explorer browser even if already running instance exists. |
Opened in the default web browser according to the user's settings (new tab in existing instance, or new process). |
Window Icon |
Site's favicon is used as a window icon automatically. |
Icon cannot be changed at runtime. Site's favicon can be used if specified explicitly in the HTA's source. |
Site's favicon is used as a window icon automatically. |
Title Changes |
Title changes immediately. |
Title must be polled from the script. |
Title changes immediately. |
Status bar |
No status bar. |
No status bar. |
Status bar can be switched on and off. |
In order to acquire full control over the WebBrowser control, we need to host it ourselves. ATL framework is the natural choice for COM-intensive code. That's why the WebApp project uses ATL as a helping tool.
ATL hosting of ActiveX controls is implemented primarily in two main classes: CAxWindow
and CAxHostWindow
. CAxWindow
is what a developer should be using directly in his/her code. CAxHostWindow
is an undocumented class, which implements the gory details of the ActiveX host. The CAxHostWindow
instance is created behind the scenes by the CAxWindow
class. The user can control the CAxHostWindow
instance via the IAxWinHostWindow
interface, which is obtainable through a CAxWindow::QueryHost
call. Here is the execution flow of the hosted ActiveX control creation:
CAxWindow::CAxWindow()
constructor runs
AtlAxWinInit()
function is called
- Two special window messages are registered:
WM_ATLGETHOST
, WM_ATLGETCONTROL
- Special window classes are registered:
AtlAxWinNN
, AtlAxWinLicNN
, where NN is the ATL version number. For example, AtlAxWin90
. The window procedure for these classes is: AtlAxWindowProc
.
- User calls the
CAxWindow::Create
function
- New window is created
- Within the
WM_CREATE
handler of the AtlAxWindowProc
procedure:
- New
CAxHostWindow
COM object is created
CAxHostWindow
object subclasses a newly created window
CAxHostWindow
creates a hosted ActiveX control instance
CAxHostWindow
sets itself as an OLE site for the hosted control
From now on, the user can access the hosted ActiveX control via a CAxWindow::QueryControl
call.
The default ATL hosting implementation would be OK if there was no, one little problem: the WebBrowser control calls the host's IOleInPlaceSite::OnPosRectChange
method with incorrect parameters. Suppose a webpage has the following script:
window.open("page.htm", null, "left=150,top=150,height=500,width=400");
Now, these window coordinates are not the same. Left and Top values are in screen coordinates, while Height and Width values are the size of the client area of the new window. Apparently, the WebBrowser control doesn't care about the difference and passes the new coordinates as is to its host. ATL's default implementation of the IOleInPlaceSite::OnPosRectChange
method is as follows:
STDMETHOD(OnPosRectChange)(LPCRECT lprcPosRect)
{
ATLTRACE2(atlTraceHosting, 0, _T("IOleInPlaceSite::OnPosRectChange"));
if (lprcPosRect==NULL) { return E_POINTER; }
RECT rect = *lprcPosRect;
ClientToScreen( &rect );
HWND hWnd = GetParent();
if(hWnd != NULL)
{
CWindow wndParent(hWnd);
wndParent.ScreenToClient(&rect);
wndParent.Detach ();
}
MoveWindow( &rect);
return S_OK;
}
The above implementation is good for most of the cases. However, in our case, the code doesn't take into account the difference between the coordinates, and therefore moves the WebBrowser control window within its parent window instead of moving the parent window itself and adjusting the parent's client area.
How can we override the IOleInPlaceSite::OnPosRectChange
method without changing ATL's source files? The answer is: by aggregating the CAxHostWindow
control and by providing our own implementation of the IOleInPlaceSite
interface. But we cannot aggregate the CAxHostWindow
control yet, since it is created deep inside the ATL implementation of the AtlAxWindowProc
procedure. So, we need to create a hosting window and the CAxHostWindow
object instance separately, and explicitly provide our own host object as controlling the outer one.
The CBrowserHost
class that maintains a hosting window for the WebBrowser control using the same approach as the CAxHostWindow
class: instead of being a regular C++ class, CBrowserHost
is a COM object, which implements its own COM interface for internal use. First of all, we declare the IBrowserHost
COM interface:
struct IBrowserHost : public IUnknown
{
public:
virtual HRESULT STDMETHODCALLTYPE CreateHostWindow(
HWND hwndParent,
int nShowCmd,
bool bMainWindow) = 0;
virtual HRESULT STDMETHODCALLTYPE GetWebBrowser(
SHDocVw::IWebBrowser2** ppWebBrowser) = 0;
virtual HRESULT STDMETHODCALLTYPE SetUrl(
LPCWSTR pwszUrl) = 0;
virtual HRESULT STDMETHODCALLTYPE PreTranslateMessage(
const MSG& msg) = 0;
};
Then, we make a static CBrowserHost::Create
function to create object instances. First, this function creates a new instance of the BrowserHost
COM control, then uses the IBrowserHost::CreateHostWindow
method to create a hosting window and a WebBrowser control. Here is the execution flow of the BrowserHost
creation:
- The user calls the
CBrowserHost::Create
static method.
- A new overlapped window is created.
CBrowserHost::_CreatorClass::CreateInstance
is called and a new instance of BrowserHost
is created. This instance implements a window procedure for the overlapped window.
- Within the
CBrowserHost::FinalConstruct
method, we create an aggregated CAxHostWindow
object.
IBrowserHost::CreateHostWindow
method is called.
- A new child window is created. This child window will host a WebBrowser control. The
CAxHostWindow
object will subclass this window.
IAxWinHostWindow::CreateControl("Shell.Explorer", ...)
is called. The WebBrowser instance is created. The child window is subclassed.
Here are the relevant parts of the code:
CBrowserHostPtr CBrowserHost::Create(
HWND hwndParent,
int nShowCmd,
bool bMainWindow)
{
CComPtr<IUnknown> ptrUnk;
HRESULT hr = CBrowserHost::_CreatorClass::CreateInstance(
NULL, IID_PPV_ARGS(&ptrUnk));
ATLASSERT(SUCCEEDED(hr));
CBrowserHostPtr ptrBrwHost = ptrUnk;
if(ptrBrwHost)
{
hr = ptrBrwHost->CreateHostWindow(hwndParent, nShowCmd, bMainWindow);
ATLASSERT(SUCCEEDED(hr));
if(FAILED(hr)) ptrBrwHost.Release();
}
return ptrBrwHost;
}
HRESULT CBrowserHost::FinalConstruct()
{
ATLVERIFY(InitSettings());
HRESULT hr = CAxHostWindow::_CreatorClass::CreateInstance(
static_cast<IBrowserHost*>(this), IID_PPV_ARGS(&m_ptrUnkInner));
ATLASSERT(SUCCEEDED(hr));
if(SUCCEEDED(hr))
m_ptrOleInPlaceSite = m_ptrUnkInner;
return hr;
}
STDMETHODIMP CBrowserHost::CreateHostWindow(
HWND hwndParent,
int nShowCmd,
bool bMainWindow)
{
m_bMainWindow = bMainWindow;
HRESULT hr = CreateWebBrowser(hwndParent);
if(FAILED(hr))
return hr;
ShowWindow(nShowCmd);
return hr;
}
HRESULT CBrowserHost::CreateWebBrowser(HWND hwndParent)
{
HRESULT hr = E_FAIL;
CComQIPtr<IAxWinHostWindow> ptrWinHost = m_ptrUnkInner;
if(ptrWinHost)
{
hr = ptrWinHost->CreateControl(L"Shell.Explorer", m_webBrowser, NULL);
ATLASSERT(SUCCEEDED(hr));
}
if(SUCCEEDED(hr))
hr = HookUpWebBrowser();
return hr;
}
Note: The AtlAxWindowProc
procedure calls OleInitialize
on the WM_CREATE
message, and OleUninitialize
on the WM_NCDESTROY
message. It is crucial to call OleInitialize
instead of CoInitialize[Ex]
in order to enable drag and drop, clipboard, and other OLE related operations. To ensure that OleInitialize
is the first COM call made, we need to override the default implementation of the CAtlExeModuleT::InitializeCom
method:
class CWebAppModule : public CAtlExeModuleT<CWebAppModule>
{
public:
static HRESULT InitializeCom() throw()
{
return ::OleInitialize(NULL);
}
static void UninitializeCom() throw()
{
::OleUninitialize();
}
};
The single interface we're interested in is IOleInPlaceSite
. This interface is implemented by the BrowserHost
object. And the single method of this interface that requires special handling is IOleInPlaceSite::OnPosRectChange
. Calls to all other methods are forwarded to the implementation provided by the CAxHostWindow
object.
class ATL_NO_VTABLE CBrowserHost :
public CComObjectRoot,
public CComCoClass<CBrowserHost, &CLSID_NULL>,
public IOleInPlaceSite,
public IBrowserHost,
...
{
DECLARE_NO_REGISTRY();
BEGIN_COM_MAP(CBrowserHost)
COM_INTERFACE_ENTRY(IBrowserHost)
COM_INTERFACE_ENTRY(IOleInPlaceSite)
COM_INTERFACE_ENTRY_AGGREGATE_BLIND(m_ptrUnkInner)
END_COM_MAP()
};
The COM_INTERFACE_ENTRY_AGGREGATE_BLIND
macro redirects all interface queries to the inner CAxHostWindow
object.
CBrowserHost
class' implementation of the IOleInPlaceSite::OnPosRectChange
method leaves it empty, and always returns a E_NOTIMPL
code. The actual resizing of the web browser window happens in response to the following web browser events:
DWebBrowserEvents2::WindowSetLeft
DWebBrowserEvents2::WindowSetTop
DWebBrowserEvents2::WindowSetWidth
DWebBrowserEvents2::WindowSetHeight
The traditional way to respond to new window requests from the WebBrowser control is to handle the DWebBrowserEvents2::NewWindow3
(or the NewWindow2
) browser event:
void DWebBrowserEvents2::NewWindow2(
IDispatch **ppDisp,
VARIANT_BOOL *Cancel
);
void DWebBrowserEvents2::NewWindow3(
IDispatch **ppDisp,
VARIANT_BOOL *Cancel,
DWORD dwFlags,
BSTR bstrUrlContext,
BSTR bstrUrl
);
An application has a chance to provide a new instance of the WebBrowser control for a new window or to cancel a new window altogether. Most of the hosting applications let the control to create a new window as it sees fit. WebApp is different in that respect. We need to implement the following logic:
- If a new window logically belongs to the currently opened web application, then create a new instance of the WebBrowser control in our process and use it for the new window. This way, the new browser window will share a security context with the main application window.
- If a new window doesn't belong to the web application, then open it in the default browser according to the user's settings: a new window or a new tab. The
ShellExecute[Ex]
function does exactly that.
Handling the first case is easy. We just create a new instance of our BrowserHost
object, query the IDispatch
interface from it, and pass it along to the WebBrowser control to use.
The second case is more tricky. If we call the ShellExecute[Ex]
function with a specified URL, then we need to cancel the new window request for the hosted WebBrowser control, so the new window won't be opened twice. However, some web applications detect this and show a misleading message box that asks the user to disable the pop-up blocker software. The message is incorrect since the new window is opened in the default browser and not actually blocked. So, handling the new window request in the DWebBrowserEvents2::NewWindow3/2
event is too late.
Luckily, the hosting application can instruct the WebBrowser control about how to create a new window by implementing the INewWindowManager
interface. This interface has a single method: INewWindowManager::EvaluateNewWindow
. So, the actual decision whether to open a new window as a part of our process is made in this method:
STDMETHODIMP CBrowserHost::EvaluateNewWindow(
LPCWSTR pszUrl,
LPCWSTR pszName,
LPCWSTR pszUrlContext,
LPCWSTR pszFeatures,
BOOL fReplace,
DWORD dwFlags,
DWORD )
{
HRESULT hr = E_FAIL; if(::OpenAsExternal(dwFlags))
{
hr = S_FALSE;
const int res = (int)::ShellExecuteW(m_hWnd, L"open",
pszUrl, NULL, NULL, SW_SHOWNORMAL);
ATLASSERT(res > 32);
}
return hr;
}
bool OpenAsExternal(DWORD dwFlags)
{
const DWORD dwOwnWndFlags =
NWMF_SHOWHELP |
NWMF_HTMLDIALOG |
NWMF_SUGGESTWINDOW;
return (dwFlags & dwOwnWndFlags) == 0;
}
The OpenAsExternal
function answers to the question whether a new window should belong to the hosting application or to an external browser. By trial and error, I discovered that the above combination of NWMF
flags produces the most adequate results.
The INewWindowManager
interface must be exposed via the SID_SNewWindowManager
service. It means that our hosting object must implement the IServiceProvider
interface:
class ATL_NO_VTABLE CBrowserHost :
public INewWindowManager,
public IServiceProviderImpl<CBrowserHost>,
...
{
BEGIN_SERVICE_MAP(CBrowserHost)
SERVICE_ENTRY(SID_SNewWindowManager)
END_SERVICE_MAP()
};
The IServiceProviderImpl
implementation and service map are provided by ATL.
In order to enable keyboard navigation between controls on a web page, we need to handle special keys like Tab, PgUp, PgDown, Space etc. This is done by calling the IOleInPlaceActiveObject::TranslateAccelerator
method of the WebBrowser control. If the control decides to translate a message, then it returns S_OK
from this method, and S_FALSE
, otherwise. We don't need to query the IOleInPlaceActiveObject
interface from the WebBrowser control, the CAxHostWindow
object already does the job. All we need to do is to forward the current message to the CAxHostWindow
object by sending an ATL-specific message, WM_FORWARDMSG
:
STDMETHODIMP CBrowserHost::PreTranslateMessage(
const MSG& msg)
{
if((msg.message < WM_KEYFIRST || msg.message > WM_KEYLAST) &&
(msg.message < WM_MOUSEFIRST || msg.message > WM_MOUSELAST))
{
return S_FALSE;
}
if(!m_webBrowser.IsWindow())
return S_FALSE;
const LRESULT lTranslated = m_webBrowser.SendMessage(WM_FORWARDMSG, 0,
reinterpret_cast<LPARAM>(&msg));
return (lTranslated ? S_OK : S_FALSE);
}
LRESULT CAxHostWindow::OnForwardMsg(
UINT ,
WPARAM ,
LPARAM lParam,
BOOL& )
{
ATLASSERT(lParam != 0);
LPMSG lpMsg = (LPMSG)lParam;
CComQIPtr<IOleInPlaceActiveObject> spInPlaceActiveObject(m_spUnknown);
if(spInPlaceActiveObject)
{
if(spInPlaceActiveObject->TranslateAccelerator(lpMsg) == S_OK)
return 1;
}
return 0;
}
The CBrowserHost::PreTranslateMessage
method is called within the message loop for the received window messages.
Looking back at the work done, the following conclusions can be drawn:
- Aggregating the
CAxHostWindow
object wasn't probably the optimal decision. Separate creation of the hosting window and the control itself made the surrounding code quite complex. This complexity exists solely for the sake of reuse of the CAxWindow
class, which otherwise would have been rewritten to better suit application needs.
- The better solution could be: inheriting from the
CAxHostWindow
class in order to override the relevant COM interfaces implementation without involving aggregation.
- The ATL framework, being a great tool for COM development, nevertheless, is extremely inflexible sometimes. The ActiveX hosting machinery is buried deep in the ATL internal code, scarcely documented, and requires considerable amount of work to customize.
- The Common Controls library is buggy and under-documented, for the last 15 years.
That's all.
- 12th October, 2009 - Bug fixed
- Script error dialog box is not shown anymore
- 13th October, 2009 - Bug fixed
IOleCommandTarget
interface is implemented properly
- Script errors are not shown
- Progress notifications handled correctly, so page doesn't appear as infinitely loading anymore