WebView
WidgetTree::webView() embeds a web view in your window. There are three backends, chosen at compile time — your PHP code is identical either way. Where capabilities differ, the table says so:
| Backend | Where | JS | Remote http(s):// | data: | Local file | zoom |
|---|---|---|---|---|---|---|
| WebView2 | Windows, when the SDK is enabled | ✅ | ✅ | ✅ | ✅ | ✅ |
| WKWebView | macOS (enabled by default) | ✅ | ✅ | ✅ | ✅ | ✅ |
| QTextBrowser | Linux, and when both switches above are off | ❌ | ❌ | ❌ | ✅ | ❌ silently ignored |
QTextBrowser is Qt's built-in subset HTML renderer with no network stack at all: hand it https://… and you get an empty box, without an error. Apps that need a remote page or JS should degrade via webViewSupportsJs() rather than rely on it.
Ask which one you got:
$app->webViewBackend(); // 'webview2' | 'wkwebview' | 'textbrowser'
$app->webViewSupportsJs(); // bool
Use it to degrade gracefully rather than showing a blank box:
$app->view(function () use ($state, $app): array {
return WidgetTree::vbox([
$app->webViewSupportsJs()
? WidgetTree::webView($state->url, ['id' => 'wv', 'grow' => 1])
: WidgetTree::label('This page needs JavaScript; the current backend cannot run it.',
['id' => 'fallback']),
]);
});
Two ways to feed it
// 1. a URL — remote, or a local file resolved against the executable's directory
WidgetTree::webView('https://example.com', ['id' => 'wv']);
WidgetTree::webView('assets/help.html', ['id' => 'wv']); // local file
// 2. an HTML string, rendered directly
WidgetTree::html('<h1>Hello</h1><p>Rendered inline.</p>', ['id' => 'wv']);
| Property | Meaning |
|---|---|
url | Address to load. Anything with a scheme (http://, https://, file://, data:) is passed to the backend as-is — but only WebView2 / WKWebView understand those schemes; QTextBrowser accepts file: and nothing else. Without a scheme it is treated as a local path resolved against the executable's directory (then the working directory), which all three backends support |
html | HTML string to render directly. Takes precedence over url when both are set |
zoom | Zoom factor (WebView2 / WKWebView; QTextBrowser has no zoom concept and ignores it silently) |
Events
| Event | When | value | payload |
|---|---|---|---|
loaded | navigation finished | the final URL | success (bool) |
title | document title changed | the title | — |
navigating | navigation started | the target URL | — |
link | a new-window request (target="_blank") | the URL | — |
link does not open anything by itself — it reports, and you decide. That is the same state-driven convention as the link widget:
$app->on('wv', 'link', function (array $event) use ($app) {
$app->setStatus(['Blocked popup: ' . $event['value']]);
});
Navigation actions
Three imperative actions, for building a browser-style toolbar. They are not properties — there is no "currently reloading" state to write — so they are one-shot calls:
$app->webViewReload('wv'); // reload the current page
$app->webViewGoBack('wv'); // go back (no-op when there is no history)
$app->webViewGoForward('wv'); // go forward (no-op when there is no history)
They are driven through the same call channel as the table's appendRows / clear, so they take effect immediately rather than waiting for the next render.
| Backend | reload / goBack / goForward |
|---|---|
| WebView2 | ✅ |
| WKWebView (macOS) | ✅ |
| QTextBrowser | ❌ silently ignored — it has no navigation stack |
The no-op is deliberate, matching the "unknown property does not raise" convention: you do not need to branch on webViewBackend() before calling them.
Enabling WebView2
It is on by default in projects created by qtphp new on Windows. The switch is two lines in project.yml:
link-paths:
- ../../third_party/webview2/x64 # the vendored SDK
link-libs:
- WebView2Loader.dll.lib
cxx-flags:
- /DQT_WEBVIEW2
- /I"../../third_party/webview2/include"
Comment out /DQT_WEBVIEW2 and it falls back to QTextBrowser — the app still compiles and runs, just without JS.
qtphp build deploys the 195 KB WebView2Loader.dll next to your executable automatically. The runtime is not bundled: WebView2 apps need the WebView2 Runtime installed on the target machine, which Windows 10/11 ship with Edge. If it is missing, the widget shows an explanatory message instead of a blank box.
Enabling WKWebView (macOS)
project.macos.yml generated by qtphp new has it on by default. The switch is two lines in cxx-flags:
cxx-flags:
- -DQT_WEBVIEW_WK # use the system WebKit
- -fobjc-arc # WKWebView* members live in a C++ class, so ARC is required
ld-flags:
- -Wl,-framework,WebKit
- -Wl,-framework,Foundation
- -Wl,-framework,AppKit
Comment out -DQT_WEBVIEW_WK and it falls back to QTextBrowser — the app still compiles and runs, just without JS.
This route needs no install: WebKit is a system framework, and the binary does not grow. We deliberately did not use Homebrew's qtwebview module — on macOS it drives that same system web view anyway (official docs: "On macOS, the system web view is used in the same manner as iOS"), while its packaging drags QtWebEngine (gigabytes) along with it.
The cost is that cpp-src/qt_webview_wk.mm must appear in the macOS sources. tpc's config merge replaces lists rather than appending to them, so the macOS entry rewrites the whole source list before adding that one line — which also means the Windows / Linux entries, which never list it, can never end up compiling Objective-C++.
Caveats
--shot cannot capture the native backends
WebView2 paints into its own child window and WKWebView is grafted onto the widget's native NSView — both sit outside Qt's painting system, so QWidget::grab() (which --shot uses) excludes that area and the PNG comes out blank there. This is a limitation of the backend, not a bug.
The QTextBrowser backend is drawn by Qt and therefore is captured.
To verify a native backend actually loaded and ran a page, use events instead of pixels: have the page's script set document.title and handle the title event — that title only appears if JS really executed (loaded works the same way).
--selftest and --difftest are unaffected: they dispatch events and never look at pixels.
Give it a height
A web view contributes nothing to Qt's layout size calculation (the native backend's pixels live outside the Qt widget tree). The widget ships a sensible default and a minimum height, but if you see it collapsed to a thin strip, give it 'grow' => 1 inside a container that can expand — and make sure the window itself is tall enough for the rest of the content.
JavaScript is unavailable on QTextBrowser
Anything that renders client-side — SPAs, most modern sites — will come out empty. Check webViewSupportsJs() and show your own fallback. Static HTML, documentation pages and simple reports work fine on all three backends.
Related
- Widget Catalog — the other controls
- Properties — common properties (
grow,size, …) - Packaging — what ships next to the binary