A drop-in replacement of vbRichClient6's cWebView2 class, implemented in pure VB6
directly against the official Microsoft WebView2 COM API. No RC6.dll dependency,
no dependency on the WebView2 SDK typelib — the only redistributable is
WebView2Loader.dll, located at runtime in the exe's External subfolder, next
to the exe, or in any parent folder, targeting the Evergreen WebView2 Runtime
installed on the machine.
| Path | Purpose |
|---|---|
src/cWebView2.cls |
The public class — RC6-compatible properties, methods and events (~120KB compiled) |
src/cWebView2Callback.cls |
Implements every native callback interface (completed-handlers, event handlers, environment options) and forwards to cWebView2's Friend sinks |
typelib/VBWebView2Impl.odl / .tlb |
Hand-written companion type library with VB6-safe redeclarations of the WebView2 interfaces (see below) |
WebView2Loader.dll |
Microsoft's loader shim (x86), resolves the installed Evergreen runtime |
test/Form1.frm |
Manual test harness, grown alongside the implementation |
test/Project1.vbp |
Test exe project — references only stdole2.tlb and VBWebView2Impl.tlb |
contrib/dll/VbWebView2.vbp |
ActiveX DLL project packaging the classes as VbWebView2.dll (binary compatibility kept via VbWebView2.cmp) |
contrib/dll/test/Project1.vbp |
Test exe consuming the compiled VbWebView2.dll |
doc/WebView2.idl |
The official WebView2 SDK IDL, kept as read-only reference only |
doc/RC6.idl |
vbRichClient6 IDL, kept as API-surface reference only |
VB6 cannot Implements (or in places even call) the WebView2 SDK interfaces
because they use automation-hostile types: raw BOOL, LPWSTR/LPCWSTR,
unsigned ints, and even plain IUnknown* parameters. VBWebView2Impl.odl
redeclares every interface the code touches under an IVB... name with the
exact same IID as the SDK original — so QueryInterface and vtable calls
to/from native code resolve correctly — but with VB6-safe types:
BOOL→long(0 = False, nonzero = True)LPWSTR/LPCWSTR→long(raw pointer, converted withStrPtr/SysReAllocStringhelpers; strings the API documents as caller-owned are freed withCoTaskMemFree)UINT32/UINT64/enums/HRESULTparams →long- interface pointers not otherwise needed →
VB6IUnknown, a locally-declared marker interface binary-identical toIUnknown(VB6 rejects the imported stdoleIUnknownas a parameter type inImplementsmembers) - interface version chains (
ICoreWebView2_2..11,ICoreWebView2Settings2..6,ICoreWebView2Controllerunchanged) are flattened onto their base vtable and published under the newest IID actually needed - unused members that precede needed ones are still declared, purely to keep vtable slots aligned
- every interface is forward-declared once near the top of the file (
mktyplibsupports bareinterface IFoo;declarations), so any interface can reference any other regardless of which one is fully defined first further down
The tlb is compiled with MKTYPLIB.EXE VBWebView2Impl.odl (VC98).
RC6's synchronous call style (BindTo, Navigate, jsRun, ... block until done
or timeout) is reproduced by spinning the thread message pump
(PeekMessage/DispatchMessage + CoWaitForMultipleHandles) until the native
async completion fires. Completions are correlated by Currency tokens so
nested and interleaved calls cannot cross wires.
jsRun/jsProp/jsCallByName run over an RC6-style SetFuncObj bridge: an
injected script hands a JS function table ({run, cbn, propGet, propLet}) to the
host as a live IDispatch proxy; the wrapper resolves and calls those with raw
GetIDsOfNames/Invoke (via IVBDispatch, LCID LOCALE_USER_DEFAULT — JS
scripting proxies reject LCID 0), and the JS glue reports the result back through
a nested RaiseResultEvent. That result — including live JS object proxies,
not the JSON copies ExecuteScript yields — is what makes jsCallByName work.
The nested RaiseResultEvent is not dispatched reentrantly (WebView2 queues
the incoming host-object call), so the bridge pumps the message loop for it, with
the jsCallTimeOutSeconds timeout.
Because WebView2 serialises callbacks — no callback (including the bridge's
result) is delivered while another is on our stack — a blocking jsRun from
inside an event handler would otherwise deadlock. So notification events
(NavigationCompleted, DocumentComplete, TitleChanged, DOMContentLoaded,
JSMessage, ...) are deferred: their args are captured synchronously and the
event is raised one message-loop tick later, from a fire-once timer, on a clean
stack where jsRun/jsProp/jsCallByName work. This matches RC6, where
NavigationCompleted/DocumentComplete likewise fire after Navigate returns
(the internal done-flag stays synchronous, so blocking Navigate is unaffected).
Cancelable/answerable events (NavigationStarting, PermissionRequested,
ScriptDialogOpening, NewWindowRequested, AcceleratorKeyPressed,
WebResourceRequested, ContextMenuRequested, DownloadStarting,
MoveFocusRequested, BasicAuthenticationRequested) are raised inline because
the native side reads their ByRef answer back — jsRun cannot be used in those
(nor can it in RC6), use jsRunAsync there instead.
- The instance registers its
cWebView2Callbackas host objectvbHostand injects a globalvbH()accessor on document creation, so existing RC6-style page script likevbH().RaiseMessageEvent('title_change', document.title)works unchanged. The callback only forwards a handful of methods (RaiseMessageEvent,RaiseContextMenuEvent) to the owningcWebView2— script never obtains a direct reference to thecWebView2instance itself. Acontextmenulistener is injected the same way to raiseUserContextMenu. - Default user data folder is
%LOCALAPPDATA%\<EXENAME>, matching RC6, instead of the native default<exe>.WebView2next to the executable. JSMessagefires from both the RC6 channel (vbH().RaiseMessageEvent) and nativechrome.webview.postMessage; JSON object content is decoded into theoJSONContentparameter.AddObject "Name", Objalso publishes a globalwindow.Namealias of the synchronous host-object proxy, so page script can callName.MethodName(...)directly, like with RC6. The nativechrome.webview.hostObjects[.sync].Nameforms remain available (the async proxy returns Promises).- Return values verified against live RC6:
BindToreturns1on success and0on failure (details go to the debug console);Navigate/NavigateToString/NavigateWithWebResourceRequestreturn1whenever the navigation completed within the wait — even when it landed on an error page — andEmptyon timeout.
CapturePreviewreturns aByte()array with the raw PNG/JPEG data (RC6 returned a Cairo surface).WebResourceRequestedis a simplified allow/deny gate — deny answers the request with a synthesized403, no response-body rewriting.GotFocus/LostFocusReasonis a best-effort heuristic (primed bySetFocus, defaults toFocusReason_PROGRAMMATIC).CallDevToolsProtocolMethodblocks and returns the CDP response, decoded likejsRun's result, instead of RC6's fire-and-forget signature (RC6's own IDL has no retval either, but discarding the CDP response is rarely useful).BindTo'sAllowSingleSignOnUsingOSPrimaryAccountisBoolean, not RC6'sLong.BindTohas four trailing optional parameters beyond RC6's signature —ExclusiveUserDataFolderAccess,IsCustomCrashReportingEnabled,EnableTrackingPrevention(defaultTrue, matching WebView2's own default) andAreBrowserExtensionsEnabled— surfacingICoreWebView2EnvironmentOptions2/3/5/6.Options4's custom scheme registration is not exposed (no RC6 counterpart, different shape of feature entirely — an array of registration objects, not a simple flag).
MKTYPLIB.EXE typelib\VBWebView2Impl.odlwhenever the odl changes.- Build
test\Project1.vbpwith VB6 (VB6.EXE /make). - Ship
WebView2Loader.dllwith the compiled exe — it is located at runtime in the exe'sExternalsubfolder, next to the exe, or in any parent folder. The Evergreen WebView2 Runtime must be installed on the target machine (preinstalled on Windows 11).
All RC6 cWebView2 properties, methods and events are implemented and verified
against live pages (navigation, JS interop incl. blocking/async calls, live JS
object proxies and JSON marshaling, host objects, web messages, settings, script
dialogs, permissions, new-window, accelerator keys, focus, web-resource
filtering, response introspection, frames, downloads, capture). Notes:
jsRun/jsProp/jsCallByNamerun over theSetFuncObjbridge (see How it works) and return live JS object proxies, sojsCallByNamedispatches on objects obtained fromjsRun/jsPropexactly as in RC6. Like RC6,jsProp("window")returnsEmpty(WebView2 won't marshal the globalwindowobject) — expose a named object instead; nested objects (jsProp("window.x")) come back live and callable.jsPropreads dotted property paths only; arbitrary expressions fall back toExecuteScriptevaluation.jsRun/jsProp/jsCallByNamework inside deferred event handlers (see How it works) but not inside cancelable events (NavigationStarting, etc.) — usejsRunAsyncthere, same as RC6.GetMostRecentInstallPath— resolves the runtime version through the loader, fills theVersionStringout-param and returns%ProgramFiles(x86)%\Microsoft\<EdgeChannel>\Application\<version>when that folder exists (matching live RC6 output), empty string otherwise.
Methods with no RC6 counterpart, exposed because the native plumbing is cheap and the features are frequently requested:
PrintToPdf(ResultFilePath, [SecondsToWaitForPrintComplete], [Landscape], [ScaleFactor], [PageWidth/PageHeight], [margins], [ShouldPrintBackgrounds], [ShouldPrintSelectionOnly], [ShouldPrintHeaderAndFooter], [HeaderTitle], [FooterUri])— blocking (or fire-and-forget at 0 seconds), returnsTrue/False,Emptyon timeout. Settings rideICoreWebView2Environment6.CreatePrintSettings; the call itself uses thePrintToPdfslot already present in the_11flatten.AddBrowserExtension(ExtensionFolderPath)— installs an unpacked extension into the profile, returns the extension Id (empty string on failure). RequiresBindTo'sAreBrowserExtensionsEnabled:=True(a fourth RC6-surplus optional parameter, surfacingICoreWebView2EnvironmentOptions6— must be decided before environment creation, and allcWebView2instances sharing a user data folder must agree on it).GetBrowserExtensions()— returns aCollectionof per-extensionCollections (Id,Name,IsEnabledkeys), keyed by extension Id. Extension APIs need a recent Evergreen runtime; on older runtimes the internalProfile7QI fails soft and these return empty results.SetVirtualHostNameToFolderMapping(HostName, FolderPath, [AccessKind])/ClearVirtualHostNameToFolderMapping(HostName)— serve a local folder ashttps://<HostName>/....AccessKinddefaults toHostResourceAccess_DENY(the mapped origin still serves its own content; other origins can't read it — Microsoft's recommended default). Mappings persist for the browser instance's lifetime: set once afterBindTo, thenNavigate "https://<HostName>/...". The host name must be a bare name — no scheme or slashes.Stop_— cancels in-flight navigation/loading,WebBrowser.Stopstyle (trailing underscore becauseStopis a VB6 keyword).PostWebMessageAsString/PostWebMessageAsJson— host-to-page messaging; page script receives viachrome.webview.addEventListener('message', ...).ContainsFullScreenElement— detect HTML fullscreen (resize/borderless the host form).IsBuiltInErrorPageEnabled— the one base setting RC6 didn't surface.IsMuted(get/let),IsDocumentPlayingAudio— tab audio control.- Default download dialog control —
OpenDefaultDownloadDialog/CloseDefaultDownloadDialog/IsDefaultDownloadDialogOpen,DefaultDownloadDialogCornerAlignmentandDefaultDownloadDialogMargin. - Profile properties —
PreferredColorScheme(dark mode),DefaultDownloadFolderPath,PreferredTrackingPreventionLevel,ProfileName,ProfilePath,IsInPrivateModeEnabled. StatusBarText,BrowserVersion— runtime/status introspection;OpenTaskManagerWindow— browser diagnostics.CallDevToolsProtocolMethodForSession— CDP against a specific target session, same blocking semantics asCallDevToolsProtocolMethod.SetBoundsAndZoomFactor,NotifyParentWindowPositionChanged(also called internally on resize/move),HosthWndis now writable (re-hosting).TrySuspend/ResumeFromSuspend/IsSuspended— release memory while in the background;TrySuspendhides the webview first (a native precondition),ResumeFromSuspendmakes it visible again.ClearBrowsingData(DataKinds),ClearBrowsingDataInTimeRange(DataKinds, StartTime, EndTime),ClearBrowsingDataAll— clear cache/cookies/history etc pereWebView2BrowsingDataKindsflags; blocking with the usual fire-and-forget at 0 seconds.- Cookie management —
GetCookies([URI])returns aCollectionof per-cookieCollections (Name,Value,Domain,Path,IsSession,Expires,IsSecure,IsHttpOnly,SameSitekeys);AddOrUpdateCookie(Name, Value, Domain, [Path], [Expires], ...)(zeroExpires= session cookie);DeleteCookies(Name, URI)(both required — cookies are matched by name and URI natively);DeleteAllCookies. PrintToPdfStream([settings...])— same settings asPrintToPdfbut returns the PDF as aByte()array, no file involved.ShowPrintUIopens the browser or system print dialog.
A handful of native WebView2/WebBrowser-style events with no RC6 counterpart
are also exposed, since the native plumbing is either free (already needed for
something else) or fills a gap the JS-bridge-based events can't:
ContentLoading(IsErrorPage)— fires beforeNavigationCompleted, akin toWebBrowser's early loading moment.HistoryChanged()— back/forward stack changed; pair withCanGoBack/CanGoForward.WindowCloseRequested()— page calledwindow.close().ZoomFactorChanged()— native counterpart to theZoomFactorproperty.MoveFocusRequested(Reason, Handled)— Tab/Shift+Tab reached the edge of the web content;ReasonreuseseWebView2FocusReason.DOMContentLoaded()— the classic DOM-ready moment, betweenContentLoadingandDocumentComplete.StatusBarTextChanged(Text)— hover-link/status text, pairs with theStatusBarTextproperty.ContainsFullScreenElementChanged()— page entered/left HTML fullscreen; readContainsFullScreenElementto react.BasicAuthenticationRequested(URI, Challenge, UserName, Password, Cancel)— supply credentials for HTTP basic/proxy auth via theByRefparams. Note Chromium's flow: the challenged navigation first completes withWebErrorStatus17 (VALID_AUTHENTICATION_CREDENTIALS_REQUIRED), then the authenticated retry loads.ContextMenuRequested(PageURI, LinkURI, SelectionText, ScreenX, ScreenY, Handled)— the native context-menu event, richer than the JS-injectedUserContextMenu(which only reports coordinates): carries link/selection info and can actually suppress the browser's context menu viaHandled. Deliberately simplified — no custom menu item injection/CustomItemSelected, matching theWebResourceRequestedsimplified-gate precedent.UserContextMenuis unchanged and still fires alongside it.
ContextMenuRequested required flattening ICoreWebView2_5.._11 (only
_11's ContextMenuRequested is used; the rest are unused placeholders kept
solely to preserve vtable alignment), same technique as the earlier _4 flatten.