Programmer » Webform

Example code for embedding a web browser

This code (c) 2002-2006 Lucian Wischik. The code is free and anyone can do with it whatever they like, including incorporating it in commercial products.

Overview

This code is concerned with embedding a web browser control in a plain C++ Windows SDK application, without having to use a framework class. I call this control a "Webform". It is intended to replace the command ShowHTMLDialog, but to give you a lot more control and flexibility. For instance, it supports dynamic creation of pages: you call WebformReady() to start creating the next page, and WebformSet to add text, and call WebformGo() when you're done. The dynamic page is written to a temporary file and then displayed. The control itself can be embedded into a dialog or a regular window. I gave the control an API that's similar in style to the other win32 controls (listboxes, buttons, listviews &c.)

This web page contains examples of how to use the Webform control, documentation for the webform methods and notifications, and the files webform.h and webform.cpp. I wrote it under Visual Studio 2005 and Borland C++Builder 5.

The rest of this page describes the code. The page is publically editable. I hope that people will click the Edit button (bottom left) and make bug-fixes and comments. The History button (also bottom left) will show what changes have been made.


Examples of how to program with it.

Minimal example app.

This complete application displays just a single window (not dialog), wholly filled by a webform.

#include <windows.h>
#include <tchar.h>
#include "webform.h"

HINSTANCE hInstance;
HWND hMain;         // Our main window
HWND hwebf;         // We declare this handle globally, just for convenience

LRESULT CALLBACK PlainWndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
{ switch (msg)
  { case WM_CREATE:
    {  hwebf = WebformCreate(hwnd,103);  // We pick 103 as the id for our child control
       WebformGo(hwebf,_T("http://www.wischik.com/lu/Programmer"));
    } break;
    case WM_SIZE:
    { MoveWindow(hwebf,0,0,LOWORD(lParam), HIWORD(lParam),TRUE);
    } break;
    case WM_COMMAND:
    { int id=LOWORD(wParam), code=HIWORD(wParam);
      // The webf control does not auto-navigate. We must do that manually.
      // This gives us precise control over its behaviour. In this case,
      // we redirect any CNN-visits with the BBC. Hooray for quality broadcasting!
      if (id==103 && code==WEBFN_CLICKED)
      { const TCHAR *url = WebformLastClick(hwebf);
        if (_tcscmp(url,_T("http://cnn.com"))==0) WebformGo(hwebf,_T("http://bbc.co.uk"));
        else WebformGo(hwebf, url);
      }
    } break;
    case WM_DESTROY:
    { PostQuitMessage(0);
    } break;
  }
  return DefWindowProc(hwnd, msg, wParam, lParam);
}


int WINAPI WinMain(HINSTANCE h,HINSTANCE,LPSTR,int)
{ hInstance=h;
  OleInitialize(0);
  //
  WNDCLASSEX wcex={0}; wcex.cbSize = sizeof(WNDCLASSEX);
  wcex.style = CS_HREDRAW|CS_VREDRAW;
  wcex.lpfnWndProc = (WNDPROC)PlainWndProc;
  wcex.hInstance = hInstance;
  wcex.hCursor = LoadCursor(NULL, IDC_ARROW);
  wcex.lpszClassName = _T("PlainClass");
  ATOM res=RegisterClassEx(&wcex);
  if (res==0) {MessageBox(NULL,_T("Failed to register class"),_T("Error"),MB_OK); return 0;}
  //
  hMain = CreateWindowEx(0,_T("PlainClass"), _T("Plain Window"), WS_OVERLAPPEDWINDOW|WS_CLIPCHILDREN,
      CW_USEDEFAULT, CW_USEDEFAULT, 400, 400, NULL, NULL, hInstance, NULL);
  if (hMain==NULL) {MessageBox(NULL,_T("Failed to create window"),_T("Error"),MB_OK); return 0;}
  ShowWindow(hMain,SW_SHOW);
  //
  MSG msg;
  while (GetMessage(&msg, NULL, 0, 0))
  { TranslateMessage(&msg);
    DispatchMessage(&msg);
  }
  //
  OleUninitialize();
  return (int)msg.wParam;
}

To enable scroll bars.

Use the CreateWindow/WS_VSCROLL to create the webform control, rather than the easy WebformCreate:

hwebf = CreateWindow(WEBFORM_CLASS, _T("http://www.wischik.com/lu/Programmer"),
                     WS_CHILD|WS_CLIPSIBLINGS|WS_VISIBLE|WS_VSCROLL,
                     0,0,100,100,hwnd,(HMENU)103,hInstance,0);

To display a form.

An HTML form is roughly the equivalent of a regular dialog. For one of these, use an HTML page with this sort of content. (You might create this dynamically using WebformReady/Set/Go, or statically as a file on disk).

<html><body><form>
<input name='cx' type='checkbox' checked> Do you want some?<br>
<input name='tx' type='text' value='Come and get it!'><br>
<input name='sub' type='submit' value='Ok'>
<input name='sub' type='submit' value='Cancel'>
</form></body></html>

Then when the user clicks on Ok or Cancel, we'll get a WM_COMMAND/WEBFN_CLICKED notify message and with a url something like "a.html?cx=on&tx=hello%3F&sub=Ok". To parse the url,

const TCHAR *cx = WebformGetItem(hwebf,_T("cx"));
const TCHAR *tx = WebformGetItem(hwebf,_T("tx"));
const TCHAR *sub= WebformGetItem(hwebf,_T("sub"));
if (sub!=0 && _tcscmp(sub,_T("Ok"))==0) ...

The downloadable sample code demonstrates a technique where you can call WebformReady/Set/Go and then immediately (synchronously) continue with responding to the click -- rather than having to code your click-response asynchronously inside WM_COMMAND/WEBFN_CLICKED. It's similar to the 'loaded' technique below:

To wait until the document is loaded.

Use this sort of technique when you want to WebformGo to a url and then manipulate the page immediately in the same block of code, rather than having to manipulate it inside WM_COMMAND/WEBFN_LOADED.

bool loaded;  // we'll declare it as a global variable for convenience


// This is the code we'd use to load a page, and wait until it had finished loading
loaded=false; WebformGo(hwebf,_T("http://www.wischik.com"));
while (!loaded) {PumpMessages();Sleep(5);} 


