Introduction
This tutorial will help you build a mini browser using IE engine. It is based on WTL and uses a wrapper class which I've wrote for handling IWebBrowser2
interface. Since I know it can be hard to read code, this tutorial will help you develop a mini browser - step by step.
Background
Most of my projects are related to internet browsing. I'm using SDI with HTML view a lot of time. Sometimes, I need to use real browser capabilities, so I've written a wrapper for IWebBrowser2
. This wrapper class can handle a window which is embedded in IE. It can also handle events sinking in a very easy way (for example: OnDocumentComplete
).
Creating a new project
We first start by creating a new WTL project. I assume you have WTL files installed (if not, check here). On the first wizard screen, select an SDI application and check for generating .CPP files.
On the second screen, change the default view to an HTML view.
First step will be to edit stdafx.h. Please include atlmisc.h (we'll use CString
from time to time) and atlctrlx.h (for CMultiPaneStatusBarCtrl
). We also need to comment out the _ATL_DLL
definition (we don't want our executable to depend on atl.dll) and to change IE version to version 5.
#define WINVER 0x0400
#define _WIN32_IE 0x0500
#define _RICHEDIT_VER 0x0100
#include <atlbase.h>
#include <atlapp.h>
extern CAppModule _Module;
#include <atlcom.h>
#include <atlhost.h>
#include <atlwin.h>
#include <atlctl.h>
#include <atlmisc.h>
#include <atlframe.h>
#include <atlctrls.h>
#include <atldlgs.h>
#include <atlctrlw.h>
#include <atlctrlx.h>
Updating the view
In out view class, we need to include browser.h and inherit the view class from it. We also need to chain it to the message map so the class can handle several messages (WM_CREATE
, WM_DESTROY
).
#include "browser.h"
class CWTLBrowserView : public CWindowImpl<CWTLBrowserView,
CAxWindow>, public CWebBrowser2<CWTLBrowserView>
{
public:
DECLARE_WND_SUPERCLASS(NULL, CAxWindow::GetWndClassName())
BOOL PreTranslateMessage(MSG* pMsg);
BEGIN_MSG_MAP(CWTLBrowserView)
CHAIN_MSG_MAP(CWebBrowser2<CWTLBrowserView>)
END_MSG_MAP()
};
Creating the menu
We need to add some new items to the menu. A typical browser handles back, forward, home, stop and refresh. We'll add those commands to the menu and the toolbar.
Since some of the items aren't allowed from time to time, we need to handle their UI (we can't always use back and forward). First, we need to add them to the UI update map (in mainfrm.h).
UPDATE_ELEMENT(ID_VIEW_GOTO_BACK, UPDUI_MENUPOPUP|UPDUI_TOOLBAR)
UPDATE_ELEMENT(ID_VIEW_GOTO_FORWARD, UPDUI_MENUPOPUP|UPDUI_TOOLBAR)
We update them via the OnIdle
function.
UIEnable(ID_VIEW_GOTO_BACK,m_view.CanBack());
UIEnable(ID_VIEW_GOTO_FORWARD,m_view.CanForward());
CWebBrowser2
exposes 2 functions (CanBack
& CanForward
) which can determine the status of the back and forward actions.
Since the default wizard starts on microsoft.com, we need to change the code to about:blank, and start on the normal home page. We need to change the code in CMainFrame::OnCreate
.
m_hWndClient = m_view.Create(m_hWnd, rcDefault,
_T("about:blank"),
WS_CHILD | WS_VISIBLE | WS_CLIPSIBLINGS | WS_CLIPCHILDREN |
WS_HSCROLL | WS_VSCROLL, WS_EX_CLIENTEDGE);
.
.
.
m_view.GoHome();
return 0;
Creating the address bar
We now have a working framework, but we still can't enter any URL there! To solve the problem, we'll create an address bar, so the user can enter a URL for navigating. We'll add a new member variable (CEdit
) to our mainframe class - called m_URL
. We'll create and initialize it from CMainFrame::OnCreate
. Since we also want auto completion for our address bar, we'll use SHAutoComplete
function on the edit control.
CreateSimpleReBar(ATL_SIMPLE_REBAR_NOBORDER_STYLE);
AddSimpleReBarBand(hWndCmdBar);
AddSimpleReBarBand(hWndToolBar, NULL, TRUE);
CString szAddress;
szAddress.LoadString(IDS_ADDRESS);
m_URL.Create(m_hWnd,CRect(0,0,0,18),NULL,WS_CHILD|WS_VISIBLE,WS_EX_STATICEDGE);
AddSimpleReBarBand(m_URL,szAddress.GetBuffer(0),TRUE);
m_URL.SetFont(AtlGetDefaultGuiFont());
SHAutoComplete(m_URL,SHACF_URLALL);
CreateSimpleStatusBar();
If we try to compile the project, we'll get an error while linking the files. This error occurs since SHAutoComplete
is exported from shlwapi.dll. To solve it, we need to add the library (shlwapi.lib) to our project.
After compiling the project, we'll see our new bar waiting for us to type in. But hey! If we try to press the Enter key, our browser would just keep sleeping! Let's fix it!
Navigating
Since the HTML framework forwards all keystrokes to the HTML document, we can't just wait for the WM_CHAR
message. We need to add some code to the PreTranslateMessage
function. We need to grab the WM_CHAR
message from the address bar and handle the VK_RETURN
character.
BOOL CMainFrame::PreTranslateMessage(MSG* pMsg)
{
if(CFrameWindowImpl<CMainFrame>::PreTranslateMessage(pMsg))
return TRUE;
if (pMsg->message==WM_CHAR && m_URL==pMsg->hwnd)
{
switch (pMsg->wParam)
{
case VK_RETURN:
{
CString szURL;
int nLength=m_URL.GetWindowTextLength();
m_URL.GetWindowText(szURL.GetBuffer(nLength),nLength+1);
szURL.ReleaseBuffer();
m_view.Navigate(szURL);
return TRUE;
}
}
return FALSE;
}
return m_view.PreTranslateMessage(pMsg);
}
Commands
We can now enter a URL and watch our browser navigate it. But something is still missing. Why can't we use back, forward and the rest of our new commands? We need to add their functions to our mainframe class (since there is nothing special there, simply check the source code to see how it's done).
UI Tweaks
Let's test our browser. Try to go to http://www.codeproject.com/ and click on the WTL section. Why can't we see the new location in our address bar? To fix it, we need to update the address bar each time our location changes. A good place is to handle the OnNavigateComplete2
from our view class. Since we need to update m_URL
which resides inside the mainframe class, we'll create a new reference to it inside our view class and pass the variable on the constructor. We now can handle our message and update the address bar.
void CWTLBrowserView::OnNavigateComplete2(IDispatch* pDisp, const String& szURL)
{
m_URL.SetWindowText(GetLocationURL());
}
Another useful tweak includes a progress notification, secured icon, and status bar text. They all reside on the status bar. It's time to bring our big guns - CMultiPaneStatusBarCtrl
! We'll create a new variable inside the mainframe class, plus a reference inside our view class. The status bar should contain 3 parts: the default text, a secured icon, and a progress notification. Since the default text has a unique id (ID_DEFAULT_PANE
), all we need to do is to create additional two identifiers. From the "View->Resource Symbols" menu, we need to create our new symbols: IDR_LOCK
and IDR_PROGRESS
. After creating them, we can initialize our new status bar from the CMainFrame::OnCreate
function.
CreateSimpleStatusBar();
m_StatusBar.SubclassWindow(m_hWndStatusBar);
int nPanes[]={ID_DEFAULT_PANE,IDR_LOCK,IDR_PROGRESS};
m_StatusBar.SetPanes(nPanes,sizeof(nPanes)/sizeof(int),false);
m_StatusBar.SetPaneWidth(IDR_LOCK,30);
m_StatusBar.SetPaneWidth(IDR_PROGRESS,50);
We also need to add a new icon to our project (IDI_LOCK
) and load it into a variable (m_hSecured
). To handle proper UI updating, we'll add a new line to the UI update map:
UPDATE_ELEMENT(0, UPDUI_STATUSBAR)
In our view class, we'll add a new variable (m_bSecured
) and some code to handle our status bar updates:
void CWTLBrowserView::OnStatusTextChange(const String& szText)
{
m_StatusBar.SetPaneText(ID_DEFAULT_PANE,szText);
}
void CWTLBrowserView::OnProgressChange(long nProgress, long nProgressMax)
{
CString szText;
if (nProgressMax>0)
szText.Format(_T("%d%%"),nProgress*100/nProgressMax);
m_StatusBar.SetPaneText(IDR_PROGRESS,szText);
}
void CWTLBrowserView::OnSetSecureLockIcon(long nSecureLockIcon)
{
m_bSecured=nSecureLockIcon>0;
}
Finally, we need to add some code to CMainFrame::OnIdle
:
m_StatusBar.SetPaneIcon(IDR_LOCK,m_view.IsSecured() ? m_hSecured : NULL);
UIUpdateToolBar();
UIUpdateStatusBar();
return FALSE;
File Command
To allow the browser to save and print files, we need to handle file messages. To send a command to the browser, we need to use the ExecWB
function. To query a command status (for the UI map), we can use the QueryStatusWB
function. We need to add the proper functions (for saving/printing), update the UI update map and handle their UI from the OnIdle
function. Since it is fairly simple, I won't include it in the article (check the source code for more details).
Edit Command
Edit commands are a special case. Since they can be used with the browser, as well as with our address bar, we need to check our focus window each time we use those commands. First, we add them to the UI update map, then we handle their update via the OnIdle
function:
if (GetFocus()==m_URL)
{
DWORD dwSelection=m_URL.GetSel();
BOOL bEnable=HIWORD(dwSelection)!=LOWORD(dwSelection);
UIEnable(ID_EDIT_CUT,bEnable);
UIEnable(ID_EDIT_COPY,bEnable);
if (m_URL.OpenClipboard())
{
UIEnable(ID_EDIT_PASTE,IsClipboardFormatAvailable(CF_TEXT));
CloseClipboard();
}
else
UIEnable(ID_EDIT_PASTE,FALSE);
UIEnable(ID_EDIT_UNDO,m_URL.CanUndo());
}
else
{
UIEnable(ID_EDIT_CUT,m_view.QueryStatusWB(OLECMDID_CUT) & OLECMDF_ENABLED);
UIEnable(ID_EDIT_COPY,m_view.QueryStatusWB(OLECMDID_COPY) & OLECMDF_ENABLED);
UIEnable(ID_EDIT_PASTE,m_view.QueryStatusWB(OLECMDID_PASTE) & OLECMDF_ENABLED);
UIEnable(ID_EDIT_UNDO,m_view.QueryStatusWB(OLECMDID_UNDO) & OLECMDF_ENABLED);
}
We also need to distinguish them while executing the actual commands:
LRESULT CMainFrame::OnEditCut(WORD ,
WORD , HWND , BOOL& )
{
if (GetFocus()==m_URL)
m_URL.Cut();
else
m_view.ExecWB(OLECMDID_CUT,OLECMDEXECOPT_DONTPROMPTUSER,NULL,NULL);
return 0;
}
LRESULT CMainFrame::OnEditCopy(WORD ,
WORD , HWND , BOOL& )
{
if (GetFocus()==m_URL)
m_URL.Copy();
else
m_view.ExecWB(OLECMDID_COPY,OLECMDEXECOPT_DONTPROMPTUSER,NULL,NULL);
return 0;
}
LRESULT CMainFrame::OnEditPaste(WORD ,
WORD , HWND , BOOL& )
{
if (GetFocus()==m_URL)
m_URL.Paste();
else
m_view.ExecWB(OLECMDID_PASTE,OLECMDEXECOPT_DONTPROMPTUSER,NULL,NULL);
return 0;
}
LRESULT CMainFrame::OnEditUndo(WORD ,
WORD , HWND , BOOL& )
{
if (GetFocus()==m_URL)
m_URL.Undo();
else
m_view.ExecWB(OLECMDID_UNDO,OLECMDEXECOPT_DONTPROMPTUSER,NULL,NULL);
return 0;
}
Final Words
Please excuse me for any typing/grammar mistakes I might have made. English is not my natural language, so I make mistakes from time to time. You can use any part of the code posted with the article. If you use it within your software, please email me and let me know (don't worry - no money will be asked for it). Special thanks to Michael Dunn, Ed Gadziemski and PJ Naughter. I've learned a lot from their articles.