Why pay money for a popup window blocker when it's so easy to roll your own? Popup Blocker is implemented in ATL as a browser helper object (BHO). A BHO is a DLL that will attach itself to every new instance of Internet Explorer. In the BHO you can intercept Internet Explorer events, and access the browser window and document object model (DOM). This gives you a great deal of flexibility in modifying Internet Explorer behavior.
There are several advantages to using a BHO over an application that continually scans open windows for keywords in the title. The BHO is event driven and does not run in a loop or use a timer, so it doesn't use any CPU cycles if nothing is happening. The BHO will prevent popups from opening and downloading their content, so you save bandwidth. The BHO can be written so as to eliminate the need for a keyword list or blacklist.
Also included, but not discussed in this article, is a Windows Installer setup program.
Creating the BHO
A minimal BHO is a COM server DLL that implements
IObjectWithSite. Create a new ATL Project and accept the default settings (attributed, DLL). Add an ATL Simple Object, give it a name and on the Options tab select Aggregation: No and Support:
The only method on
IObjectWithSite that must be implemented is
SetSite(). IE will call
SetSite with a pointer to
IUnknown which we can query for a pointer to
IWebBrowser2, which gives us the keys to the house.
STDMETHODIMP CPub::SetSite(IUnknown *pUnkSite)
ATLTRACE(_T("SetSite(): pUnkSite is NULL\n"));
m_spWebBrowser2 = pUnkSite;
HRESULT hr = ManageBrowserConnection(ConnType_Advise);
ATLTRACE(_T("Failure sinking events from IWebBrowser2\n"));
ATLTRACE(_T("QI for IWebBrowser2 failed\n"));
Once we have the pointer to
IWebBrowser2 we can hook up to the
DWebBrowserEvents2 connection point in order to receive the
NewWindow2 event. This event is sent every time a new window is about to open and allows you to cancel the operation -- exactly what we want for a popup window blocker.
HRESULT CPub::ManageBrowserConnection(ConnectType eConnectType)
if (eConnectType == ConnType_Unadvise && m_dwBrowserCookie == 0)
CComQIPtr<ICONNECTIONPOINTCONTAINER &IID_IConnectionPointContainer,> spCPC(m_spWebBrowser2);
HRESULT hr = E_FAIL;
hr = spCPC->FindConnectionPoint(DIID_DWebBrowserEvents2, &spCP);
if (eConnectType == ConnType_Advise)
ATLASSERT(m_dwBrowserCookie == 0);
hr = spCP->Advise((IDispatch*)this, &m_dwBrowserCookie);
hr = spCP->Unadvise(m_dwBrowserCookie);
m_dwBrowserCookie = 0;
To create the event handler we have to derive our class from
class ATL_NO_VTABLE CPub :
public IDispatchImpl<IPUB IPub &__uuidofIPub),(>
Then add the Invoke method as follows:
STDMETHODIMP CPub::Invoke(DISPID dispidMember, REFIID riid, LCID lcid,
WORD wFlags, DISPPARAMS* pDispParams, VARIANT* pvarResult,
EXCEPINFO* pExcepInfo, UINT* puArgErr)
pDispParams->rgvarg.pvarVal->vt = VT_BOOL;
pDispParams->rgvarg.pvarVal->boolVal = VARIANT_TRUE;
Once you get this much to compile, to make IE load the BHO you need to add the CLSID to the registry.
'Browser Helper Objects'
If you've done everything right up to now, the BHO will load with every instance of IE and prevent all popup windows from opening. You can quickly test this by launching IE and selecting Open in New Window from the context menu. If the BHO is working properly the command should fail.
One problem you may notice is that even with no open browser windows the linker will sometimes fail because some process is using the BHO, forcing you to reboot the computer to release it. Needless to say, this can quickly become annoying. What is happening is that Windows Explorer also uses IE for its GUI, and so also loads the BHO. Since we want our BHO to load only when we launch IE, we need to make a change to
DllMain so that it doesn't get loaded by Windows Explorer. This should eliminate the linker problem.
BOOL WINAPI DllMain(DWORD dwReason, LPVOID lpReserved)
if (dwReason == DLL_PROCESS_ATTACH)
GetModuleFileName(NULL, pszLoader, MAX_PATH);
CString sLoader = pszLoader;
if (sLoader.Find(_T("explorer.exe")) >= 0)
g_hinstPub = _AtlBaseModule.m_hInst;
return __super::DllMain(dwReason, lpReserved);
So now that we have our basic BHO, it would be nice to be able to exert some kind of control over it. For starters, we need to be able to enable or disable it. Also, it would be nice if Open in New Window from the context menu worked normally. There are probably a number of ways to do this, but I chose to add my own menu to the normal IE context menu. To access the IE context menu we need to derive from the
IDocHostUIHandler interface and implement the
class ATL_NO_VTABLE CPub :
public IDispatchImpl<IPUB IPub &__uuidofIPub),(>,
HRESULT CPub::ShowContextMenu(DWORD dwID,
In order to get IE to call our
ShowContextMenu we use the
ICustomDoc::SetUIHandler method. Typically, this is done when we get the NavigateComplete2 event because at that point the document has been created and we can access the browsers
IHTMLDocument2 interface. However, because we need to get the default handlers for
IOleCommandTarget (more on that later), instead we will do it in
DocumentComplete after the entire document has been loaded. So we add a case to our Invoke method:
m_bBlockNewWindow = TRUE;
m_pWBDisp = pDispParams->rgvarg.pdispVal;
if (m_pWBDisp &&
m_pWBDisp == pDispParams->rgvarg.pdispVal)
ATLTRACE(_T("(%ld) DISPID_DOCUMENTCOMPLETE (final)\n"),
m_pWBDisp = NULL;
HRESULT hr = m_spWebBrowser2->get_Document(&spDisp);
if (SUCCEEDED(hr) && spDisp)
CComQIPtr<IHTMLDOCUMENT2 &IID_IHTMLDocument2,> spHTML(spDisp);
CComQIPtr<IOLEOBJECT &IID_IOleObject,> spOleObject(spDisp);
hr = spOleObject->GetClientSite(&spClientSite);
if (SUCCEEDED(hr) && spClientSite)
m_spDefaultDocHostUIHandler = spClientSite;
m_spDefaultOleCommandTarget = spClientSite;
CComQIPtr<ICUSTOMDOC &IID_ICustomDoc,> spCustomDoc(spDisp);
If you compile and run now, you should see that the IE context menu is disabled since we are always returning S_OK from the
ShowContextMenu method. In the MSDN documentation there is an excellent article titled "WebBrowser Customization" that explains exactly, with sample code, how to add code to
ShowContextMenu so as to replicate the original IE context menu, so I won't bother to talk about all that here. What the article did not explain was how to add your own menu to that context menu. Create your menu resource and then add this menu to the top of the context menu in the normal fashion. Nothing tricky here:
g_hPubMenu = LoadMenu(g_hinstPub, MAKEINTRESOURCE(IDR_PUBMENU));
::InsertMenu(hSubMenu, 0, MF_POPUP | MF_BYPOSITION,
(UINT_PTR) g_hPubMenu, _T("Popup Blocker"));
::InsertMenu(hSubMenu, 1, MF_BYPOSITION | MF_SEPARATOR, NULL, NULL);
The tricky part comes when you run the BHO and notice that the all the items on the context menu work as you expect EXCEPT for those items on your menu, which are disabled. This is because IE inconveniently disregards menu items it does not recognize and will not enable your menu items even if you tell it to do so with the
EnableMenuItem command. To fix this, we need to temporarily add our own window procedure just before the call to
TrackPopupMenu so that we can handle messages for our menu, then we restore the original window procedure immediately after
LRESULT CALLBACK CtxMenuWndProc(HWND hwnd, UINT uMsg, WPARAM wParam,
if (uMsg == WM_INITMENUPOPUP)
if (wParam == (WPARAM) g_hPubMenu)
ID_ENABLEPOPUPBLOCKER, MF_BYCOMMAND | (g_bEnabled ?
MF_CHECKED : MF_UNCHECKED));
MF_BYCOMMAND | (g_bPlaySound ? MF_CHECKED : MF_UNCHECKED));
return CallWindowProc(g_lpPrevWndProc, hwnd, uMsg, wParam, lParam);
If a message is not for our menu, we just pass it on to the original window procedure. Now that the menu is working, you can add switches and dialogs to control the behavior of the BHO. In the code included with this article I added a checked menu item to enable/disable the BHO. I also intercepted the normal Open in New Window command and set a flag temporarily disabling the BHO in order to restore normal behavior.
So at this point we have a popup blocker that works pretty well, but there are still a couple of glitches. The first one that you will notice is that certain links seem to be disabled, that is nothing happens when they are clicked. These are links that pop up another window, usually through a bit of script. The BHO happily suppresses these windows with no indication to the user. To solve this problem, I added a way to temporarily override the BHO by holding down the CTRL key. You have to be careful here to not disable the SHIFT + Click shortcut used to open a new window (thanks to Matt Newman for pointing that one out!).
SHORT nState = GetAsyncKeyState(VK_CONTROL);
BOOL bDown = (nState & 0x8000);
nState = GetAsyncKeyState(VK_SHIFT);
bDown = (nState & 0x8000);
But how is the user supposed to know when to press the hot key? In my code I chose to play a sound every time a popup window is suppressed. That way the user has an indication that the BHO is in the act when a link appears to be broken. Then all the user needs to do is hold down the CTRL key while clicking the link, and the link will work normally.
Now our popup blocker is working really well. It blocks all popup windows and we can shut it off if we need to. However, there are still a couple of problems to solve and you probably won't notice them right away. For one, when browsing the web you will start to see these IE Script Error dialogs popping up:
This is even more annoying than the normal popup ads. They occur because some scripts expect to have the popup window available and generate an error when it is not. Because not all script writers add error handlers to their pages, IE acts as the default error handler and poups up its own cryptic dialog box. Another problem is that some options on the Save As dialog under "Save as type" have disappeared, and there are various other UI problems when hosting IE -- like scroll bars going away when viewing help inside Visual Studio.
To solve script error problem, I implemented
IOleCommandTarget and added the methods for
Exec. If I get an
Exec, I set
VARIANT_TRUE to indicate that I wish to continue running scripts and return
S_OK instead of the default to suppress the script error dialog.
class ATL_NO_VTABLE CPub :
public IDispatchImpl<IPUB IPub &__uuidofIPub IPub),(>,
const GUID *pguidCmdGroup,
return m_spDefaultOleCommandTarget->QueryStatus(pguidCmdGroup, cCmds,
const GUID *pguidCmdGroup,
if (nCmdID == OLECMDID_SHOWSCRIPTERROR)
(*pvaOut).vt = VT_BOOL;
(*pvaOut).boolVal = VARIANT_TRUE;
return m_spDefaultOleCommandTarget->Exec(pguidCmdGroup, nCmdID,
nCmdExecOpt, pvaIn, pvaOut);
IOleCommandTarget implementation in place all unhandled script errors are gobbled up and the default IE Script Error dialogs never show up.
I should mention that in my first implementation I was intercepting the
HTMLWindowEvents2::onerror event which gets fired whenever an unhandled script error occurs. It is indeed possible to do it that way, but you have to be careful to attach the handler to the top-level window. If you attach to a child frame, then events in a sibling frame will not be caught (as pointed out by ferdo. Thanks!). If you attach to the top-level window, then unhandled events will eventually bubble up to your handler. In digging into that problem, I decided to scrap that method in favor of the
IOleCommandTarget implementation just because I thought it was cleaner and easier to understand.
The Save As and UI problems are solved by simply calling the default handler from within each
IDocHostUIHandler method . Using the pointer to the default handler we got during
DISPID_DOCUMENTCOMPLETE, we call the default if we are not handling the method ourselves.
STDMETHOD(GetHostInfo)(DOCHOSTUIINFO FAR *pInfo)
It's not clear why these UI problems are not mentioned in MSDN documentation on BHOs (if they are, I missed them), but they definately should be.
<shameless_plug>The latest version can be installed from the web at Osborn Technologies.</shameless_plug>
There is still room for a number of improvements:
- Make the sound and hotkeys user selectable.
- Add an optional visual indicator that a popup window has been blocked.
- Add a whitelist that will allow popups for an entire website or domain.
- April 26,2003: Fixed SHIFT+Click and untrapped script error problems.