// It relies upon this handler for WM_COMMAND/WEBFN_CLICKED
// This is the notification we get when a page is loaded:
case WM_COMMAND:
{  int id=LOWORD(wParam), code=HIWORD(wParam);
   if (id==103 && code=WEBFN_LOADED) loaded=true;
}


// This is the code for the message pump
void PumpMessages()
{ MSG msg; while (true)
  { BOOL res=PeekMessage(&msg,0,0,0,PM_REMOVE);
    if (!res || msg.message==WM_QUIT) return;
    TranslateMessage(&msg); DispatchMessage(&msg);
  }
}

Actually, this code doesn't deal properly with the case where the user terminates the application while the page is loading, i.e. within the while/PumpMessages loop. To handle that, PumpMessages should set a global "isquit" boolean flag, and the while loop should break if it sees that flag set. The downloadable sample code illustrates how.

To extract information.

This example extract's the page's title. You can call it only after the page has loaded, since otherwise WebformGetDoc will just return 0. You can use the technique above to wait until the page is loaded.

IHTMLDocument2 *doc = WebformGetDoc(hwebf);
BSTR b=0; doc->get_title(&b);
MessageBoxW(hwnd,b,L"Title:",MB_OK);
if (b!=0) SysFreeString(b);
doc->Release();

To access/modify a particular html element.

This is the equivalent of document.getElementById("id"). It finds an element in the page with a given id or name (and here we alter its properties). Again, it requires the page to be loaded.

// Let's alter a property of something on the form
IHTMLDivElement *e = WebformGetElement<IHTMLDivElement>(hwebf,_T("id"));
if (e!=0) {e->put_align(L"right"); e->Release();}

// And alter the text: document.all["id"].innerHTML="fred<br>,mary";
IHTMLElement *e2 = WebformGetElement<IHTMLElement>(hwebf,_T("id"));
if (e2!=0) {e2->put_innerHTML(L"fred<br>mary"); e2->Release();}

To execute (invoke) a script.

This example will execute a javascript. Again, it requires the page to be loaded.

IHTMLDocument2 *doc = WebformGetDoc(hwebf);
IHTMLWindow2 *win=0; doc->get_parentWindow(&win);
if (win!=0)
{ BSTR cmd = SysAllocString(L"MyJavascriptFunc('hello from javascript')");
  VARIANT v; VariantInit(&v);
  win->execScript(cmd,NULL,&v);
  VariantClear(&v);
  SysFreeString(cmd);
  win->Release();
}
doc->Release();

To retrieve text from an input.

Obviously, it's easier to use a form and just do WebformGetItem(hwebf,"key") to get the value of a text input. But you can use the following code to retrieve the value without the user needing to click or change page.

// We could also have queried the inputs using DOM:
IHTMLInputTextElement *inptxt = WebformGetElement<IHTMLInputTextElement>(hwebf,_T("tx"));
if (inptxt!=0)
{ IHTMLTxtRange *range=0; inptxt->createTextRange(&range);
  if (range!=0)
  { BSTR b=0; range->get_text(&b);
    if (b!=0)
    { MessageBoxW(hwnd,b,L"Contents of text box",MB_OK);
      SysFreeString(b);
    }
    range->Release();
  }
  inptxt->Release();
}

To use the standard keyboard shortcuts.

Here we respond to the familiar accelerators Backspace, Alt+Left, Alt+Right. Note that the control already, automatically, responds to the cursor keys for scrolling.

case WM_SYSKEYDOWN: // respond to keypresses
{ WPARAM key=wParam;
  bool isalt = (lParam & 0x20000000)!=0;
  if (isalt && key==VK_LEFT) {IWebBrowser2 *ibrowser=WebformGetBrowser(hwebf); ibrowser->GoBack(); ibrowser->Release();}
  else if (isalt && key==VK_RIGHT) {IWebBrowser2 *ibrowser=WebformGetBrowser(hwebf); ibrowser->GoForward(); ibrowser->Release();}
} break;

case WM_KEYDOWN:
{ WPARAM key=wParam; // 0xA6=VK_BROWSER_BACK, 0xA7=VK_BROWSER_FORWARD
  if (key==0xA6) {IWebBrowser2 *ibrowser=WebformGetBrowser(hwebf); ibrowser->GoBack(); ibrowser->Release();}
  if (key==0xA7) {IWebBrowser2 *ibrowser=WebformGetBrowser(hwebf); ibrowser->GoForward(); ibrowser->Release();}
  if (key==VK_BACK)
  { // If focus is in a text-element, then Backspace will delete characters.
    // Otherwise, it will go to the previous page.
    bool hastextfocus=false;
    IHTMLDocument2 *doc = WebformGetDoc(hwebf);
    IHTMLElement *focus=0; doc->get_activeElement(&focus);
    if (focus!=0)
    { IHTMLTextElement *text=0;
      focus->QueryInterface(IID_IHTMLInputTextElement,(void**)&text);
      if (text!=0) {hastextfocus=true; text->Release();}
      focus->Release();
    }
    doc->Release();
    if (!hastextfocus) {IWebBrowser2 *ibrowser=WebformGetBrowser(hwebf); ibrowser->GoBack(); ibrowser->Release();}
  }
} break;

The header file 'webform.h'

#ifndef __webform_H
#define __webform_H
#include <mshtml.h>
#include <exdisp.h>
#include <tchar.h>



#define WEBFORM_CLASS (_T("WebformClass"))
// Create a Webfrom control either by calling HWND hwebf=WebformCreate(hparent,id);
// or with CreateWindow(WEBFORM_CLASS,_T("initial-url"),...)
// If you specify WS_VSCROLL style then the webform will be created with scrollbars.

#define WEBFN_CLICKED      2 
// This notification is sent via WM_COMMAND to the parent window when a link
// has been clicked in the webform. Use WebformLastClick(hwebf) to obtain the url.

#define WEBFN_LOADED       3    
// This notification is sent via WM_COMMAND when you have called WebformGo(hwebf,url).
// It indicates that the page has finished loading.


#define WEBFM_READY        (WM_USER+0)
// HANDLE hf = WebformReady(hwebf);
// HANDLE hf = (HANDLE)SendMessage(hwebf,WEBFM_READY,0,0);
// Both are equivalent. Call this when you want to create a dynamic page.
// Build the dynamic page either with WriteFile(hf) or with WebformSet(hwebf,text)
// There is no need to close the handle. It will be closed automatically when
// you call WebformGo, or when the webform control closes.

