From 1ecb389dd4afd41d3d6542e725ae3dc5b9a1b65b Mon Sep 17 00:00:00 2001 From: Kuba Sunderland-Ober Date: Wed, 13 May 2026 19:08:39 +0200 Subject: [PATCH 1/2] Extend WebView2 docs with wisdom from bundled tB code samples. --- docs/Reference/WebView2/WebView2/index.md | 132 ++++++++++++++ .../WebView2/Building a browser shell.md | 128 +++++++++++++ docs/Tutorials/WebView2/Driving Monaco.md | 137 ++++++++++++++ .../WebView2/Hosting local web assets.md | 128 +++++++++++++ .../WebView2/Images/MonacoArchitecture.svg | 1 + docs/Tutorials/WebView2/JavaScript interop.md | 169 ++++++++++++++++++ .../WebView2/_Images/MonacoArchitecture.md | 11 ++ docs/Tutorials/WebView2/index.md | 17 +- 8 files changed, 722 insertions(+), 1 deletion(-) create mode 100644 docs/Tutorials/WebView2/Building a browser shell.md create mode 100644 docs/Tutorials/WebView2/Driving Monaco.md create mode 100644 docs/Tutorials/WebView2/Hosting local web assets.md create mode 100644 docs/Tutorials/WebView2/Images/MonacoArchitecture.svg create mode 100644 docs/Tutorials/WebView2/JavaScript interop.md create mode 100644 docs/Tutorials/WebView2/_Images/MonacoArchitecture.md diff --git a/docs/Reference/WebView2/WebView2/index.md b/docs/Reference/WebView2/WebView2/index.md index 5472c868..3255e76e 100644 --- a/docs/Reference/WebView2/WebView2/index.md +++ b/docs/Reference/WebView2/WebView2/index.md @@ -381,6 +381,14 @@ The control's width. **Single**. Inherited. The current zoom factor — `1.0` is 100%, `1.5` is 150%, and so on. **Double**. The design-time default is `0`, which means "do not override Edge's default of 1.0". +> [!NOTE] +> Because the design-time default is `0`, not `1.0`, arithmetic that multiplies the current value silently starts from zero unless the host clamps it to `1` first: +> +> ```tb +> If WebView21.ZoomFactor = 0 Then WebView21.ZoomFactor = 1 +> WebView21.ZoomFactor *= 1.1 ' 110% on first click, 121% on second, … +> ``` + Methods ------- @@ -400,6 +408,27 @@ Syntax: *object*.**AddObject** *ObjName*, *Object* [, *UseDeferredInvoke* ] *UseDeferredInvoke* : *optional* A **Boolean**, default **False**. When **True**, calls from the page are deferred onto the BASIC message-loop — safe to re-enter the WebView2 control from within them, but the page cannot read a return value back. Use **False** when the page needs to read return values. +```tb +Private Sub WebView21_Ready() + WebView21.AddObject "myCalculator", New MyCalculator +End Sub + +Class MyCalculator + Public Function MultiplyByTen(ByVal Value As Long) As Long + Return Value * 10 + End Function +End Class +``` + +```js +async function callHostCalculator() { + let result = await chrome.webview.hostObjects.myCalculator.MultiplyByTen(7); + alert("BASIC returned: " + result); // -> 70 +} +``` + +Calls into the host object are asynchronous on the JavaScript side and must be `await`-ed inside an `async` function — even when *UseDeferredInvoke* is **False**. See the [Re-entrancy](../../../../Tutorials/WebView2/Re-entrancy) tutorial for when to pass **True**. + ### AddScriptToExecuteOnDocumentCreated {: .no_toc } @@ -516,6 +545,12 @@ Syntax: *object*.**JsRun** ( *FuncName*, [ *args* ] ) **As Variant** *args* : *optional* Any number of **Variant** arguments. Each is JSON-encoded before being passed to the function. Strings, numerics, **Boolean**, **Null**, and **Empty** are supported. +```tb +' Calls the page-side function `multiplyTheseNumbers(a, b)` and waits for the result. +Dim product As Long = WebView21.JsRun("multiplyTheseNumbers", 5, 6) +Debug.Print product ' 30 +``` + ### JsRunAsync {: .no_toc } @@ -529,6 +564,21 @@ Syntax: *object*.**JsRunAsync** ( *FuncName*, [ *args* ] ) **As LongLong** *args* : *optional* Any number of **Variant** arguments, JSON-encoded as in [**JsRun**](#jsrun). +```tb +Private Sub btnRun_Click() + WebView21.JsRunAsync "multiplyTheseNumbers", 5, 6 +End Sub + +Private Sub WebView21_JsAsyncResult( _ + ByVal Result As Variant, Token As LongLong, ErrString As String) + If LenB(ErrString) = 0 Then + Debug.Print "Async result: "; Result + Else + Debug.Print "Async error: "; ErrString + End If +End Sub +``` + ### Move {: .no_toc } @@ -553,6 +603,18 @@ Syntax: *object*.**Navigate** *uri* *uri* : *required* A **String** URI such as `"https://www.twinbasic.com"` or `"file:///C:/page.html"`. +```tb +Private Sub AddressBar_KeyDown(KeyCode As Integer, Shift As Integer) + If KeyCode = vbKeyReturn Then WebView21.Navigate AddressBar.Text +End Sub + +Private Sub WebView21_NavigationComplete( _ + ByVal IsSuccess As Boolean, ByVal WebErrorStatus As Long) + btnBack.Enabled = WebView21.CanGoBack + btnForward.Enabled = WebView21.CanGoForward +End Sub +``` + ### NavigateCustom {: .no_toc } @@ -587,6 +649,10 @@ Syntax: *object*.**NavigateToString** *htmlContent* *htmlContent* : *required* A **String** of HTML source. +```tb +WebView21.NavigateToString "

Hello, world!