#define WEBFM_SET          (WM_USER+1)
// WebformSet(hwebf,_T(text));
// SendMessage(hwebf,WEBFM_SET,0,(LPARAM)tchar);
// Both are equivalent. Call this to build the dynamic page.

// WebformGo(hwebf,_T("http://www.wischik.com/lu/Programmer"));
// WebformGo(hwebf,_T("about:blank"));
// WebformGo(hwebf,_T("c:\\temp\\file.html"));
// SetWindowText(hwebf,_T("http://www.wischik.com"));
// WebformGo(hwebf,0);
// All these are equivalent ways of navigating the browser.
// If the navigation destination is 0, as in the last one, then
// it will navigate to the dynamic page you have been building with Ready/Set.

#define WEBFM_GETLASTCLICK (WM_USER+2)
// TCHAR *url = WebformGetLastClick(hwebf);
// TCHAR *url = (TCHAR*)SendMessage(hwebf,WEBFM_GETLASTCLICK,0,0);
// Both are equivalent. This retrieves the URL that the user just clicked on.

#define WEBFM_GETITEM      (WM_USER+3)
// TCHAR *val = WebformGetItem(hwebf,_T("key"));
// TCHAR *val = (TCHAR*)SendMessage(hwebf,WEBFM_GETITEM,0,(LPARAM)key);
// Both are equivalent. If the user had clicked on a form-submission link,
// forming a get-request like mypage.html?key=val&x=y
// then call WebformGetItem(hwebf,_T("key")) to retrieve "val",
// and similarly for the other key/val pairs in the get-request

#define WEBFM_GETDOC       (WM_USER+4)
// IHTMLDocument2 *doc = WebformGetDoc(hwebf);
// IHTMLDocument2 *doc = (IHTMLDocument2*)SendMessage(hwebf,WEBFM_GETDOC,0,0);
// Both are equivalent. They retrieve the "doc" that's currently
// being displayed. While the page is being loaded, doc will return NULL.
// If you call this, you are responsible for doing doc->Release() afterwards.

#define WEBFM_GETBROWSER   (WM_USER+5)
// IWebBrowser2 *ibrowser = WebformGetBrowser(hwebf);
// IWebBrowser2 *ibrowser = (IWebBrowser2*)SendMessage(hwebf,WEBFM_GETBROWSER,0,0);
// Both are equivalent. They retrieve the "browser" object.
// This will always be valid. If you call this, you are responsible for
// doing ibrowser->Release() afterwards.

#define WEBFM_GETELEMENT   (WM_USER+6)
// IHTMLElement *elem = WebformGetElement<IHTMLElement>(hwebf,"id");
// IHTMLDivElement *div = WebformGetElement<IHTMLDivElement>(hwebf,"id");
// IHTMLElement *elem = (IHTMLElement*)SendMessage(hwebf,WEBFM_GETELEMENT,0,(LPARAM)id);
// This function obtains an html element from the document. This will
// only work once the document is loaded; before then it will return NULL.
// If you call this and get a non-NULL result then you are responsible
// for calling Release() afterwards.
// The Sendmessage form always returns the IHTMLElement which has html id="id".
// The WebformGetElement form is templated. It fetches the html element,
// then does QueryInterface to obtain the indicated interface type on it.
// If the element is of the wrong type (e.g. you try to get IHTMLDivElement
// on something that's really an IHTMLTextElement) then it will return 0.



#pragma warning(suppress:4312)
inline HWND WebformCreate(HWND hparent,UINT id) {return CreateWindowEx(0,WEBFORM_CLASS,_T("about:blank"),WS_CHILD|WS_CLIPSIBLINGS|WS_VISIBLE,0,0,100,100,hparent,(HMENU)id,GetModuleHandle(0),0);}
inline void WebformDestroy(HWND hwebf) {DestroyWindow(hwebf);}

inline HANDLE WebformReady(HWND hwebf) {return (HANDLE)SendMessage(hwebf,WEBFM_READY,0,0);}
inline void WebformSet(HWND hwebf, const TCHAR *buf) {SendMessage(hwebf,WEBFM_SET,0,(LPARAM)buf);}
inline void WebformGo(HWND hwebf,const TCHAR *fn) {SetWindowText(hwebf,fn);}

inline const TCHAR* WebformLastClick(HWND hwebf) {return (TCHAR*)SendMessage(hwebf,WEBFM_GETLASTCLICK,0,0);}
inline const TCHAR* WebformGetItem(HWND hwebf, const TCHAR *name) {return (TCHAR*)SendMessage(hwebf,WEBFM_GETITEM,0,(LPARAM)name);}

inline IHTMLDocument2 *WebformGetDoc(HWND hwebf) {return (IHTMLDocument2*)SendMessage(hwebf,WEBFM_GETDOC,0,0);}
inline IWebBrowser2 *WebformGetBrowser(HWND hwebf) {return (IWebBrowser2*)SendMessage(hwebf,WEBFM_GETBROWSER,0,0);}

template<class T> inline T* WebformGetElement(HWND hwebf,const TCHAR *id)
{ IHTMLElement *e = (IHTMLElement*)SendMessage(hwebf,WEBFM_GETELEMENT,0,(LPARAM)id); if (e==0) return 0;
  T *r=0; e->QueryInterface(__uuidof(T),(void**)&r); e->Release();
  return r;
}
template<> inline IHTMLElement* WebformGetElement<IHTMLElement>(HWND hwebf,const TCHAR *id)
{ return (IHTMLElement*)SendMessage(hwebf,WEBFM_GETELEMENT,0,(LPARAM)id);
}



#endif

The source file 'webform.cpp'

#include <windows.h>
#include <mshtmhst.h>
#include <mshtmdid.h>
#include <exdispid.h>
#include <tchar.h>
#include <list>
#include <string>
#include "webform.h"
using namespace std;
typedef basic_string<TCHAR> tstring;

#ifndef DOCHOSTUIFLAG_NO3DOUTERBORDER
#define DOCHOSTUIFLAG_NO3DOUTERBORDER 0x200000
#endif

// An HWEBF handle is actually just an (opaque) pointer to one of these.
//
struct TWebf : public IUnknown
{ 
  long ref;
  // IUnknown
  HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void **ppv)
  { if (riid==IID_IUnknown) {*ppv=this; AddRef(); return S_OK;}
    if (riid==IID_IOleClientSite) {*ppv=&clientsite; AddRef(); return S_OK;}
    if (riid==IID_IOleWindow || riid==IID_IOleInPlaceSite) {*ppv=&site; AddRef(); return S_OK;}
    if (riid==IID_IOleInPlaceUIWindow || riid==IID_IOleInPlaceFrame) {*ppv=&frame; AddRef(); return S_OK;}
    if (riid==IID_IDispatch) {*ppv=&dispatch; AddRef(); return S_OK;}
    if (riid==IID_IDocHostUIHandler) {*ppv=&uihandler; AddRef(); return S_OK;}
    return E_NOINTERFACE;
  }
  ULONG STDMETHODCALLTYPE AddRef() {return InterlockedIncrement(&ref);}
  ULONG STDMETHODCALLTYPE Release() {int tmp = InterlockedDecrement(&ref); if (tmp==0) delete this; return tmp;}

  struct TOleClientSite : public IOleClientSite
  { public: TWebf *webf;
    // IUnknown
    HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void **ppv) {return webf->QueryInterface(riid,ppv);}
    ULONG STDMETHODCALLTYPE AddRef() {return webf->AddRef();}
    ULONG STDMETHODCALLTYPE Release() {return webf->Release();}
    // IOleClientSite
    HRESULT STDMETHODCALLTYPE SaveObject() {return E_NOTIMPL;}
    HRESULT STDMETHODCALLTYPE GetMoniker(DWORD dwAssign,DWORD dwWhichMoniker,IMoniker **ppmk) {return E_NOTIMPL;}
    HRESULT STDMETHODCALLTYPE GetContainer(IOleContainer **ppContainer) {*ppContainer=0; return E_NOINTERFACE;}
    HRESULT STDMETHODCALLTYPE ShowObject() {return S_OK;}
    HRESULT STDMETHODCALLTYPE OnShowWindow(BOOL fShow) {return E_NOTIMPL;}
    HRESULT STDMETHODCALLTYPE RequestNewObjectLayout() {return E_NOTIMPL;}
  } clientsite;

  struct TOleInPlaceSite : public IOleInPlaceSite
  { TWebf *webf;
    // IUnknown
    HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void **ppv) {return webf->QueryInterface(riid,ppv);}
    ULONG STDMETHODCALLTYPE AddRef() {return webf->AddRef();}
    ULONG STDMETHODCALLTYPE Release() {return webf->Release();}
    // IOleWindow
    HRESULT STDMETHODCALLTYPE GetWindow(HWND *phwnd) {*phwnd=webf->hhost; return S_OK;}
    HRESULT STDMETHODCALLTYPE ContextSensitiveHelp(BOOL fEnterMode) {return E_NOTIMPL;}
    // IOleInPlaceSite
    HRESULT STDMETHODCALLTYPE CanInPlaceActivate() {return S_OK;}
    HRESULT STDMETHODCALLTYPE OnInPlaceActivate() {return S_OK;}
    HRESULT STDMETHODCALLTYPE OnUIActivate() {return S_OK;}
    HRESULT STDMETHODCALLTYPE GetWindowContext(IOleInPlaceFrame **ppFrame,IOleInPlaceUIWindow **ppDoc,LPRECT lprcPosRect,LPRECT lprcClipRect,LPOLEINPLACEFRAMEINFO info)
    { *ppFrame = &webf->frame; webf->frame.AddRef();
      *ppDoc = 0;
      info->fMDIApp=FALSE; info->hwndFrame=webf->hhost; info->haccel=0; info->cAccelEntries=0;
      GetClientRect(webf->hhost,lprcPosRect);
      GetClientRect(webf->hhost,lprcClipRect);
	    return(S_OK);
    }
    HRESULT STDMETHODCALLTYPE Scroll(SIZE scrollExtant) {return E_NOTIMPL;}
    HRESULT STDMETHODCALLTYPE OnUIDeactivate(BOOL fUndoable) {return S_OK;}
    HRESULT STDMETHODCALLTYPE OnInPlaceDeactivate() {return S_OK;}
    HRESULT STDMETHODCALLTYPE DiscardUndoState() {return E_NOTIMPL;}
    HRESULT STDMETHODCALLTYPE DeactivateAndUndo() {return E_NOTIMPL;}
    HRESULT STDMETHODCALLTYPE OnPosRectChange(LPCRECT lprcPosRect)
    { IOleInPlaceObject *iole=0;
      webf->ibrowser->QueryInterface(IID_IOleInPlaceObject,(void**)&iole);
      if (iole!=0) {iole->SetObjectRects(lprcPosRect,lprcPosRect); iole->Release();}
      return S_OK;
    }
  } site;

  struct TOleInPlaceFrame : public IOleInPlaceFrame
  { TWebf *webf;
    // IUnknown
    HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void **ppv) {return webf->QueryInterface(riid,ppv);}
    ULONG STDMETHODCALLTYPE AddRef() {return webf->AddRef();}
    ULONG STDMETHODCALLTYPE Release() {return webf->Release();}
    // IOleWindow
    HRESULT STDMETHODCALLTYPE GetWindow(HWND *phwnd) {*phwnd=webf->hhost; return S_OK;}
    HRESULT STDMETHODCALLTYPE ContextSensitiveHelp(BOOL fEnterMode) {return E_NOTIMPL;}
    // IOleInPlaceUIWindow
    HRESULT STDMETHODCALLTYPE GetBorder(LPRECT lprectBorder) {return E_NOTIMPL;}
    HRESULT STDMETHODCALLTYPE RequestBorderSpace(LPCBORDERWIDTHS pborderwidths) {return E_NOTIMPL;}
    HRESULT STDMETHODCALLTYPE SetBorderSpace(LPCBORDERWIDTHS pborderwidths) {return E_NOTIMPL;}
    HRESULT STDMETHODCALLTYPE SetActiveObject(IOleInPlaceActiveObject *pActiveObject,LPCOLESTR pszObjName) {return S_OK;}\
    // IOleInPlaceFrame
    HRESULT STDMETHODCALLTYPE InsertMenus(HMENU hmenuShared,LPOLEMENUGROUPWIDTHS lpMenuWidths) {return E_NOTIMPL;}
    HRESULT STDMETHODCALLTYPE SetMenu(HMENU hmenuShared,HOLEMENU holemenu,HWND hwndActiveObject) {return S_OK;}
    HRESULT STDMETHODCALLTYPE RemoveMenus(HMENU hmenuShared) {return E_NOTIMPL;}
    HRESULT STDMETHODCALLTYPE SetStatusText(LPCOLESTR pszStatusText) {return S_OK;}
    HRESULT STDMETHODCALLTYPE EnableModeless(BOOL fEnable) {return S_OK;}
    HRESULT STDMETHODCALLTYPE TranslateAccelerator(LPMSG lpmsg,WORD wID) {return E_NOTIMPL;}
   } frame;

  struct TDispatch : public IDispatch
  { TWebf *webf;
    // IUnknown
    HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void **ppv) {return webf->QueryInterface(riid,ppv);}
    ULONG STDMETHODCALLTYPE AddRef() {return webf->AddRef();}
    ULONG STDMETHODCALLTYPE Release() {return webf->Release();}
    // IDispatch
    HRESULT STDMETHODCALLTYPE GetTypeInfoCount(UINT *pctinfo) {*pctinfo=0; return S_OK;}
    HRESULT STDMETHODCALLTYPE GetTypeInfo(UINT iTInfo,LCID lcid,ITypeInfo **ppTInfo) {return E_FAIL;}
    HRESULT STDMETHODCALLTYPE GetIDsOfNames(REFIID riid,LPOLESTR *rgszNames,UINT cNames,LCID lcid,DISPID *rgDispId) {return E_FAIL;}
    HRESULT STDMETHODCALLTYPE Invoke(DISPID dispIdMember,REFIID riid,LCID lcid,WORD wFlags,DISPPARAMS *Params,VARIANT *pVarResult,EXCEPINFO *pExcepInfo,UINT *puArgErr)
    { switch (dispIdMember) 
      { // DWebBrowserEvents2
        case DISPID_BEFORENAVIGATE2: webf->BeforeNavigate2(Params->rgvarg[5].pvarVal->bstrVal, Params->rgvarg[0].pboolVal); break;
        case DISPID_DOCUMENTCOMPLETE: webf->DocumentComplete(Params->rgvarg[0].pvarVal->bstrVal); break;
        // http://msdn2.microsoft.com/en-us/library/ms671911(VS.80).aspx
        case DISPID_AMBIENT_DLCONTROL: 
        { pVarResult->vt = VT_I4;
          pVarResult->lVal = DLCTL_DLIMAGES | DLCTL_VIDEOS | DLCTL_BGSOUNDS | DLCTL_SILENT;
        } break;
        default: return DISP_E_MEMBERNOTFOUND;
      }
      return S_OK;
    }
  } dispatch;


  struct TDocHostUIHandler : public IDocHostUIHandler
  { TWebf *webf;
    // IUnknown
    HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void **ppv) {return webf->QueryInterface(riid,ppv);}
    ULONG STDMETHODCALLTYPE AddRef() {return webf->AddRef();}
    ULONG STDMETHODCALLTYPE Release() {return webf->Release();}
    // IDocHostUIHandler
    HRESULT STDMETHODCALLTYPE ShowContextMenu(DWORD dwID, POINT *ppt, IUnknown *pcmdtReserved, IDispatch *pdispReserved) {return S_OK;}
    HRESULT STDMETHODCALLTYPE GetHostInfo(DOCHOSTUIINFO *pInfo) {pInfo->dwFlags=(webf->hasscrollbars?0:DOCHOSTUIFLAG_SCROLL_NO)|DOCHOSTUIFLAG_NO3DOUTERBORDER; return S_OK;}
    HRESULT STDMETHODCALLTYPE ShowUI(DWORD dwID,IOleInPlaceActiveObject *pActiveObject,IOleCommandTarget *pCommandTarget,IOleInPlaceFrame *pFrame,IOleInPlaceUIWindow *pDoc) {return S_OK;}
    HRESULT STDMETHODCALLTYPE HideUI() {return S_OK;}
    HRESULT STDMETHODCALLTYPE UpdateUI() {return S_OK;}
    HRESULT STDMETHODCALLTYPE EnableModeless(BOOL fEnable) {return S_OK;}
    HRESULT STDMETHODCALLTYPE OnDocWindowActivate(BOOL fActivate) {return S_OK;}
    HRESULT STDMETHODCALLTYPE OnFrameWindowActivate(BOOL fActivate) {return S_OK;}
    HRESULT STDMETHODCALLTYPE ResizeBorder(LPCRECT prcBorder,IOleInPlaceUIWindow *pUIWindow,BOOL fRameWindow) {return S_OK;}
    HRESULT STDMETHODCALLTYPE TranslateAccelerator(LPMSG lpMsg,const GUID *pguidCmdGroup,DWORD nCmdID) {return S_FALSE;}
    HRESULT STDMETHODCALLTYPE GetOptionKeyPath(LPOLESTR *pchKey,DWORD dw) {return S_FALSE;}
    HRESULT STDMETHODCALLTYPE GetDropTarget(IDropTarget *pDropTarget,IDropTarget **ppDropTarget) {return S_FALSE;}
    HRESULT STDMETHODCALLTYPE GetExternal(IDispatch **ppDispatch) {*ppDispatch=0; return S_FALSE;}
    HRESULT STDMETHODCALLTYPE TranslateUrl(DWORD dwTranslate,OLECHAR *pchURLIn,OLECHAR **ppchURLOut) {*ppchURLOut=0; return S_FALSE;}
    HRESULT STDMETHODCALLTYPE FilterDataObject(IDataObject *pDO,IDataObject **ppDORet) {*ppDORet=0; return S_FALSE;}
  } uihandler;

  struct TDocHostShowUI : public IDocHostShowUI
  { TWebf *webf;
    // IUnknown
    HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void **ppv) {return webf->QueryInterface(riid,ppv);}
    ULONG STDMETHODCALLTYPE AddRef() {return webf->AddRef();}
    ULONG STDMETHODCALLTYPE Release() {return webf->Release();}
    // IDocHostShowUI
    HRESULT STDMETHODCALLTYPE ShowMessage(HWND hwnd,LPOLESTR lpstrText,LPOLESTR lpstrCaption,DWORD dwType,LPOLESTR lpstrHelpFile,DWORD dwHelpContext,LRESULT *plResult) {*plResult=IDCANCEL; return S_OK;}
    HRESULT STDMETHODCALLTYPE ShowHelp(HWND hwnd,LPOLESTR pszHelpFile,UINT uCommand,DWORD dwData,POINT ptMouse,IDispatch *pDispatchObjectHit) {return S_OK;}
  } showui;


  TWebf(HWND hhost);
  ~TWebf();
  void CloseThread();
  void Close();
  //
  HANDLE Ready();
  DWORD Set(const TCHAR *buf);
  void Go(const TCHAR *fn);
  const TCHAR *LastClick();
  const TCHAR *GetItem(const TCHAR *name);
  IHTMLDocument2 *GetDoc();
  IHTMLElement *GetElement(const TCHAR *id);
  void DeleteOldFiles();
  //
  void BeforeNavigate2(const wchar_t *url,short *cancel);
  void DocumentComplete(const wchar_t *url); 
  unsigned int isnaving;    // bitmask: 4=haven't yet finished Navigate call, 2=haven't yet received DocumentComplete, 1=haven't yet received BeforeNavigate
  //
  HWND hhost;               // This is the window that hosts us
  IWebBrowser2 *ibrowser;   // Our pointer to the browser itself. Released in Close().
  DWORD cookie;             // By this cookie shall the watcher be known
  //
  bool hasscrollbars;       // This is read from WS_VSCROLL|WS_HSCROLL at WM_CREATE
  TCHAR *url;               // This was the url that the user just clicked on
  TCHAR *kurl;              // Key\0Value\0Key2\0Value2\0\0 arguments for the url just clicked on
  HANDLE hf; TCHAR fn[MAX_PATH]; // if we're in the middle of a Ready/Set/Go, then these say what we're writing to
  list<tstring> oldfiles;   // temp files that we must delete
};