" +``` + ### OpenDefaultDownloadDialog {: .no_toc } @@ -624,6 +690,21 @@ Syntax: *object*.**PostWebMessage** *Message* Requires [**IsWebMessageEnabled**](#iswebmessageenabled). +```tb +WebView21.PostWebMessage "Hello from twinBASIC!" + +Private Sub WebView21_JsMessage(ByVal Message As Variant) + Debug.Print "Reply from page: "; Message +End Sub +``` + +```js +window.chrome.webview.addEventListener('message', (e) => { + alert("Host sent: " + e.data); + window.chrome.webview.postMessage("Thanks, twinBASIC!"); +}); +``` + ### PostWebMessageJSON {: .no_toc } @@ -664,6 +745,16 @@ Syntax: *object*.**PrintToPdf** *outputPath* [, *Orientation* [, *ScaleFactor* [ *HeaderTitle*, *FooterUri* : *optional* **String**s overriding the default header title and footer URI. +```tb +Private Sub btnSave_Click() + WebView21.PrintToPdf Environ$("USERPROFILE") & "\Documents\page.pdf" +End Sub + +Private Sub WebView21_PrintToPdfCompleted() + MsgBox "PDF saved.", vbInformation +End Sub +``` + ### Reload {: .no_toc } @@ -731,6 +822,17 @@ Syntax: *object*.**SetVirtualHostNameToFolderMapping** *hostName*, *folderPath* Requires [**SupportsFolderMappingFeatures**](#supportsfoldermappingfeatures). +```tb +Private Sub WebView21_Ready() + Dim folderPath As String = Environ$("USERPROFILE") & "\Documents\MyApp" + WebView21.SetVirtualHostNameToFolderMapping _ + "myapp.example", folderPath & "\", wv2ResourceAllow + WebView21.Navigate "https://myapp.example/index.html" +End Sub +``` + +See the [Hosting local web assets](../../../../Tutorials/WebView2/Hosting-Local-Web-Assets) tutorial for the matching pattern of bundling the assets through the project's `Resources` folder. + ### Suspend {: .no_toc } @@ -801,6 +903,22 @@ Raised when the WebView2 environment or controller fails to initialise — most Syntax: *object*\_**Error**( *code* **As Long**, *msg* **As String** ) +> [!NOTE] +> Code `&H80070002` (`ERROR_FILE_NOT_FOUND`) is the canonical signal that the WebView2 Evergreen runtime is missing from the machine — the right cue to prompt the user to install it. + +```tb +Private Sub WebView21_Error(ByVal code As Long, ByVal msg As String) + Const ERROR_FILE_NOT_FOUND As Long = &H80070002 + If code = ERROR_FILE_NOT_FOUND Then + MsgBox "The WebView2 (Evergreen) runtime is not installed on this machine.", _ + vbExclamation, "WebView2" + Else + MsgBox "WebView2 error " & Hex$(code) & ": " & msg, _ + vbExclamation, "WebView2" + End If +End Sub +``` + ### JsAsyncResult {: .no_toc } @@ -829,6 +947,20 @@ Raised before each navigation begins. Set *Cancel* to **True** to block the navi Syntax: *object*\_**NavigationStarting**( *Uri* **As String**, *IsUserInitiated* **As Boolean**, *IsRedirected* **As Boolean**, *RequestHeaders* **As** [**WebView2RequestHeaders**](../WebView2RequestHeaders), *Cancel* **As Boolean** ) +```tb +' Block any navigation to a URL outside our own virtual host. +Private Sub WebView21_NavigationStarting( _ + ByVal Uri As String, ByVal IsUserInitiated As Boolean, _ + ByVal IsRedirected As Boolean, _ + ByVal RequestHeaders As WebView2RequestHeaders, _ + Cancel As Boolean) + If Not (Uri Like "https://myapp.example/*" Or Uri = "about:blank") Then + MsgBox "External link blocked: " & Uri + Cancel = True + End If +End Sub +``` + ### NewWindowRequested {: .no_toc } diff --git a/docs/Tutorials/WebView2/Building a browser shell.md b/docs/Tutorials/WebView2/Building a browser shell.md new file mode 100644 index 00000000..a1d6bea5 --- /dev/null +++ b/docs/Tutorials/WebView2/Building a browser shell.md @@ -0,0 +1,128 @@ +--- +title: Building a browser shell +parent: WebView2 +grand_parent: Tutorials +nav_order: 4 +permalink: /Tutorials/WebView2/Building-A-Browser-Shell +--- + +# Building a browser shell + +A short worked tutorial: turn a [**WebView2**](../../tB/Packages/WebView2/WebView2/) control into a working browser with an address bar, back / forward / reload buttons, zoom, and a few helpers (DevTools, Task Manager, PDF export). + +The complete project ships as *Sample 0 — WebView2 Examples* in the New-Project dialog (form *Example 1*). This tutorial walks through its key pieces. + +## The form + +Drop a [**WebView2**](../../tB/Packages/WebView2/WebView2/) control onto a Form and rename it `WebView`. Around it, add a `TextBox` named `AddressBar` plus seven `CommandButton`s — `btnBack`, `btnForward`, `btnRefresh`, `btnZoomIn`, `btnZoomOut`, `btnPDF`, `btnDevTools`, `btnTaskMgr`. + +## Navigating + +The bare-bones navigation surface — [**Navigate**](../../tB/Packages/WebView2/WebView2/#navigate), [**GoBack**](../../tB/Packages/WebView2/WebView2/#goback), [**GoForward**](../../tB/Packages/WebView2/WebView2/#goforward), [**Reload**](../../tB/Packages/WebView2/WebView2/#reload) — is one-liners: + +```tb +Private Sub btnBack_Click() Handles btnBack.Click + WebView.GoBack() +End Sub + +Private Sub btnForward_Click() Handles btnForward.Click + WebView.GoForward() +End Sub + +Private Sub btnRefresh_Click() Handles btnRefresh.Click + WebView.Reload() +End Sub +``` + +To make the back / forward buttons follow the actual history state, sync them against [**CanGoBack**](../../tB/Packages/WebView2/WebView2/#cangoback) and [**CanGoForward**](../../tB/Packages/WebView2/WebView2/#cangoforward) after every navigation: + +```tb +Private Sub WebView_NavigationComplete( _ + ByVal IsSuccess As Boolean, ByVal WebErrorStatus As Long) _ + Handles WebView.NavigationComplete + btnBack.Enabled = WebView.CanGoBack + btnForward.Enabled = WebView.CanGoForward +End Sub +``` + +## The address bar + +Pressing **Enter** in the address bar triggers a navigation. The reverse direction — keeping the visible URL in sync with the page — is the [**SourceChanged**](../../tB/Packages/WebView2/WebView2/#sourcechanged) event, which fires whenever [**DocumentURL**](../../tB/Packages/WebView2/WebView2/#documenturl) changes (including same-document `history.pushState` updates): + +```tb +Private Sub AddressBar_KeyDown(KeyCode As Integer, Shift As Integer) _ + Handles AddressBar.KeyDown + If KeyCode = vbKeyReturn Then WebView.Navigate AddressBar.Text +End Sub + +Private Sub WebView_SourceChanged(ByVal IsNewDocument As Boolean) _ + Handles WebView.SourceChanged + AddressBar.Text = WebView.DocumentURL +End Sub +``` + +[**Navigate**](../../tB/Packages/WebView2/WebView2/#navigate) accepts any URI string; if the scheme prefix is missing, `https://` is added automatically. + +## Zoom + +[**ZoomFactor**](../../tB/Packages/WebView2/WebView2/#zoomfactor) is a **Double** — `1.0` is 100%, `1.5` is 150%. The design-time default is `0`, meaning *"don't override Edge's default of 1.0"* — so multiplying by `1.1` from cold gives `0`, not `1.1`. Clamp to `1` before scaling: + +```tb +Private Sub btnZoomIn_Click() Handles btnZoomIn.Click + If WebView.ZoomFactor = 0 Then WebView.ZoomFactor = 1 + WebView.ZoomFactor *= 1.1 +End Sub + +Private Sub btnZoomOut_Click() Handles btnZoomOut.Click + If WebView.ZoomFactor = 0 Then WebView.ZoomFactor = 1 + WebView.ZoomFactor /= 1.1 +End Sub +``` + +## PDF export + +[**PrintToPdf**](../../tB/Packages/WebView2/WebView2/#printtopdf) saves the current document to disk asynchronously — the result surfaces as [**PrintToPdfCompleted**](../../tB/Packages/WebView2/WebView2/#printtopdfcompleted) or [**PrintToPdfFailed**](../../tB/Packages/WebView2/WebView2/#printtopdffailed): + +```tb +Private Sub btnPDF_Click() Handles btnPDF.Click + Dim outputPath As String = _ + Environ$("USERPROFILE") & "\Documents\page.pdf" + WebView.PrintToPdf(outputPath) +End Sub + +Private Sub WebView_PrintToPdfCompleted() Handles WebView.PrintToPdfCompleted + MsgBox "PDF saved.", vbInformation +End Sub +``` + +## DevTools and Task Manager + +Both windows are one-shot — call the matching method and Edge opens the window in its own process: + +```tb +Private Sub btnDevTools_Click() Handles btnDevTools.Click + WebView.OpenDevToolsWindow() +End Sub + +Private Sub btnTaskMgr_Click() Handles btnTaskMgr.Click + WebView.OpenTaskManagerWindow() +End Sub +``` + +[**OpenDevToolsWindow**](../../tB/Packages/WebView2/WebView2/#opendevtoolswindow) works even when [**AreDevToolsEnabled**](../../tB/Packages/WebView2/WebView2/#aredevtoolsenabled) is **False** (that setting only disables the user-initiated path — keyboard shortcut and context menu). + +## Form-title sync + +To make the host window's caption track the page's ``, listen for [**DocumentTitleChanged**](../../tB/Packages/WebView2/WebView2/#documenttitlechanged) and read [**DocumentTitle**](../../tB/Packages/WebView2/WebView2/#documenttitle): + +```tb +Private Sub WebView_DocumentTitleChanged() Handles WebView.DocumentTitleChanged + Me.Caption = WebView.DocumentTitle +End Sub +``` + +## Where next + +- [Hosting local web assets](Hosting-Local-Web-Assets) — serve HTML / JS / CSS from a folder without an HTTP server. +- [JavaScript interop](JavaScript-Interop) — pass values and method calls between BASIC and the page. +- [WebView2 reference](../../tB/Packages/WebView2/WebView2/) — every property, method, and event. diff --git a/docs/Tutorials/WebView2/Driving Monaco.md b/docs/Tutorials/WebView2/Driving Monaco.md new file mode 100644 index 00000000..53ca6552 --- /dev/null +++ b/docs/Tutorials/WebView2/Driving Monaco.md @@ -0,0 +1,137 @@ +--- +title: Driving Monaco from twinBASIC +parent: WebView2 +grand_parent: Tutorials +nav_order: 7 +permalink: /Tutorials/WebView2/Driving-Monaco +--- + +# Driving Monaco from twinBASIC + +A case study combining everything from the previous tutorials: a form with **two** [**WebView2**](../../tB/Packages/WebView2/WebView2/) controls — the Microsoft Monaco editor on the left, a live HTML preview on the right. As the user types, Monaco posts the edited source to twinBASIC, which mirrors it into the preview pane. + +The complete project ships as *Sample 0 — WebView2 Examples* in the New-Project dialog (form *Example 3*). + +## Architecture + +![](Images/MonacoArchitecture.svg) + +The editor runs as a local web app under a virtual hostname; the preview pane is fed raw HTML through [**NavigateToString**](../../tB/Packages/WebView2/WebView2/#navigatetostring). + +## Setting up the editor's assets + +The Monaco editor ships as a ~2 MB collection of JavaScript, CSS, and font files. Drop them into a `Resources` sub-folder of your project — call it `MONACO_DEMO` — alongside an `index.html` and a small bootstrap `script.js`. The [Hosting local web assets](Hosting-Local-Web-Assets) tutorial describes the layout. + +The page itself is a single `<div id='container'>` plus the bootstrap script that listens for an *initial-content* message from the host: + +```html +<!DOCTYPE html> +<html> + <head> + <script src="/vs/loader.js"></script> + <script src="/script.js"></script> + <link rel="stylesheet" href="/styles.css"> + </head> + <body> + <div id="container"></div> + </body> +</html> +``` + +```js +window.chrome.webview.addEventListener('message', (event) => { + let initialHTML = event.data; + + require.config({ paths: { 'vs': 'https://monaco.example/vs' } }); + require(["vs/editor/editor.main"], () => { + let editor = monaco.editor.create(document.getElementById('container'), { + value: initialHTML, + language: 'html', + theme: 'vs-dark', + minimap: { enabled: false } + }); + + editor.onDidChangeModelContent(() => { + // Inform the host of every edit. + window.chrome.webview.postMessage(editor.getValue()); + }); + }); +}); +``` + +## The BASIC side + +Drop two `WebView2` controls on a form — `WebView` (the editor) and `WebViewPreview` (the renderer). The `Ready` handler deploys the assets, registers the virtual host, and navigates: + +```tb +Private localPath As String + +Private Sub WebView_Ready() Handles WebView.Ready + localPath = Environ$("USERPROFILE") & "\Documents\tbMonacoDemo" + CopyResourcesFolderContentsToLocalPath "MONACO_DEMO", localPath + + WebView.SetVirtualHostNameToFolderMapping _ + "monaco.example", localPath & "\", wv2ResourceAllow + WebView.Navigate "https://monaco.example/index.html" +End Sub +``` + +(`CopyResourcesFolderContentsToLocalPath` is the helper from [Hosting local web assets](Hosting-Local-Web-Assets).) + +## Pushing the initial content + +Once Monaco has finished loading, the bootstrap script listens for a `message` event carrying the HTML to seed the editor with. Fire that message after the editor's [**NavigationComplete**](../../tB/Packages/WebView2/WebView2/#navigationcomplete): + +```tb +Private Sub WebView_NavigationComplete( _ + ByVal IsSuccess As Boolean, ByVal WebErrorStatus As Long) _ + Handles WebView.NavigationComplete + + Dim initialHTML As String = _ + StrConv(LoadResData("initial-editor-html.html", "MONACO_DEMO"), vbFromUTF8) + + WebView.PostWebMessage(initialHTML) + WebViewPreview.NavigateToString(initialHTML) +End Sub +``` + +[**LoadResData**](../../tB/Packages/VB/Global/#loadresdata) returns the resource bytes; `StrConv(..., vbFromUTF8)` decodes them. [**PostWebMessage**](../../tB/Packages/WebView2/WebView2/#postwebmessage) hands the string to Monaco's `message` listener; [**NavigateToString**](../../tB/Packages/WebView2/WebView2/#navigatetostring) seeds the preview pane with the same text rendered as HTML. + +## Live preview + +Every keystroke in Monaco fires its `onDidChangeModelContent` callback, which `postMessage`s the new content back to BASIC. That arrives as the [**JsMessage**](../../tB/Packages/WebView2/WebView2/#jsmessage) event — feed it straight into the preview: + +```tb +Private Sub WebView_JsMessage(ByVal Message As Variant) Handles WebView.JsMessage + WebViewPreview.NavigateToString(Message) +End Sub +``` + +That's it — the preview pane re-renders on every edit. + +## Detecting a missing Edge runtime + +A reasonable fraction of users will run the application on a machine where the WebView2 Evergreen runtime isn't installed. The [**Error**](../../tB/Packages/WebView2/WebView2/#error) event surfaces this case as Win32 error code `&H80070002` (`ERROR_FILE_NOT_FOUND`): + +```tb +Private Sub WebView_Error(ByVal code As Long, ByVal msg As String) _ + Handles WebView.Error + Const ERROR_FILE_NOT_FOUND As Long = &H80070002 + If code = ERROR_FILE_NOT_FOUND Then + MsgBox "Failed to initialize the WebView2 control." & vbCrLf & _ + "Please install the WebView2 (Evergreen) runtime.", _ + vbExclamation, "WebView2" + Else + MsgBox "WebView2 error " & Hex$(code) & ": " & msg, _ + vbExclamation, "WebView2" + End If +End Sub +``` + +It is worth handling this even in single-WebView applications — the message you show here is the difference between *"nothing happens"* and *"oh, I need to install something"*. + +## Where next + +- [Hosting local web assets](Hosting-Local-Web-Assets) — the `CopyResourcesFolderContentsToLocalPath` helper and virtual-host pattern this tutorial builds on. +- [JavaScript interop](JavaScript-Interop) — the three bridges between BASIC and JavaScript. +- [WebView2 reference](../../tB/Packages/WebView2/WebView2/) — every property, method, and event. diff --git a/docs/Tutorials/WebView2/Hosting local web assets.md b/docs/Tutorials/WebView2/Hosting local web assets.md new file mode 100644 index 00000000..7cb6e93d --- /dev/null +++ b/docs/Tutorials/WebView2/Hosting local web assets.md @@ -0,0 +1,128 @@ +--- +title: Hosting local web assets +parent: WebView2 +grand_parent: Tutorials +nav_order: 5 +permalink: /Tutorials/WebView2/Hosting-Local-Web-Assets +--- + +# Hosting local web assets + +A [**WebView2**](../../tB/Packages/WebView2/WebView2/) control can serve HTML, JavaScript, CSS, and any other assets straight from a folder on disk — no embedded HTTP server required. Edge's [**SetVirtualHostNameToFolderMapping**](../../tB/Packages/WebView2/WebView2/#setvirtualhostnametofoldermapping) routes a virtual `https://` hostname to a local folder so that resources behave as if they came from a real origin: same-origin `fetch`, Content Security Policy, service workers, and so on all work as expected. + +This tutorial walks through the pattern used by *Sample 0 — WebView2 Examples* (forms *Example 2*, *Example 3*, *Example 4*). + +## The three-step pattern + +1. **Choose a folder.** It must exist on disk and contain `index.html` (plus whatever assets the page wants — scripts, styles, images). +2. **Register a virtual host** mapping to that folder. +3. **Navigate** to a URL under the virtual hostname. + +Hook into the [**Ready**](../../tB/Packages/WebView2/WebView2/#ready) event so the control is fully initialised before the mapping is installed: + +```tb +Private Sub WebView_Ready() Handles WebView.Ready + Dim folderPath As String = _ + Environ$("USERPROFILE") & "\Documents\MyApp" + WebView.SetVirtualHostNameToFolderMapping _ + "myapp.example", folderPath & "\", wv2ResourceAllow + WebView.Navigate "https://myapp.example/index.html" +End Sub +``` + +Once mapped, every request to `https://myapp.example/<path>` is served from `folderPath\<path>`. A `<script src="/script.js">` on the page resolves to `folderPath\script.js` exactly as if a real web server were sitting on `myapp.example`. + +## Picking a hostname + +The Edge runtime resolves the virtual hostname through DNS *before* applying the local override. Hostnames that happen to be resolvable on the public Internet introduce a small (≈2 s) stall on every request — see [WebView2Feedback#2381](https://github.com/MicrosoftEdge/WebView2Feedback/issues/2381). + +The safe convention is to pick a name under a TLD that will never resolve, like `.example`, `.invalid`, or `.test`: + +| Recommended | Avoid | +|---------------------|-----------------------------| +| `myapp.example` | `myapp.com`, `app.local` | +| `editor.invalid` | `editor.dev` | +| `assets.test` | `assets.io` | + +## Bundling assets in the project's Resources folder + +Most applications want to ship their HTML / JS / CSS *inside* the executable and drop them onto disk on first run. twinBASIC's `Resources` folder is the right place to keep them. + +1. In the IDE's Project explorer, expand **Resources** and add a sub-folder (right-click → *Add new subfolder*). Name it something memorable like `WEB_APP`. +2. Drop the assets in — `index.html`, `script.js`, `styles.css`, plus any sub-directories you need. + +At runtime, the helper below copies the contents of a `Resources` sub-folder out to a local path. Drop it into a `.twin` module in your project: + +```tb +Module Files + + Private Sub CreateFile(ByVal Path As String, ByRef Data() As Byte) + On Error Resume Next : Kill Path : On Error GoTo 0 + Dim fileNum As Integer = FreeFile + Open Path For Binary As fileNum + Put fileNum, 1, Data + Close fileNum + End Sub + + Private Sub CreateLocalFileFromResource( _ + ByVal OutputLocalFolderPath As String, _ + ByVal InputResourceSubFolderName As String, _ + ByVal ResourceName As String) + + Dim splitPath As Variant = Split(ResourceName, "~") + On Error Resume Next : MkDir OutputLocalFolderPath : On Error GoTo 0 + + Dim i As Long + For i = 0 To UBound(splitPath) - 1 + OutputLocalFolderPath &= "\" & splitPath(i) + On Error Resume Next : MkDir OutputLocalFolderPath : On Error GoTo 0 + Next + + Dim Data() As Byte + Data = LoadResData(ResourceName, InputResourceSubFolderName) + CreateFile(OutputLocalFolderPath & "\" & splitPath(i), Data) + End Sub + + [Description("Copy every file from a Resources subfolder onto disk. " & _ + "'~' characters in resource names represent subfolders.")] + Public Sub CopyResourcesFolderContentsToLocalPath( _ + ByVal InputResourceSubFolderName As String, _ + ByVal OutputLocalFolderPath As String) + + Dim resourceId As Variant + For Each resourceId In LoadResIdList(InputResourceSubFolderName) + CreateLocalFileFromResource _ + OutputLocalFolderPath, InputResourceSubFolderName, resourceId + Next + End Sub + +End Module +``` + +[**LoadResIdList**](../../tB/Packages/VB/Global/#loadresidlist) returns every resource ID under the named sub-folder; [**LoadResData**](../../tB/Packages/VB/Global/#loadresdata) hands back the bytes. The helper splits each resource name on `~` to reconstruct the original sub-directory tree on disk — the twinBASIC IDE flattens nested folders by joining their names with `~` when the resources are compiled in. + +## Putting it together + +The complete deploy-on-`Ready` pattern looks like this: + +```tb +Private Sub WebView_Ready() Handles WebView.Ready + ' Resources/WEB_APP/* is copied here on every launch. + Dim folderPath As String = _ + Environ$("USERPROFILE") & "\Documents\MyApp" + + CopyResourcesFolderContentsToLocalPath "WEB_APP", folderPath + + WebView.SetVirtualHostNameToFolderMapping _ + "myapp.example", folderPath & "\", wv2ResourceAllow + WebView.Navigate "https://myapp.example/index.html" +End Sub +``` + +Once deployed, the application can launch DevTools ([**OpenDevToolsWindow**](../../tB/Packages/WebView2/WebView2/#opendevtoolswindow)) to inspect the loaded files, and users can edit `index.html` directly on disk and hit **Refresh** — useful for rapid iteration during development. + +## Where next + +- [JavaScript interop](JavaScript-Interop) — how a hosted page exchanges values and method calls with the BASIC application. +- [Driving Monaco from twinBASIC](Driving-Monaco) — a full case study built on top of this pattern. +- [SetVirtualHostNameToFolderMapping](../../tB/Packages/WebView2/WebView2/#setvirtualhostnametofoldermapping) — full reference. diff --git a/docs/Tutorials/WebView2/Images/MonacoArchitecture.svg b/docs/Tutorials/WebView2/Images/MonacoArchitecture.svg new file mode 100644 index 00000000..bff6ab3d --- /dev/null +++ b/docs/Tutorials/WebView2/Images/MonacoArchitecture.svg @@ -0,0 +1 @@ +<svg id="mermaidChart0" width="100%" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" class="flowchart mermaid-svg" style="max-width: 1102.07px; color: rgb(51, 51, 51); fill: rgb(51, 51, 51); stroke: none; stroke-width: 1px; opacity: 1; stroke-dasharray: none; font-size: 16px; font-family: sans-serif; text-anchor: start; padding: 0px; margin: 0px;" viewBox="0 0 1102.0728759765625 164" role="graphics-document document" aria-roledescription="flowchart-v2"><style style="background-color: rgba(0, 0, 0, 0); color: rgb(51, 51, 51); fill: rgb(51, 51, 51); stroke: none; stroke-width: 1px; opacity: 1; stroke-dasharray: none; font-size: 16px; font-family: sans-serif; text-anchor: start; padding: 0px; margin: 0px;">#mermaidChart0{font-family:sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaidChart0 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaidChart0 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaidChart0 .error-icon{fill:#552222;}#mermaidChart0 .error-text{fill:#552222;stroke:#552222;}#mermaidChart0 .edge-thickness-normal{stroke-width:1px;}#mermaidChart0 .edge-thickness-thick{stroke-width:3.5px;}#mermaidChart0 .edge-pattern-solid{stroke-dasharray:0;}#mermaidChart0 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaidChart0 .edge-pattern-dashed{stroke-dasharray:3;}#mermaidChart0 .edge-pattern-dotted{stroke-dasharray:2;}#mermaidChart0 .marker{fill:#333333;stroke:#333333;}#mermaidChart0 .marker.cross{stroke:#333333;}#mermaidChart0 svg{font-family:sans-serif;font-size:16px;}#mermaidChart0 p{margin:0;}#mermaidChart0 .label{font-family:sans-serif;color:#333;}#mermaidChart0 .cluster-label text{fill:#333;}#mermaidChart0 .cluster-label span{color:#333;}#mermaidChart0 .cluster-label span p{background-color:transparent;}#mermaidChart0 .label text,#mermaidChart0 span{fill:#333;color:#333;}#mermaidChart0 .node rect,#mermaidChart0 .node circle,#mermaidChart0 .node ellipse,#mermaidChart0 .node polygon,#mermaidChart0 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaidChart0 .rough-node .label text,#mermaidChart0 .node .label text,#mermaidChart0 .image-shape .label,#mermaidChart0 .icon-shape .label{text-anchor:middle;}#mermaidChart0 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaidChart0 .rough-node .label,#mermaidChart0 .node .label,#mermaidChart0 .image-shape .label,#mermaidChart0 .icon-shape .label{text-align:center;}#mermaidChart0 .node.clickable{cursor:pointer;}#mermaidChart0 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaidChart0 .arrowheadPath{fill:#333333;}#mermaidChart0 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaidChart0 .flowchart-link{stroke:#333333;fill:none;}#mermaidChart0 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaidChart0 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaidChart0 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaidChart0 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaidChart0 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaidChart0 .cluster text{fill:#333;}#mermaidChart0 .cluster span{color:#333;}#mermaidChart0 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaidChart0 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaidChart0 rect.text{fill:none;stroke-width:0;}#mermaidChart0 .icon-shape,#mermaidChart0 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaidChart0 .icon-shape p,#mermaidChart0 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaidChart0 .icon-shape rect,#mermaidChart0 .image-shape rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaidChart0 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaidChart0 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaidChart0 :root{--mermaid-alt-font-family:sans-serif;}</style><g style="background-color: rgba(0, 0, 0, 0); color: rgb(51, 51, 51); fill: rgb(51, 51, 51); stroke: none; stroke-width: 1px; opacity: 1; stroke-dasharray: none; font-size: 16px; font-family: sans-serif; text-anchor: start; padding: 0px; margin: 0px;"><marker id="mermaidChart0_flowchart-v2-pointEnd" class="marker flowchart-v2" viewBox="0 0 10 10" refX="5" refY="5" markerUnits="userSpaceOnUse" markerWidth="8" markerHeight="8" orient="auto" style="background-color: rgba(0, 0, 0, 0); color: rgb(51, 51, 51); fill: rgb(51, 51, 51); stroke: rgb(51, 51, 51); stroke-width: 1px; opacity: 1; stroke-dasharray: none; font-size: 16px; font-family: sans-serif; text-anchor: start; padding: 0px; margin: 0px;"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowMarkerPath" style="stroke-width: 1px; stroke-dasharray: 1px, 0px; background-color: rgba(0, 0, 0, 0); color: rgb(51, 51, 51); fill: rgb(51, 51, 51); stroke: rgb(51, 51, 51); opacity: 1; font-size: 16px; font-family: sans-serif; text-anchor: start; padding: 0px; margin: 0px;"></path></marker><marker id="mermaidChart0_flowchart-v2-pointStart" class="marker flowchart-v2" viewBox="0 0 10 10" refX="4.5" refY="5" markerUnits="userSpaceOnUse" markerWidth="8" markerHeight="8" orient="auto" style="background-color: rgba(0, 0, 0, 0); color: rgb(51, 51, 51); fill: rgb(51, 51, 51); stroke: rgb(51, 51, 51); stroke-width: 1px; opacity: 1; stroke-dasharray: none; font-size: 16px; font-family: sans-serif; text-anchor: start; padding: 0px; margin: 0px;"><path d="M 0 5 L 10 10 L 10 0 z" class="arrowMarkerPath" style="stroke-width: 1px; stroke-dasharray: 1px, 0px; background-color: rgba(0, 0, 0, 0); color: rgb(51, 51, 51); fill: rgb(51, 51, 51); stroke: rgb(51, 51, 51); opacity: 1; font-size: 16px; font-family: sans-serif; text-anchor: start; padding: 0px; margin: 0px;"></path></marker><marker id="mermaidChart0_flowchart-v2-circleEnd" class="marker flowchart-v2" viewBox="0 0 10 10" refX="11" refY="5" markerUnits="userSpaceOnUse" markerWidth="11" markerHeight="11" orient="auto" style="background-color: rgba(0, 0, 0, 0); color: rgb(51, 51, 51); fill: rgb(51, 51, 51); stroke: rgb(51, 51, 51); stroke-width: 1px; opacity: 1; stroke-dasharray: none; font-size: 16px; font-family: sans-serif; text-anchor: start; padding: 0px; margin: 0px;"><circle cx="5" cy="5" r="5" class="arrowMarkerPath" style="stroke-width: 1px; stroke-dasharray: 1px, 0px; background-color: rgba(0, 0, 0, 0); color: rgb(51, 51, 51); fill: rgb(51, 51, 51); stroke: rgb(51, 51, 51); opacity: 1; font-size: 16px; font-family: sans-serif; text-anchor: start; padding: 0px; margin: 0px;"></circle></marker><marker id="mermaidChart0_flowchart-v2-circleStart" class="marker flowchart-v2" viewBox="0 0 10 10" refX="-1" refY="5" markerUnits="userSpaceOnUse" markerWidth="11" markerHeight="11" orient="auto" style="background-color: rgba(0, 0, 0, 0); color: rgb(51, 51, 51); fill: rgb(51, 51, 51); stroke: rgb(51, 51, 51); stroke-width: 1px; opacity: 1; stroke-dasharray: none; font-size: 16px; font-family: sans-serif; text-anchor: start; padding: 0px; margin: 0px;"><circle cx="5" cy="5" r="5" class="arrowMarkerPath" style="stroke-width: 1px; stroke-dasharray: 1px, 0px; background-color: rgba(0, 0, 0, 0); color: rgb(51, 51, 51); fill: rgb(51, 51, 51); stroke: rgb(51, 51, 51); opacity: 1; font-size: 16px; font-family: sans-serif; text-anchor: start; padding: 0px; margin: 0px;"></circle></marker><marker id="mermaidChart0_flowchart-v2-crossEnd" class="marker cross flowchart-v2" viewBox="0 0 11 11" refX="12" refY="5.2" markerUnits="userSpaceOnUse" markerWidth="11" markerHeight="11" orient="auto" style="background-color: rgba(0, 0, 0, 0); color: rgb(51, 51, 51); fill: rgb(51, 51, 51); stroke: rgb(51, 51, 51); stroke-width: 1px; opacity: 1; stroke-dasharray: none; font-size: 16px; font-family: sans-serif; text-anchor: start; padding: 0px; margin: 0px;"><path d="M 1,1 l 9,9 M 10,1 l -9,9" class="arrowMarkerPath" style="stroke-width: 2px; stroke-dasharray: 1px, 0px; background-color: rgba(0, 0, 0, 0); color: rgb(51, 51, 51); fill: rgb(51, 51, 51); stroke: rgb(51, 51, 51); opacity: 1; font-size: 16px; font-family: sans-serif; text-anchor: start; padding: 0px; margin: 0px;"></path></marker><marker id="mermaidChart0_flowchart-v2-crossStart" class="marker cross flowchart-v2" viewBox="0 0 11 11" refX="-1" refY="5.2" markerUnits="userSpaceOnUse" markerWidth="11" markerHeight="11" orient="auto" style="background-color: rgba(0, 0, 0, 0); color: rgb(51, 51, 51); fill: rgb(51, 51, 51); stroke: rgb(51, 51, 51); stroke-width: 1px; opacity: 1; stroke-dasharray: none; font-size: 16px; font-family: sans-serif; text-anchor: start; padding: 0px; margin: 0px;"><path d="M 1,1 l 9,9 M 10,1 l -9,9" class="arrowMarkerPath" style="stroke-width: 2px; stroke-dasharray: 1px, 0px; background-color: rgba(0, 0, 0, 0); color: rgb(51, 51, 51); fill: rgb(51, 51, 51); stroke: rgb(51, 51, 51); opacity: 1; font-size: 16px; font-family: sans-serif; text-anchor: start; padding: 0px; margin: 0px;"></path></marker><g class="root" style="background-color: rgba(0, 0, 0, 0); color: rgb(51, 51, 51); fill: rgb(51, 51, 51); stroke: none; stroke-width: 1px; opacity: 1; stroke-dasharray: none; font-size: 16px; font-family: sans-serif; text-anchor: start; padding: 0px; margin: 0px;"><g class="clusters" style="background-color: rgba(0, 0, 0, 0); color: rgb(51, 51, 51); fill: rgb(51, 51, 51); stroke: none; stroke-width: 1px; opacity: 1; stroke-dasharray: none; font-size: 16px; font-family: sans-serif; text-anchor: start; padding: 0px; margin: 0px;"></g><g class="edgePaths" style="background-color: rgba(0, 0, 0, 0); color: rgb(51, 51, 51); fill: rgb(51, 51, 51); stroke: none; stroke-width: 1px; opacity: 1; stroke-dasharray: none; font-size: 16px; font-family: sans-serif; text-anchor: start; padding: 0px; margin: 0px;"></g><g class="edgeLabels" style="background-color: rgba(0, 0, 0, 0); color: rgb(51, 51, 51); fill: rgb(51, 51, 51); stroke: none; stroke-width: 1px; opacity: 1; stroke-dasharray: none; font-size: 16px; font-family: sans-serif; text-anchor: start; padding: 0px; margin: 0px;"></g><g class="nodes" style="background-color: rgba(0, 0, 0, 0); color: rgb(51, 51, 51); fill: rgb(51, 51, 51); stroke: none; stroke-width: 1px; opacity: 1; stroke-dasharray: none; font-size: 16px; font-family: sans-serif; text-anchor: start; padding: 0px; margin: 0px;"><g class="root" transform="translate(0, 0)" style="background-color: rgba(0, 0, 0, 0); color: rgb(51, 51, 51); fill: rgb(51, 51, 51); stroke: none; stroke-width: 1px; opacity: 1; stroke-dasharray: none; font-size: 16px; font-family: sans-serif; text-anchor: start; padding: 0px; margin: 0px;"><g class="clusters" style="background-color: rgba(0, 0, 0, 0); color: rgb(51, 51, 51); fill: rgb(51, 51, 51); stroke: none; stroke-width: 1px; opacity: 1; stroke-dasharray: none; font-size: 16px; font-family: sans-serif; text-anchor: start; padding: 0px; margin: 0px;"><g class="cluster " id="form" data-look="classic" style="background-color: rgba(0, 0, 0, 0); color: rgb(51, 51, 51); fill: rgb(51, 51, 51); stroke: none; stroke-width: 1px; opacity: 1; stroke-dasharray: none; font-size: 16px; font-family: sans-serif; text-anchor: start; padding: 0px; margin: 0px;"><rect style="background-color: rgba(0, 0, 0, 0); color: rgb(51, 51, 51); fill: rgb(255, 255, 222); stroke: rgb(170, 170, 51); stroke-width: 1px; opacity: 1; stroke-dasharray: none; font-size: 16px; font-family: sans-serif; text-anchor: start; padding: 0px; margin: 0px;" x="8" y="8" width="1086.0729217529297" height="148"></rect><g class="cluster-label " transform="translate(492.578125, 8)" style="background-color: rgba(0, 0, 0, 0); color: rgb(51, 51, 51); fill: rgb(51, 51, 51); stroke: none; stroke-width: 1px; opacity: 1; stroke-dasharray: none; font-size: 16px; font-family: sans-serif; text-anchor: start; padding: 0px; margin: 0px;"><foreignObject width="116.91667175292969" height="24" style="background-color: rgba(0, 0, 0, 0); color: rgb(51, 51, 51); fill: rgb(51, 51, 51); stroke: none; stroke-width: 1px; opacity: 1; stroke-dasharray: none; font-size: 16px; font-family: sans-serif; text-anchor: start; padding: 0px; margin: 0px;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center; background-color: rgba(0, 0, 0, 0); color: rgb(51, 51, 51); fill: rgb(51, 51, 51); stroke: none; stroke-width: 1px; opacity: 1; stroke-dasharray: none; font-size: 16px; font-family: sans-serif; text-anchor: start; padding: 0px; margin: 0px;"><span class="nodeLabel " style="background-color: rgba(0, 0, 0, 0); color: rgb(51, 51, 51); fill: rgb(51, 51, 51); stroke: none; stroke-width: 1px; opacity: 1; stroke-dasharray: none; font-size: 16px; font-family: sans-serif; text-anchor: start; padding: 0px; margin: 0px;"><p style="background-color: rgba(0, 0, 0, 0); color: rgb(51, 51, 51); fill: rgb(51, 51, 51); stroke: none; stroke-width: 1px; opacity: 1; stroke-dasharray: none; font-size: 16px; font-family: sans-serif; text-anchor: start; padding: 0px 2px; margin: 0px;">twinBASIC form</p></span></div></foreignObject></g></g></g><g class="edgePaths" style="background-color: rgba(0, 0, 0, 0); color: rgb(51, 51, 51); fill: rgb(51, 51, 51); stroke: none; stroke-width: 1px; opacity: 1; stroke-dasharray: none; font-size: 16px; font-family: sans-serif; text-anchor: start; padding: 0px; margin: 0px;"><path d="M210.896,82L316.422,82L417.948,82" id="L_WebView_handler_0" class=" edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style="background-color: rgba(0, 0, 0, 0); color: rgb(51, 51, 51); fill: none; stroke: rgb(51, 51, 51); stroke-width: 1px; opacity: 1; stroke-dasharray: 0px; font-size: 16px; font-family: sans-serif; text-anchor: start; padding: 0px; margin: 0px;" marker-end="url(#mermaidChart0_flowchart-v2-pointEnd)"></path><path d="M624.698,82L743.563,82L858.427,82" id="L_handler_WebViewPreview_0" class=" edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style="background-color: rgba(0, 0, 0, 0); color: rgb(51, 51, 51); fill: none; stroke: rgb(51, 51, 51); stroke-width: 1px; opacity: 1; stroke-dasharray: 0px; font-size: 16px; font-family: sans-serif; text-anchor: start; padding: 0px; margin: 0px;" marker-end="url(#mermaidChart0_flowchart-v2-pointEnd)"></path></g><g class="edgeLabels" style="background-color: rgba(0, 0, 0, 0); color: rgb(51, 51, 51); fill: rgb(51, 51, 51); stroke: none; stroke-width: 1px; opacity: 1; stroke-dasharray: none; font-size: 16px; font-family: sans-serif; text-anchor: start; padding: 0px; margin: 0px;"><g class="edgeLabel" transform="translate(316.421875, 82)" style="background-color: rgba(232, 232, 232, 0.8); color: rgb(51, 51, 51); fill: rgb(51, 51, 51); stroke: none; stroke-width: 1px; opacity: 1; stroke-dasharray: none; font-size: 16px; font-family: sans-serif; text-anchor: start; padding: 0px; margin: 0px;"><g class="label" transform="translate(-68.02604675292969, -12)" style="background-color: rgba(0, 0, 0, 0); color: rgb(51, 51, 51); fill: rgb(51, 51, 51); stroke: none; stroke-width: 1px; opacity: 1; stroke-dasharray: none; font-size: 16px; font-family: sans-serif; text-anchor: start; padding: 0px; margin: 0px;"><foreignObject width="136.05209350585938" height="24" style="background-color: rgba(0, 0, 0, 0); color: rgb(51, 51, 51); fill: rgb(51, 51, 51); stroke: none; stroke-width: 1px; opacity: 1; stroke-dasharray: none; font-size: 16px; font-family: sans-serif; text-anchor: start; padding: 0px; margin: 0px;"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center; background-color: rgba(232, 232, 232, 0.5); color: rgb(51, 51, 51); fill: rgb(51, 51, 51); stroke: none; stroke-width: 1px; opacity: 1; stroke-dasharray: none; font-size: 16px; font-family: sans-serif; text-anchor: start; padding: 0px; margin: 0px;"><span class="edgeLabel " style="background-color: rgba(232, 232, 232, 0.8); color: rgb(51, 51, 51); fill: rgb(51, 51, 51); stroke: none; stroke-width: 1px; opacity: 1; stroke-dasharray: none; font-size: 16px; font-family: sans-serif; text-anchor: start; padding: 0px; margin: 0px;"><p style="background-color: rgba(232, 232, 232, 0.8); color: rgb(51, 51, 51); fill: rgb(51, 51, 51); stroke: none; stroke-width: 1px; opacity: 1; stroke-dasharray: none; font-size: 16px; font-family: sans-serif; text-anchor: start; padding: 0px; margin: 0px;">postMessage(html)</p></span></div></foreignObject></g></g><g class="edgeLabel" transform="translate(743.5625076293945, 82)" style="background-color: rgba(232, 232, 232, 0.8); color: rgb(51, 51, 51); fill: rgb(51, 51, 51); stroke: none; stroke-width: 1px; opacity: 1; stroke-dasharray: none; font-size: 16px; font-family: sans-serif; text-anchor: start; padding: 0px; margin: 0px;"><g class="label" transform="translate(-81.36458587646484, -12)" style="background-color: rgba(0, 0, 0, 0); color: rgb(51, 51, 51); fill: rgb(51, 51, 51); stroke: none; stroke-width: 1px; opacity: 1; stroke-dasharray: none; font-size: 16px; font-family: sans-serif; text-anchor: start; padding: 0px; margin: 0px;"><foreignObject width="162.7291717529297" height="24" style="background-color: rgba(0, 0, 0, 0); color: rgb(51, 51, 51); fill: rgb(51, 51, 51); stroke: none; stroke-width: 1px; opacity: 1; stroke-dasharray: none; font-size: 16px; font-family: sans-serif; text-anchor: start; padding: 0px; margin: 0px;"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center; background-color: rgba(232, 232, 232, 0.5); color: rgb(51, 51, 51); fill: rgb(51, 51, 51); stroke: none; stroke-width: 1px; opacity: 1; stroke-dasharray: none; font-size: 16px; font-family: sans-serif; text-anchor: start; padding: 0px; margin: 0px;"><span class="edgeLabel " style="background-color: rgba(232, 232, 232, 0.8); color: rgb(51, 51, 51); fill: rgb(51, 51, 51); stroke: none; stroke-width: 1px; opacity: 1; stroke-dasharray: none; font-size: 16px; font-family: sans-serif; text-anchor: start; padding: 0px; margin: 0px;"><p style="background-color: rgba(232, 232, 232, 0.8); color: rgb(51, 51, 51); fill: rgb(51, 51, 51); stroke: none; stroke-width: 1px; opacity: 1; stroke-dasharray: none; font-size: 16px; font-family: sans-serif; text-anchor: start; padding: 0px; margin: 0px;">NavigateToString(html)</p></span></div></foreignObject></g></g></g><g class="nodes" style="background-color: rgba(0, 0, 0, 0); color: rgb(51, 51, 51); fill: rgb(51, 51, 51); stroke: none; stroke-width: 1px; opacity: 1; stroke-dasharray: none; font-size: 16px; font-family: sans-serif; text-anchor: start; padding: 0px; margin: 0px;"><g class="node default " id="flowchart-WebView-0" transform="translate(128.19791412353516, 82)" style="background-color: rgba(0, 0, 0, 0); color: rgb(51, 51, 51); fill: rgb(51, 51, 51); stroke: none; stroke-width: 1px; opacity: 1; stroke-dasharray: none; font-size: 16px; font-family: sans-serif; text-anchor: start; padding: 0px; margin: 0px;"><rect class="basic label-container" style="background-color: rgba(0, 0, 0, 0); color: rgb(51, 51, 51); fill: rgb(236, 236, 255); stroke: rgb(147, 112, 219); stroke-width: 1px; opacity: 1; stroke-dasharray: none; font-size: 16px; font-family: sans-serif; text-anchor: start; padding: 0px; margin: 0px;" x="-82.69791793823242" y="-39" width="165.39583587646484" height="78"></rect><g class="label" style="background-color: rgba(0, 0, 0, 0); color: rgb(51, 51, 51); fill: rgb(51, 51, 51); stroke: none; stroke-width: 1px; opacity: 1; stroke-dasharray: none; font-size: 16px; font-family: sans-serif; text-anchor: start; padding: 0px; margin: 0px;" transform="translate(-52.69791793823242, -24)"><rect style="background-color: rgba(0, 0, 0, 0); color: rgb(51, 51, 51); fill: rgb(236, 236, 255); stroke: rgb(147, 112, 219); stroke-width: 1px; opacity: 1; stroke-dasharray: none; font-size: 16px; font-family: sans-serif; text-anchor: start; padding: 0px; margin: 0px;"></rect><foreignObject width="105.39583587646484" height="48" style="background-color: rgba(0, 0, 0, 0); color: rgb(51, 51, 51); fill: rgb(51, 51, 51); stroke: none; stroke-width: 1px; opacity: 1; stroke-dasharray: none; font-size: 16px; font-family: sans-serif; text-anchor: start; padding: 0px; margin: 0px;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center; background-color: rgba(0, 0, 0, 0); color: rgb(51, 51, 51); fill: rgb(51, 51, 51); stroke: none; stroke-width: 1px; opacity: 1; stroke-dasharray: none; font-size: 16px; font-family: sans-serif; text-anchor: start; padding: 0px; margin: 0px;"><span class="nodeLabel " style="background-color: rgba(0, 0, 0, 0); color: rgb(51, 51, 51); fill: rgb(51, 51, 51); stroke: none; stroke-width: 1px; opacity: 1; stroke-dasharray: none; font-size: 16px; font-family: sans-serif; text-anchor: start; padding: 0px; margin: 0px;"><p style="background-color: rgba(0, 0, 0, 0); color: rgb(51, 51, 51); fill: rgb(51, 51, 51); stroke: none; stroke-width: 1px; opacity: 1; stroke-dasharray: none; font-size: 16px; font-family: sans-serif; text-anchor: start; padding: 0px 2px; margin: 0px;"><b style="background-color: rgba(0, 0, 0, 0); color: rgb(51, 51, 51); fill: rgb(51, 51, 51); stroke: none; stroke-width: 1px; opacity: 1; stroke-dasharray: none; font-size: 16px; font-family: sans-serif; text-anchor: start; padding: 0px; margin: 0px;">WebView</b><br/><i style="background-color: rgba(0, 0, 0, 0); color: rgb(51, 51, 51); fill: rgb(51, 51, 51); stroke: none; stroke-width: 1px; opacity: 1; stroke-dasharray: none; font-size: 16px; font-family: sans-serif; text-anchor: start; padding: 0px; margin: 0px;">Monaco editor</i></p></span></div></foreignObject></g></g><g class="node default " id="flowchart-handler-1" transform="translate(523.3229217529297, 82)" style="background-color: rgba(0, 0, 0, 0); color: rgb(51, 51, 51); fill: rgb(51, 51, 51); stroke: none; stroke-width: 1px; opacity: 1; stroke-dasharray: none; font-size: 16px; font-family: sans-serif; text-anchor: start; padding: 0px; margin: 0px;"><rect class="basic label-container" style="background-color: rgba(0, 0, 0, 0); color: rgb(51, 51, 51); fill: rgb(236, 236, 255); stroke: rgb(147, 112, 219); stroke-width: 1px; opacity: 1; stroke-dasharray: none; font-size: 16px; font-family: sans-serif; text-anchor: start; padding: 0px; margin: 0px;" x="-101.375" y="-39" width="202.75" height="78"></rect><g class="label" style="background-color: rgba(0, 0, 0, 0); color: rgb(51, 51, 51); fill: rgb(51, 51, 51); stroke: none; stroke-width: 1px; opacity: 1; stroke-dasharray: none; font-size: 16px; font-family: sans-serif; text-anchor: start; padding: 0px; margin: 0px;" transform="translate(-71.375, -24)"><rect style="background-color: rgba(0, 0, 0, 0); color: rgb(51, 51, 51); fill: rgb(236, 236, 255); stroke: rgb(147, 112, 219); stroke-width: 1px; opacity: 1; stroke-dasharray: none; font-size: 16px; font-family: sans-serif; text-anchor: start; padding: 0px; margin: 0px;"></rect><foreignObject width="142.75" height="48" style="background-color: rgba(0, 0, 0, 0); color: rgb(51, 51, 51); fill: rgb(51, 51, 51); stroke: none; stroke-width: 1px; opacity: 1; stroke-dasharray: none; font-size: 16px; font-family: sans-serif; text-anchor: start; padding: 0px; margin: 0px;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center; background-color: rgba(0, 0, 0, 0); color: rgb(51, 51, 51); fill: rgb(51, 51, 51); stroke: none; stroke-width: 1px; opacity: 1; stroke-dasharray: none; font-size: 16px; font-family: sans-serif; text-anchor: start; padding: 0px; margin: 0px;"><span class="nodeLabel " style="background-color: rgba(0, 0, 0, 0); color: rgb(51, 51, 51); fill: rgb(51, 51, 51); stroke: none; stroke-width: 1px; opacity: 1; stroke-dasharray: none; font-size: 16px; font-family: sans-serif; text-anchor: start; padding: 0px; margin: 0px;"><p style="background-color: rgba(0, 0, 0, 0); color: rgb(51, 51, 51); fill: rgb(51, 51, 51); stroke: none; stroke-width: 1px; opacity: 1; stroke-dasharray: none; font-size: 16px; font-family: sans-serif; text-anchor: start; padding: 0px 2px; margin: 0px;">JsMessage handler<br/><i style="background-color: rgba(0, 0, 0, 0); color: rgb(51, 51, 51); fill: rgb(51, 51, 51); stroke: none; stroke-width: 1px; opacity: 1; stroke-dasharray: none; font-size: 16px; font-family: sans-serif; text-anchor: start; padding: 0px; margin: 0px;">(twinBASIC code)</i></p></span></div></foreignObject></g></g><g class="node default " id="flowchart-WebViewPreview-2" transform="translate(959.5000076293945, 82)" style="background-color: rgba(0, 0, 0, 0); color: rgb(51, 51, 51); fill: rgb(51, 51, 51); stroke: none; stroke-width: 1px; opacity: 1; stroke-dasharray: none; font-size: 16px; font-family: sans-serif; text-anchor: start; padding: 0px; margin: 0px;"><rect class="basic label-container" style="background-color: rgba(0, 0, 0, 0); color: rgb(51, 51, 51); fill: rgb(236, 236, 255); stroke: rgb(147, 112, 219); stroke-width: 1px; opacity: 1; stroke-dasharray: none; font-size: 16px; font-family: sans-serif; text-anchor: start; padding: 0px; margin: 0px;" x="-97.07292175292969" y="-39" width="194.14584350585938" height="78"></rect><g class="label" style="background-color: rgba(0, 0, 0, 0); color: rgb(51, 51, 51); fill: rgb(51, 51, 51); stroke: none; stroke-width: 1px; opacity: 1; stroke-dasharray: none; font-size: 16px; font-family: sans-serif; text-anchor: start; padding: 0px; margin: 0px;" transform="translate(-67.07292175292969, -24)"><rect style="background-color: rgba(0, 0, 0, 0); color: rgb(51, 51, 51); fill: rgb(236, 236, 255); stroke: rgb(147, 112, 219); stroke-width: 1px; opacity: 1; stroke-dasharray: none; font-size: 16px; font-family: sans-serif; text-anchor: start; padding: 0px; margin: 0px;"></rect><foreignObject width="134.14584350585938" height="48" style="background-color: rgba(0, 0, 0, 0); color: rgb(51, 51, 51); fill: rgb(51, 51, 51); stroke: none; stroke-width: 1px; opacity: 1; stroke-dasharray: none; font-size: 16px; font-family: sans-serif; text-anchor: start; padding: 0px; margin: 0px;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center; background-color: rgba(0, 0, 0, 0); color: rgb(51, 51, 51); fill: rgb(51, 51, 51); stroke: none; stroke-width: 1px; opacity: 1; stroke-dasharray: none; font-size: 16px; font-family: sans-serif; text-anchor: start; padding: 0px; margin: 0px;"><span class="nodeLabel " style="background-color: rgba(0, 0, 0, 0); color: rgb(51, 51, 51); fill: rgb(51, 51, 51); stroke: none; stroke-width: 1px; opacity: 1; stroke-dasharray: none; font-size: 16px; font-family: sans-serif; text-anchor: start; padding: 0px; margin: 0px;"><p style="background-color: rgba(0, 0, 0, 0); color: rgb(51, 51, 51); fill: rgb(51, 51, 51); stroke: none; stroke-width: 1px; opacity: 1; stroke-dasharray: none; font-size: 16px; font-family: sans-serif; text-anchor: start; padding: 0px 2px; margin: 0px;"><b style="background-color: rgba(0, 0, 0, 0); color: rgb(51, 51, 51); fill: rgb(51, 51, 51); stroke: none; stroke-width: 1px; opacity: 1; stroke-dasharray: none; font-size: 16px; font-family: sans-serif; text-anchor: start; padding: 0px; margin: 0px;">WebViewPreview</b><br/><i style="background-color: rgba(0, 0, 0, 0); color: rgb(51, 51, 51); fill: rgb(51, 51, 51); stroke: none; stroke-width: 1px; opacity: 1; stroke-dasharray: none; font-size: 16px; font-family: sans-serif; text-anchor: start; padding: 0px; margin: 0px;">HTML preview</i></p></span></div></foreignObject></g></g></g></g></g></g></g></svg> \ No newline at end of file diff --git a/docs/Tutorials/WebView2/JavaScript interop.md b/docs/Tutorials/WebView2/JavaScript interop.md new file mode 100644 index 00000000..d7e47fec --- /dev/null +++ b/docs/Tutorials/WebView2/JavaScript interop.md @@ -0,0 +1,169 @@ +--- +title: JavaScript interop +parent: WebView2 +grand_parent: Tutorials +nav_order: 6 +permalink: /Tutorials/WebView2/JavaScript-Interop +--- + +# JavaScript interop + +The [**WebView2**](../../tB/Packages/WebView2/WebView2/) control offers three complementary bridges between twinBASIC and the JavaScript running in the page: + +1. **Host objects** — publish a BASIC COM object to the page so JavaScript can call its methods and read its properties as if it were any other JS object. +2. **Messages** — push a value (string, number, array, …) in either direction and listen for it on the other side. +3. **Scripted calls** — call a named JavaScript function from BASIC and (optionally) wait for its return value. + +This tutorial covers all three, with the matching JavaScript side shown next to each BASIC side. The worked code comes from *Sample 0 — WebView2 Examples* (form *Example 2*). + +## Bridge 1 — Host objects + +[**AddObject**](../../tB/Packages/WebView2/WebView2/#addobject) publishes a BASIC class instance under `chrome.webview.hostObjects.<Name>`. Define a small class with public methods or properties: + +```tb +Class MyCalculator + Public Function MultiplyByTen(ByVal Value As Long) As Long + Return Value * 10 + End Function +End Class +``` + +Register it once the control is ready: + +```tb +Private Sub WebView_Ready() Handles WebView.Ready + WebView.AddObject "myCalculator", New MyCalculator +End Sub +``` + +JavaScript can now call into it — but the proxy is asynchronous, so the call must be `await`ed inside an `async` function: + +```js +async function testHostCalculator() { + let value = Math.floor(Math.random() * 100000); + let result = await chrome.webview.hostObjects.myCalculator.MultiplyByTen(value); + alert(`BASIC said ${value} × 10 = ${result}`); +} +``` + +To trigger the JS function from BASIC, call [**ExecuteScript**](../../tB/Packages/WebView2/WebView2/#executescript): + +```tb +Private Sub btnTest_Click() Handles btnTest.Click + WebView.ExecuteScript("testHostCalculator()") +End Sub +``` + +Requires [**AreHostObjectsAllowed**](../../tB/Packages/WebView2/WebView2/#arehostobjectsallowed) (default **True**). See [Re-entrancy](Re-entrancy) for the trade-off between synchronous calls (default) and the **UseDeferredInvoke:=True** variant. + +## Bridge 2 — Messages + +Messages are values that travel in either direction. Use them for notifications and ad-hoc payloads where you don't want to define a method signature ahead of time. + +### BASIC → page + +[**PostWebMessage**](../../tB/Packages/WebView2/WebView2/#postwebmessage) sends a value to the page; the page receives it through a `message` event on `window.chrome.webview`: + +```tb +WebView.PostWebMessage "Hello from twinBASIC!" +``` + +```js +window.chrome.webview.addEventListener('message', (e) => { + alert("Host sent: " + e.data); +}); +``` + +Strings arrive as JavaScript strings; every other type is JSON-encoded before transit. If you already have serialised JSON, [**PostWebMessageJSON**](../../tB/Packages/WebView2/WebView2/#postwebmessagejson) sends it through verbatim. + +### Page → BASIC + +The page calls `window.chrome.webview.postMessage(value)`; BASIC receives it as the [**JsMessage**](../../tB/Packages/WebView2/WebView2/#jsmessage) event: + +```js +function sendHostAMessage() { + window.chrome.webview.postMessage("This is a message from JavaScript."); +} +``` + +```tb +Private Sub WebView_JsMessage(ByVal Message As Variant) _ + Handles WebView.JsMessage + Debug.Print "Page sent: "; Message +End Sub +``` + +Both directions require [**IsWebMessageEnabled**](../../tB/Packages/WebView2/WebView2/#iswebmessageenabled) (default **True**). + +## Bridge 3 — Scripted calls + +When the page exposes named JS functions, BASIC can call them directly. There are three variants: + +| Method | Returns | Use it when | +|-----------------------------------------------------------------------------------|--------------------------------------------------|-------------------------------------------------------------------| +| [**JsRun**](../../tB/Packages/WebView2/WebView2/#jsrun) | **Variant**, synchronously | You need the result inline and the JS is quick. | +| [**JsRunAsync**](../../tB/Packages/WebView2/WebView2/#jsrunasync) | **LongLong** token; result via `JsAsyncResult` | The JS may take a while and you don't want to block the UI. | +| [**ExecuteScript**](../../tB/Packages/WebView2/WebView2/#executescript) | nothing (fire-and-forget) | You just want to trigger something — no return value needed. | + +### JsRun (synchronous) + +Given a page-side function: + +```js +function multiplyTheseNumbers(a, b) { + return a * b; +} +``` + +BASIC can call it and read the result on the same line: + +```tb +Dim product As Long = WebView.JsRun("multiplyTheseNumbers", 5, 6) +Debug.Print product ' 30 +``` + +The call blocks for up to [**JsCallTimeOutSeconds**](../../tB/Packages/WebView2/WebView2/#jscalltimeoutseconds) (default 0 — wait forever). + +### JsRunAsync (asynchronous) + +```tb +Private Sub btnRun_Click() Handles btnRun.Click + WebView.JsRunAsync "multiplyTheseNumbers", 5, 6 +End Sub + +Private Sub WebView_JsAsyncResult( _ + ByVal Result As Variant, Token As LongLong, ErrString As String) _ + Handles WebView.JsAsyncResult + If LenB(ErrString) = 0 Then + Debug.Print "Async result: "; Result + Else + Debug.Print "Async error: "; ErrString + End If +End Sub +``` + +The return value of [**JsRunAsync**](../../tB/Packages/WebView2/WebView2/#jsrunasync) is a token; the [**JsAsyncResult**](../../tB/Packages/WebView2/WebView2/#jsasyncresult) event carries the same token so a single handler can demultiplex multiple in-flight calls. + +### ExecuteScript (fire-and-forget) + +```tb +WebView.ExecuteScript "startTimer()" +``` + +No return value, no event. The simplest way to nudge the page into doing something. + +## Re-entrancy + +The Edge runtime forbids host code from calling back into the WebView2 object model while a host-object method is still executing — re-entry deadlocks the browser process. The control protects most events by deferring them through the BASIC message loop ([**UseDeferredEvents**](../../tB/Packages/WebView2/WebView2/#usedeferredevents)), but host-object method calls are synchronous by default. + +The full discussion lives in the [Re-entrancy tutorial](Re-entrancy); the short summary is: + +- **`AddObject(name, obj)`** — synchronous calls; the page can read return values but the BASIC method **must not** call back into the WebView2 control. +- **`AddObject(name, obj, UseDeferredInvoke:=True)`** — asynchronous calls; the BASIC method is free to call any WebView2 member but the page cannot read a return value. + +## Where next + +- [Hosting local web assets](Hosting-Local-Web-Assets) — bundle and serve the JavaScript that talks to the host. +- [Driving Monaco from twinBASIC](Driving-Monaco) — a full case study using all three bridges. +- [Re-entrancy](Re-entrancy) — the deeper story behind **UseDeferredInvoke**. +- [WebView2 reference](../../tB/Packages/WebView2/WebView2/) — every property, method, and event. diff --git a/docs/Tutorials/WebView2/_Images/MonacoArchitecture.md b/docs/Tutorials/WebView2/_Images/MonacoArchitecture.md new file mode 100644 index 00000000..5a307abf --- /dev/null +++ b/docs/Tutorials/WebView2/_Images/MonacoArchitecture.md @@ -0,0 +1,11 @@ +```mermaid +flowchart LR + subgraph form["twinBASIC form"] + direction LR + WebView["<b>WebView</b><br/><i>Monaco editor</i>"] + handler["JsMessage handler<br/><i>(twinBASIC code)</i>"] + WebViewPreview["<b>WebViewPreview</b><br/><i>HTML preview</i>"] + WebView -- "postMessage(html)" --> handler + handler -- "NavigateToString(html)" --> WebViewPreview + end +``` diff --git a/docs/Tutorials/WebView2/index.md b/docs/Tutorials/WebView2/index.md index a6feea6e..a1356100 100644 --- a/docs/Tutorials/WebView2/index.md +++ b/docs/Tutorials/WebView2/index.md @@ -1,10 +1,25 @@ --- title: WebView2 parent: Tutorials -permalink: /Tutorials/WebView2 +permalink: /Tutorials/WebView2/ redirect_from: - /WebView2 --- # WebView2 +The [**WebView2**](../../tB/Packages/WebView2/WebView2/) control hosts the Microsoft Edge browser engine inside a twinBASIC form — navigate to web pages, run local web apps, exchange messages and method calls with JavaScript, intercept HTTP traffic, and print pages to PDF. + +These tutorials walk through the most common patterns: + +- [Getting started](Getting-Started) — adding the package references and dropping a control onto a form. +- [Customize the UserDataFolder](Customize-UserDataFolder) — relocating the runtime's working folder for hosted scenarios (Office add-ins, kiosk installs). +- [Re-entrancy](Re-entrancy) — what the control's deferred-event machinery does for you, and the one place you still have to think about it. +- [Building a browser shell](Building-A-Browser-Shell) — address bar, back / forward / reload, zoom, PDF export — turning the control into a working browser. +- [Hosting local web assets](Hosting-Local-Web-Assets) — serve HTML / JS / CSS from a project resource folder, without an HTTP server. +- [JavaScript interop](JavaScript-Interop) — the three bridges between BASIC and the page: host objects, messages, and scripted calls. +- [Driving Monaco from twinBASIC](Driving-Monaco) — a case study combining everything above: embed Microsoft's Monaco editor next to a live HTML preview pane. + +The complete sample code for the last four tutorials ships as *Sample 0 — WebView2 Examples* in the New-Project dialog. + +For the full surface area of the control itself, see the [**WebView2** class reference](../../tB/Packages/WebView2/WebView2/). From b1112426bf69ab4e5c2d991551a41f036d432e51 Mon Sep 17 00:00:00 2001 From: Kuba Sunderland-Ober <kuba@mareimbrium.org> Date: Wed, 13 May 2026 19:29:23 +0200 Subject: [PATCH 2/2] Extend CustomControls docs with wisdom from bundled tB code samples. --- .../Enumerations/CornerShape.md | 13 +++++++++++ .../CustomControls/Enumerations/DockMode.md | 14 ++++++++++++ .../Enumerations/FillPattern.md | 18 +++++++++++++++ .../CustomControls/Styles/Borders.md | 18 +++++++++++++++ .../CustomControls/Styles/Corners.md | 13 +++++++++++ docs/Reference/CustomControls/Styles/Fill.md | 12 ++++++++++ .../CustomControls/Styles/TextRendering.md | 19 ++++++++++++++++ .../CustomControls/WaynesForm/index.md | 9 ++++++++ docs/Reference/CustomControls/WaynesFrame.md | 2 ++ .../CustomControls/WaynesGrid/index.md | 22 +++++++++++++++++++ docs/Reference/CustomControls/WaynesLabel.md | 15 +++++++++++++ .../CustomControls/WaynesSlider/index.md | 20 +++++++++++++++++ .../CustomControls/WaynesTextBox/index.md | 22 +++++++++++++++++++ 13 files changed, 197 insertions(+) diff --git a/docs/Reference/CustomControls/Enumerations/CornerShape.md b/docs/Reference/CustomControls/Enumerations/CornerShape.md index a10b3734..3fc0d1bd 100644 --- a/docs/Reference/CustomControls/Enumerations/CornerShape.md +++ b/docs/Reference/CustomControls/Enumerations/CornerShape.md @@ -14,3 +14,16 @@ Determines how a single corner of a control is shaped. Carried by [**Corner.Shap | **tbCurve**{: #tbCurve } | 0 | Quarter-circle round corner; the radius gives the curve. | | **tbNotched**{: #tbNotched } | 1 | Diagonal notch across the corner; the radius gives the cut depth. | | **tbCutOut**{: #tbCutOut } | 2 | Inverse round-corner — the corner area is carved *out* of the control. | + +[**Corners.SetAll**](../Styles/Corners#setall) applies one shape to every corner at once; setting [**TopLeft**](../Styles/Corners#topleft) / [**TopRight**](../Styles/Corners#topright) / [**BottomLeft**](../Styles/Corners#bottomleft) / [**BottomRight**](../Styles/Corners#bottomright) individually lets the shapes mix: + +```tb +With btnDemo.NormalState.Corners + .TopLeft.Shape = tbCurve : .TopLeft.Radius = 16 ' rounded + .TopRight.Shape = tbNotched : .TopRight.Radius = 16 ' diagonal cut + .BottomLeft.Shape = tbCutOut : .BottomLeft.Radius = 16 ' carved-out + .BottomRight.Shape = tbCurve : .BottomRight.Radius = 0 ' sharp 90° +End With +``` + +A [**Radius**](../Styles/Corners#radius) of 0 produces a sharp 90° corner regardless of [**Shape**](../Styles/Corners#shape); a radius greater than or equal to half the control's smaller dimension turns a [**tbCurve**](#tbCurve) corner into a quarter-circle that touches the centreline, which is the technique the package's `Circle` sample button uses to render a full circle from a rectangular control. diff --git a/docs/Reference/CustomControls/Enumerations/DockMode.md b/docs/Reference/CustomControls/Enumerations/DockMode.md index 6ae547eb..b35f2371 100644 --- a/docs/Reference/CustomControls/Enumerations/DockMode.md +++ b/docs/Reference/CustomControls/Enumerations/DockMode.md @@ -17,3 +17,17 @@ How a control is positioned relative to its container — pinned to one edge, fi | **tbDockRight**{: #tbDockRight } | 3 | Pinned to the container's right edge. Width is preserved; height is stretched. | | **tbDockBottom**{: #tbDockBottom } | 4 | Pinned to the container's bottom edge. Height is preserved; width is stretched. | | **tbDockFill**{: #tbDockFill } | 5 | Fills the entire remaining client area, after other docked siblings have claimed their edges. | + +Order matters when more than one sibling is docked inside the same container: each docked control claims its edge from whatever client area remains *after* its earlier-added siblings have claimed theirs. The control with **Dock = tbDockFill** is therefore added last so that it inherits the residual space: + +```tb +Private Sub Form_Load() + lblHeader.Dock = tbDockTop ' pinned to the top, full width + lblStatus.Dock = tbDockBottom ' pinned to the bottom, full width + pnlTree.Dock = tbDockLeft ' pinned to the left, between header and status + pnlAside.Dock = tbDockRight ' pinned to the right, between header and status + pnlMain.Dock = tbDockFill ' fills whatever is left in the middle +End Sub +``` + +Setting **Dock** to anything other than **tbDockNone** makes the control's own [**Anchors**](../Styles/Anchors) irrelevant — docking takes over the position and size completely. To return to manual positioning, set **Dock** back to **tbDockNone**. diff --git a/docs/Reference/CustomControls/Enumerations/FillPattern.md b/docs/Reference/CustomControls/Enumerations/FillPattern.md index 7ed6215c..2004ddb8 100644 --- a/docs/Reference/CustomControls/Enumerations/FillPattern.md +++ b/docs/Reference/CustomControls/Enumerations/FillPattern.md @@ -34,3 +34,21 @@ Identifies how the colour table held by a [**Fill**](../Styles/Fill) is applied | **tbGradientCornerBottomRightAlt**{: #tbGradientCornerBottomRightAlt } | 20 | Alternate bottom-right corner gradient with the stops mirrored. | The colour table itself comes from the array of [**FillColorPoint**](../Styles/Fill#fillcolorpoint-class) values inside [**Fill.ColorPoints**](../Styles/Fill#colorpoints), interpolated to the configured [**Granularity**](../Styles/Fill#granularity). + +The same two-stop pair painted with three different patterns produces three quite different results: + +```tb +' Top fades to bottom +pnlOne.BackgroundFill.SetSimplePattern vbWhite, &H99CCFF, _ + Pattern:=tbGradientNorthToSouth + +' Left fades to right +pnlTwo.BackgroundFill.SetSimplePattern vbWhite, &H99CCFF, _ + Pattern:=tbGradientWestToEast + +' Emanates from the top-left corner +pnlThree.BackgroundFill.SetSimplePattern vbWhite, &H99CCFF, _ + Pattern:=tbGradientCornerTopLeft +``` + +For a flat region with no gradient at all, use **tbPatternNone** — the `Fill` becomes fully transparent and the area behind the control shows through. diff --git a/docs/Reference/CustomControls/Styles/Borders.md b/docs/Reference/CustomControls/Styles/Borders.md index 22c43abc..0b8096dd 100644 --- a/docs/Reference/CustomControls/Styles/Borders.md +++ b/docs/Reference/CustomControls/Styles/Borders.md @@ -17,6 +17,24 @@ Reached as `<state>.Borders`, [**CellRenderingOptions.Borders**](../WaynesGrid/C btnGo.NormalState.Borders.SetSimpleBorder StrokeSize:=1, ColorRGB:=vbBlack ``` +Layered borders — multiple [**Border**](#border-class) instances stroked in order — are assigned to the [**Elements**](#elements) array directly. Each element can have its own [**StrokeSize**](#strokesize) and its own [**Fill**](Fill), so a thin black outline can sit on top of a wide coloured band, or three bands of different colours can stack into a "shadow": + +```tb +Dim elems(0 To 2) As Border +Set elems(0) = New Border +elems(0).StrokeSize = 4 +elems(0).Fill.ColorPoints.SetSolidColor vbBlack +Set elems(1) = New Border +elems(1).StrokeSize = 7 +elems(1).Fill.ColorPoints.SetSolidColor &H99CCFF ' light blue band +Set elems(2) = New Border +elems(2).StrokeSize = 4 +elems(2).Fill.ColorPoints.SetSolidColor &H4D7AB4 ' deeper blue +btnGo.NormalState.Borders.Elements = elems +``` + +A single [**Border**](#border-class) can also carry a gradient instead of a solid colour — assign a multi-stop [**Fill**](Fill) to its [**Fill**](#fill) member. Set [**BlendWithBackgroundFill**](#blendwithbackgroundfill) to **True** on a translucent border to make it tint with the control's own **BackgroundFill** rather than with whatever lies under the control. + * TOC {:toc} diff --git a/docs/Reference/CustomControls/Styles/Corners.md b/docs/Reference/CustomControls/Styles/Corners.md index ea942e6e..648db0a0 100644 --- a/docs/Reference/CustomControls/Styles/Corners.md +++ b/docs/Reference/CustomControls/Styles/Corners.md @@ -18,6 +18,19 @@ With btnGo.NormalState.Corners End With ``` +The three [**CornerShape**](../Enumerations/CornerShape) values can mix on a single control. Setting [**TopLeft**](#topleft), [**TopRight**](#topright), [**BottomLeft**](#bottomleft), and [**BottomRight**](#bottomright) individually gives full control over the silhouette: + +```tb +With btnTab.NormalState.Corners + .TopLeft.Shape = tbCurve : .TopLeft.Radius = 12 + .TopRight.Shape = tbCurve : .TopRight.Radius = 12 + .BottomLeft.Shape = tbNotched : .BottomLeft.Radius = 0 + .BottomRight.Shape = tbNotched : .BottomRight.Radius = 0 +End With +``` + +A circular control is just a square one with all four corners set to [**tbCurve**](../Enumerations/CornerShape#tbCurve) and a radius greater than or equal to half the control's smaller dimension — that is what the `Circle` button in the package's sample forms uses. + * TOC {:toc} diff --git a/docs/Reference/CustomControls/Styles/Fill.md b/docs/Reference/CustomControls/Styles/Fill.md index d3502673..91eb4f10 100644 --- a/docs/Reference/CustomControls/Styles/Fill.md +++ b/docs/Reference/CustomControls/Styles/Fill.md @@ -19,6 +19,18 @@ btnGo.HoverState.BackgroundFill.SetSimplePattern vbBlue, vbWhite, _ Pattern:=tbGradientNorthToSouth ``` +For three or more colour stops, build [**FillColorPoint**](#fillcolorpoint-class) instances and pass them to [**SetColorPoints**](#setcolorpoints). The stops accept fully-opaque ARGB literals (`&HFF` alpha in the high byte) — see [**ColorRGBA**](../Enumerations/ColorRGBA) for the encoding: + +```tb +With pnlHeader.BackgroundFill + .Pattern = tbGradientNorthToSouth + .ColorPoints.SetColorPoints _ + New FillColorPoint(&HFFF3E58F, 0), _ + New FillColorPoint(&HFF99CCFF, 50), _ + New FillColorPoint(&HFF014C99, 100) +End With +``` + * TOC {:toc} diff --git a/docs/Reference/CustomControls/Styles/TextRendering.md b/docs/Reference/CustomControls/Styles/TextRendering.md index e7b19813..4b7fff48 100644 --- a/docs/Reference/CustomControls/Styles/TextRendering.md +++ b/docs/Reference/CustomControls/Styles/TextRendering.md @@ -22,6 +22,25 @@ With lblTitle.TextRendering End With ``` +[**Fill**](#fill) can hold a gradient just as well as a solid colour, so glyphs themselves can be painted with a top-to-bottom or corner-to-corner colour transition. [**Outlines**](#outlines) is an array of [**Border**](Borders#border-class) elements stroked around the glyphs — a single thin black outline gives a "stickered" look; layering several outlines with different [**StrokeSize**](Borders#strokesize) values produces a glow or drop-shadow: + +```tb +With lblBanner.TextRendering + .Font.Size = 32 + .Font.Weight = tbBold + .Alignment = tbAlignMiddleCenter + .Fill.SetSimplePattern vbWhite, &HCCCCFF, _ + Pattern:=tbGradientNorthToSouth + Dim outline(0 To 0) As Border + Set outline(0) = New Border + outline(0).StrokeSize = 2 + outline(0).Fill.ColorPoints.SetSolidColor vbBlack + .Outlines = outline +End With +``` + +Set [**OverflowMode**](#overflowmode) to **tbShrinkToFit** to scale the glyphs down rather than truncating with an ellipsis when the text is too long for the available width — useful on fixed-width labels whose caption is set at runtime from data of unpredictable length. + * TOC {:toc} diff --git a/docs/Reference/CustomControls/WaynesForm/index.md b/docs/Reference/CustomControls/WaynesForm/index.md index 996baf53..da97ee3d 100644 --- a/docs/Reference/CustomControls/WaynesForm/index.md +++ b/docs/Reference/CustomControls/WaynesForm/index.md @@ -26,6 +26,15 @@ Private Sub Form_Load() End Sub ``` +[**BackgroundFill**](#backgroundfill) is an ordinary [**Fill**](../Styles/Fill), so the form can carry a gradient backdrop just as easily as a solid colour — this is what the package's `HelloWorld` sample form uses to give itself a soft top-to-bottom wash: + +```tb +Private Sub Form_Load() + Me.BackgroundFill.SetSimplePattern &HE5E5E5, &HF8F8F8, _ + Pattern:=tbGradientNorthToSouth +End Sub +``` + * TOC {:toc} diff --git a/docs/Reference/CustomControls/WaynesFrame.md b/docs/Reference/CustomControls/WaynesFrame.md index 14fd71fd..cd9eb877 100644 --- a/docs/Reference/CustomControls/WaynesFrame.md +++ b/docs/Reference/CustomControls/WaynesFrame.md @@ -18,6 +18,8 @@ Private Sub Form_Load() End Sub ``` +Frames work well as containers for [**Dock**](Enumerations/DockMode)-positioned children. Set the frame's own **Dock** to **tbDockFill** so it claims the form's body, then dock its children to **tbDockTop** / **tbDockLeft** / **tbDockFill** / etc. — the docking calculation walks the container tree, so children dock to the frame's client area rather than to the form. The order in which the children are added still determines which edges they claim first. + ## Properties ### Anchors diff --git a/docs/Reference/CustomControls/WaynesGrid/index.md b/docs/Reference/CustomControls/WaynesGrid/index.md index 364068ec..c28957d7 100644 --- a/docs/Reference/CustomControls/WaynesGrid/index.md +++ b/docs/Reference/CustomControls/WaynesGrid/index.md @@ -35,6 +35,28 @@ Private Sub Grid1_GetCellText( _ End Sub ``` +The six [**CellRenderingOptions**](CellRenderingOptions) sub-objects ([**ColumnHeaderOptions**](#columnheaderoptions), [**RowHeaderOptions**](#rowheaderoptions), [**CellOptions**](#celloptions), [**HoverCellOptions**](#hovercelloptions), [**SelectedCellOptions**](#selectedcelloptions), [**MultiSelectCellOptions**](#multiselectcelloptions)) give the grid its visual personality. A typical setup gives headers a gradient, body cells left-aligned text with a small left-padding indent, and the selected cell a contrasting border: + +```tb +With Grid1.ColumnHeaderOptions + .Fill.SetSimplePattern &HE0E0E0, &HC0C0C0, _ + Pattern:=tbGradientNorthToSouth + .TextRendering.Font.Weight = tbBold + .TextRendering.Alignment = tbAlignMiddleCenter +End With + +With Grid1.CellOptions + .Fill.ColorPoints.SetSolidColor vbWhite + .TextRendering.Alignment = tbAlignMiddleLeft + .TextRendering.Padding.Left = 10 +End With + +With Grid1.SelectedCellOptions + .Fill.ColorPoints.SetSolidColor &HFFEEDD ' pale blue + .Borders.SetSimpleBorder StrokeSize:=2, ColorRGB:=vbBlue +End With +``` + * TOC {:toc} diff --git a/docs/Reference/CustomControls/WaynesLabel.md b/docs/Reference/CustomControls/WaynesLabel.md index d2bdfdec..0f1802c6 100644 --- a/docs/Reference/CustomControls/WaynesLabel.md +++ b/docs/Reference/CustomControls/WaynesLabel.md @@ -23,6 +23,21 @@ Private Sub Form_Load() End Sub ``` +Because [**BackgroundFill**](#backgroundfill) and [**TextRendering**](#textrendering) accept the same [**Fill**](Styles/Fill) gradients and the same [**Outlines**](Styles/TextRendering#outlines) array as any other control, a label can serve as a banner, header strip, or status panel without dropping a heavier control onto the form. Setting [**TextRendering.OverflowMode**](Styles/TextRendering#overflowmode) to **tbShrinkToFit** keeps a dynamically-set caption visible even when it is wider than the label: + +```tb +With Label1.TextRendering + .Font.Size = 24 + .Font.Weight = tbBold + .Alignment = tbAlignMiddleCenter + .OverflowMode = tbShrinkToFit + .Fill.SetSimplePattern vbWhite, &HCCCCFF, _ + Pattern:=tbGradientNorthToSouth +End With +Label1.BackgroundFill.SetSimplePattern &H014C99, &H99CCFF, _ + Pattern:=tbGradientNorthWestToSouthEast +``` + ## Properties ### Anchors diff --git a/docs/Reference/CustomControls/WaynesSlider/index.md b/docs/Reference/CustomControls/WaynesSlider/index.md index 55f832ec..42aff717 100644 --- a/docs/Reference/CustomControls/WaynesSlider/index.md +++ b/docs/Reference/CustomControls/WaynesSlider/index.md @@ -24,6 +24,26 @@ Private Sub Form_Load() End Sub ``` +[**Value**](#value) is just a **Long** property — assigning to it from outside the control moves the block and triggers a repaint. Combined with a [**WaynesTimer**](../WaynesTimer), the slider can animate itself across its range: + +```tb +Private Sub Form_Load() + sldProgress.MinValue = 0 + sldProgress.MaxValue = 100 + sldProgress.Value = 0 + Timer1.Interval = 50 + Timer1.Enabled = True +End Sub + +Private Sub Timer1_Timer() + If sldProgress.Value < sldProgress.MaxValue Then + sldProgress.Value = sldProgress.Value + 1 + Else + Timer1.Enabled = False + End If +End Sub +``` + * TOC {:toc} diff --git a/docs/Reference/CustomControls/WaynesTextBox/index.md b/docs/Reference/CustomControls/WaynesTextBox/index.md index 789699c1..57a94f7b 100644 --- a/docs/Reference/CustomControls/WaynesTextBox/index.md +++ b/docs/Reference/CustomControls/WaynesTextBox/index.md @@ -22,6 +22,28 @@ Private Sub Form_Load() End Sub ``` +The three states are styled independently — a common pattern is to give the focused state a heavier border in an accent colour and brighten its fill, so the active field stands out from its siblings: + +```tb +Private Sub Form_Load() + With txtName.NormalState + .BackgroundFill.ColorPoints.SetSolidColor vbWhite + .Borders.SetSimpleBorder StrokeSize:=1, ColorRGB:=&HC0C0C0 + .Corners.SetAll tbCurve, 4 + .TextRendering.Padding.Left = 6 + .TextRendering.Padding.Right = 6 + End With + + With txtName.FocusedState + .BackgroundFill.ColorPoints.SetSolidColor vbWhite + .Borders.SetSimpleBorder StrokeSize:=2, ColorRGB:=&HC07014 ' accent blue + .Corners.SetAll tbCurve, 4 + .TextRendering.Padding.Left = 6 + .TextRendering.Padding.Right = 6 + End With +End Sub +``` + * TOC {:toc}