TWebf::TWebf(HWND hhost)
{ ref=0; clientsite.webf=this; site.webf=this; frame.webf=this; dispatch.webf=this; uihandler.webf=this; showui.webf=this;
  this->hhost=hhost; hf=INVALID_HANDLE_VALUE;
  ibrowser=0; cookie=0; isnaving=0; url=0; kurl=0;
  hasscrollbars = (GetWindowLongPtr(hhost,GWL_STYLE)&(WS_HSCROLL|WS_VSCROLL))!=0;
  RECT rc; GetClientRect(hhost,&rc);
  //
  HRESULT hr; IOleObject* iole=0;
  hr = CoCreateInstance(CLSID_WebBrowser,0,CLSCTX_INPROC_SERVER,IID_IOleObject,(void**)&iole); if (iole==0) return;
  hr = iole->SetClientSite(&clientsite); if (hr!=S_OK) {iole->Release(); return;}
  hr = iole->SetHostNames(L"MyHost",L"MyDoc"); if (hr!=S_OK) {iole->Release(); return;}
  hr = OleSetContainedObject(iole,TRUE); if (hr!=S_OK) {iole->Release(); return;}
  hr = iole->DoVerb(OLEIVERB_SHOW,0,&clientsite,0,hhost, &rc); if (hr!=S_OK) {iole->Release(); return;}
  bool connected=false;
  IConnectionPointContainer *cpc=0; iole->QueryInterface(IID_IConnectionPointContainer,(void**)&cpc);
  if (cpc!=0)
  { IConnectionPoint *cp=0;  cpc->FindConnectionPoint(DIID_DWebBrowserEvents2,&cp);
    if (cp!=0) {cp->Advise(&dispatch,&cookie); cp->Release(); connected=true;}
    cpc->Release();
  }
  if (!connected) {iole->Release(); return;}
  iole->QueryInterface(IID_IWebBrowser2,(void**)&ibrowser); iole->Release();
}


void TWebf::Close()
{ if (ibrowser!=0)
  { IConnectionPointContainer *cpc=0; ibrowser->QueryInterface(IID_IConnectionPointContainer,(void**)&cpc);
    if (cpc!=0)
    { IConnectionPoint *cp=0; cpc->FindConnectionPoint(DIID_DWebBrowserEvents2,&cp);
      if (cp!=0) {cp->Unadvise(cookie); cp->Release();}
      cpc->Release();
    }
    IOleObject *iole=0; ibrowser->QueryInterface(IID_IOleObject,(void**)&iole); ibrowser->Release(); ibrowser=0;
    if (iole!=0) {iole->Close(OLECLOSE_NOSAVE); iole->Release(); }
  }
}

TWebf::~TWebf()
{ DeleteOldFiles();
  if (hf!=INVALID_HANDLE_VALUE) CloseHandle(hf); hf=INVALID_HANDLE_VALUE;
  if (url!=0) delete[] url;
  if (kurl!=0) delete[] kurl;
}

void TWebf::DeleteOldFiles()
{ // The timing of DeleteOldFiles is sensitive. We can't delete
  // a file that is currently being displayed. That's because if
  // it's deleted, then subsequent GET requests to it will fail to
  // populat the GET query. Therefore, we delete old files only
  // at the start of Go() when the old file will surely not be needed.
  list<tstring> newfiles;
  for (list<tstring>::const_iterator i=oldfiles.begin(); i!=oldfiles.end(); i++)
  { tstring tfn = *i;
    BOOL res=DeleteFile(tfn.c_str());
    if (!res) newfiles.push_back(tfn);
  }
  oldfiles=newfiles;
}



void TCharToWide(const char *src,wchar_t *dst,int dst_size_in_wchars)   {MultiByteToWideChar(CP_ACP,MB_PRECOMPOSED,src,-1,dst,dst_size_in_wchars);}
#pragma warning(suppress:4996)
void TCharToWide(const wchar_t *src,wchar_t *dst,int dst_size_in_wchars){wcscpy(dst,src);}
void WideToTChar(const wchar_t *src,char *dst,int dst_size_in_tchars)   {WideCharToMultiByte(CP_ACP,0,src,-1,dst,dst_size_in_tchars,NULL,NULL);}
#pragma warning(suppress:4996)
void WideToTChar(const wchar_t *src,wchar_t *dst,int dst_size_in_tchars){wcscpy(dst,src);}


HANDLE TWebf::Ready()
{ if (hf!=INVALID_HANDLE_VALUE) {OutputDebugString(_T("Webform error: asked for Ready(), but something already is.")); return hf;}
  GetTempPath(MAX_PATH,fn); TCHAR *fnp=fn+_tcslen(fn);
  for (unsigned long t=GetTickCount()/100; hf==INVALID_HANDLE_VALUE; t++)
  { wsprintf(fnp,_T("wbf%lu.html"),(unsigned long)t%100000);
    hf = CreateFile(fn,GENERIC_WRITE,0,0,CREATE_NEW,FILE_ATTRIBUTE_NORMAL|FILE_ATTRIBUTE_TEMPORARY,0);
  }
  return hf;
}

DWORD TWebf::Set(const TCHAR *buf)
{ if (hf==INVALID_HANDLE_VALUE) {OutputDebugString(_T("Webform error: tried to Set, but haven't Ready()ed")); return 0;}
  DWORD writ; WriteFile(hf,buf,(DWORD)(_tcslen(buf)*sizeof(TCHAR)),&writ,0);
  return writ;
}

void TWebf::Go(const TCHAR *url)
{ DeleteOldFiles();
  if (url==0)
  { if (hf==INVALID_HANDLE_VALUE) {OutputDebugString(_T("Webform error: tried to Go, but haven't yet Ready/Set")); return;}
    url=fn;
  }
  if (hf!=INVALID_HANDLE_VALUE) {CloseHandle(hf); hf=INVALID_HANDLE_VALUE; oldfiles.push_back(fn);}
  // Navigate to the new one and delete the old one
  wchar_t ws[MAX_PATH]; TCharToWide(url,ws,MAX_PATH);
  isnaving = 7;
  VARIANT v; v.vt=VT_I4; v.lVal=0; //v.lVal=navNoHistory;
  ibrowser->Navigate(ws,&v,NULL,NULL,NULL);
  // nb. the events know not to bother us for currentlynav.
  // (Special case: maybe it's already loaded by the time we get here!)
  if ((isnaving&2)==0)
  { WPARAM w = (GetWindowLong(hhost,GWL_ID)&0xFFFF) | ((WEBFN_LOADED&0xFFFF)<<16);
    PostMessage(GetParent(hhost),WM_COMMAND,w,(LPARAM)hhost);
  }
  isnaving &= ~4;
  return;
}


void TWebf::DocumentComplete(const wchar_t *)
{ isnaving &= ~2;
  if (isnaving&4) return; // "4" means that we're in the middle of Go(), so the notification will be handled there
  WPARAM w = (GetWindowLong(hhost,GWL_ID)&0xFFFF) | ((WEBFN_LOADED&0xFFFF)<<16);
  PostMessage(GetParent(hhost),WM_COMMAND,w,(LPARAM)hhost);
}



void TWebf::BeforeNavigate2(const wchar_t *cu,short *cancel)
{ int oldisnav=isnaving; isnaving &= ~1; if (oldisnav&1) return; // ignore events that came from our own Go()
  *cancel=TRUE;
  //
  if (url!=0) delete[] url; if (kurl!=0) delete[] kurl;
  int len = (int)wcslen(cu);
  url = new TCHAR[len*2];
  kurl = new TCHAR[len*2];
  WideToTChar(cu,url,len*2);
  // parse into kurl. From http://a.com/b.html?key=val&keyb=valb%3F+t
  // we generate "key\0val\0keyb\0valb! t\0\0"
  const TCHAR *c=url; TCHAR *d=kurl;
  while (*c!=0 && *c!='?') c++; // http://a.com/b.html?key=val&key2=val2&key3=val%3D
  if (*c=='?') c++; // key=val&key2=val2
  if (*c==0) {*d=0; d++;}
  while (*c!=0)
  { while (*c!='=' && *c!=0) {*d=*c; d++; c++;} *d=0; d++;
    if (*c=='=') c++;
    while (*c!=0 && *c!='&')
    { if (*c=='%' && c[1]!=0 && c[2]!=0)
      { int hi=c[1]; if (hi>='0' && hi<='9') hi-='0'; else if (hi>='A' && hi<='F') hi=hi+10-'A'; else if (hi>='a' && hi<='f') hi=hi+10-'a'; else hi=0;
        int lo=c[2]; if (lo>='0' && lo<='9') lo-='0'; else if (lo>='A' && lo<='F') lo=lo+10-'A'; else if (lo>='a' && lo<='f') lo=lo+10-'a'; else lo=0;
        int i=hi*16+lo; *d=(TCHAR)i; if (*d==0) *d='%';
        d++; c+=3;
      }
      else if (*c=='+') {*d=' '; d++; c++;}
      else {*d=*c; d++; c++;}
    }
    if (*c=='&') c++;
    *d=0; d++;
  }
  *d=0;
  //
  WPARAM w = (GetWindowLong(hhost,GWL_ID)&0xFFFF) | ((WEBFN_CLICKED&0xFFFF)<<16);
  PostMessage(GetParent(hhost),WM_COMMAND,w,(LPARAM)hhost);
}

const TCHAR *TWebf::LastClick()
{ return url;
}

const TCHAR *TWebf::GetItem(const TCHAR *key)
{ const TCHAR *c=kurl;
  while (*c!=0)
  { if (_tcscmp(c,key)==0) return c+_tcslen(key)+1;
    while (*c!=0) c++; c++; while (*c!=0) c++; c++;
  }
  return 0;
}

IHTMLDocument2 *TWebf::GetDoc()
{ IDispatch *dispatch=0; ibrowser->get_Document(&dispatch); if (dispatch==0) return 0;
  IHTMLDocument2 *doc;  dispatch->QueryInterface(IID_IHTMLDocument2,(void**)&doc);
  dispatch->Release(); return doc;
}

IHTMLElement *TWebf::GetElement(const TCHAR *id)
{ IHTMLElement *ret=0; if (id==0) return 0;
  IHTMLDocument2 *doc = GetDoc(); if (doc==0) return 0;
  IHTMLElementCollection* doc_all;
  HRESULT hr = doc->get_all(&doc_all);      // this is like doing document.all
  if (hr==S_OK)
  { VARIANT vid; vid.vt=VT_BSTR;
    int len=(int)_tcslen(id);
    wchar_t *ws = new wchar_t[len+1]; TCharToWide(id,ws,len+1);
    vid.bstrVal=ws;
    VARIANT v0; VariantInit(&v0);
    IDispatch* disp;
    hr = doc_all->item(vid,v0,&disp);       // this is like doing document.all["messages"]
    delete[] ws;
    if (hr==S_OK && disp!=0)
    { hr = disp->QueryInterface(IID_IHTMLElement,(void **)&ret); // it's the caller's responsibility to release ret
      disp->Release();
    }
    doc_all->Release();
  }
  doc->Release();
  return ret;
}



LRESULT CALLBACK WebformWndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
{ if (msg==WM_CREATE)
  { TWebf *webf=new TWebf(hwnd); if (webf->ibrowser==0) {delete webf; webf=0;} else webf->AddRef();
    #pragma warning(suppress:4244)
    SetWindowLongPtr(hwnd,GWLP_USERDATA,(LONG_PTR)webf);
    CREATESTRUCT *cs = (CREATESTRUCT*)lParam;
    if (cs->style & (WS_HSCROLL|WS_VSCROLL)) SetWindowLongPtr(hwnd,GWL_STYLE,cs->style & ~(WS_HSCROLL|WS_VSCROLL));
    if (cs->lpszName!=0 && cs->lpszName[0]!=0) webf->Go(cs->lpszName);
    return DefWindowProc(hwnd,msg,wParam,lParam);
  }
  #pragma warning(suppress:4312)
  TWebf *webf = (TWebf*)GetWindowLongPtr(hwnd,GWLP_USERDATA);
  if (webf==0) return DefWindowProc(hwnd,msg,wParam,lParam);
  if (msg==WM_DESTROY) {webf->Close(); webf->Release(); SetWindowLongPtr(hwnd,GWLP_USERDATA,0);}
  //
  switch (msg)
  { case WM_SETTEXT: webf->Go((TCHAR*)lParam); break;
    case WM_SIZE:    webf->ibrowser->put_Width(LOWORD(lParam)); webf->ibrowser->put_Height(HIWORD(lParam)); break;
    case WEBFM_GETLASTCLICK: return (LRESULT)webf->LastClick();
    case WEBFM_READY: return (LRESULT)webf->Ready();
    case WEBFM_GETITEM: return (LRESULT)webf->GetItem((TCHAR*)lParam);
    case WEBFM_GETDOC: return (LRESULT)webf->GetDoc();
    case WEBFM_GETBROWSER: {webf->ibrowser->AddRef(); return (LRESULT)webf->ibrowser;}
    case WEBFM_GETELEMENT: return (LRESULT)webf->GetElement((TCHAR*)lParam);
    case WEBFM_SET: return (LRESULT)webf->Set((TCHAR*)lParam);
  };
  return DefWindowProc(hwnd, msg, wParam, lParam);
}


struct TWebformAutoRegister
{ TWebformAutoRegister()
  { WNDCLASSEX wcex={0}; wcex.cbSize = sizeof(WNDCLASSEX); wcex.style = CS_HREDRAW | CS_VREDRAW;
    wcex.lpfnWndProc = (WNDPROC)WebformWndProc; wcex.hInstance = GetModuleHandle(0);;
    wcex.lpszClassName = WEBFORM_CLASS; RegisterClassEx(&wcex);
  }
} WebformAutoRegister;
Question: The URL http://labs.google.com loads ok but no suggestions appear as you type. Any idea what is needed to make it work?
Answer: One needs to send keystroke messages for the InternetExplorer window within the web browser window to the TranslateAccelerator method of the IOleInPlaceActiveObject object. For sample code, see the comments for the cwebpage article at codeproject.com. Another posting there also shows how to get rid of Javascript error messages.
Question: Some flash contents are not displaying very well, it's messing up with some flash's UI. What can be wrong?
Hello,
 
DevC++, MingW compiler Error (line 104 in webform.h):
 
 C:\Documents and Settings\khan\Desktop\RADC++.EMB Jan 30, 2005\HTML WOW\Dev C++\webform.h In function `T* WebformGetElement(HWND__*, const TCHAR*)':
 
104 C:\Documents and Settings\khan\Desktop\RADC++.EMB Jan 30, 2005\HTML WOW\Dev C++\webform.h expected primary-expression before ')' token
 

I have been looking for such thing since ages, but most of programmers who work with COM always claim win32 compatible code, but that is not.
 
PROBLEM:
 
I cannot compile it with Dev C++ that uses MingW compiler.
 
Will be waiting for your kind response at support@image-host-script.com
 
regards
Ali
Ali, it looks like MingW lacks the "uuidof" operator. So you'll have to alter the prototype of WebformGetElement to take an additional CLSID argument.
 
Have a look here: http://code.google.com/p/flylinkdc/source/browse/trunk/windows/upnp.cpp?r=168
 
Here the function "__initialize_pointers" has the comment "// Lacking the __uuidof in mingw" and so explicitly constructs the CLSID.

Well thanks for the reply Lucian, but problem still persists. That might be because Mingw does not have mshtmhst.h and mshtmdid.h in ./includes.
 
Secondly, if you get some time to ddownload Dev C++ IDE (small in size) and test it, it would be big contribution, becuase almost all MingW users rely on some browser embedding DLL files, and link them explicitely (including me). I am WINAPI developer but never got into COM. I wrote RAD C++ GUI Library, and wanted a way to have Browser class (too) included in it, but no luck because of lack of support in headers available with Mingw.
 
Will be waiting for your response.
 
Ali (support@image-host-script.com)
When a page containing iframes is loaded, each successive iframe is shown but then immediately replaced by the next one. Therefore, when the page has finished loading, only the last iframe is seen on the page. It's as if each iframe is overwriting the previous one.
How can this be fixed?
 
 
Thank you. Its very helped me.
I think I just spotted an error in the downloadable sources. "case DISPID_AMBIENT_DLCONTROL" is missing a "break;", it will always continue to "default:" and return DISP_E_MEMBERNOTFOUND.
 
<p>The web address that loads is also wrong. There's a capital "P" in programmer that shouldn't be there.</p>
 
<p>And the timer is set to 200ms, but the comment says it should be 20s, so the code should be:<br> <code>SetTimer(hwnd, 1, 20000, 0);</code></p>
Lucian, this is great. Nice work and many thanks.
thanks
!
Very useful library! thank you alot!
add comment  edit commentPlease add comments here