Net-Base Magasin

14.06.2026

Delphi WebView2 i FMX: korrekt initialisering, opbygning af JS-Bridge, styring af downloads og debugging

WebView2 i FireMonkey lyder som 'bare at indlejre en browser', men i praksis svigter det ved initialisering, navigation-events, JS↔Delphi-bro, download-håndtering og debugging. Dette kildekodeudsnit viser et robust mønster med klare ansvarsområder...

14.06.2026

Fra magasinets tema til projektpraksis

Passende service- og tekniske sider til artiklen

Den, som i en eksisterende business-software pludselig „lige skal“ indlejre moderne webindhold, ender på Windows med WebView2. I Delphi WebView2 FMX er grundproblemet sjældent blot at vise en URL, men den korrekte indlejring i en FireMonkey-overflade (FMX), den pålidelige initialisering (asynkron og COM-baseret), samt Edge-faldgruberne omkring User-Data-mapper, downloads, debugging og en robust JS↔Delphi-kommunikation.

Dette source-udsnit viser et mønster, jeg foretrækker til vedligeholdelige applikationer: et indkapslet „Host“-objekt, der styrer WebView2-lifecycle, samt en defineret bridge via WebMessage (JSON), i stedet for vilkårlig „ExecuteScript alle vegne“. Målet er ikke demo-kode, men en byggesten, der overlever i voksede klienter.

Hvorfor WebView2 i FMX er anderledes end et „Browser-Component drop“

WebView2 er et COM/WinRT-nært API med asynkron initialisering. FireMonkey abstraherer Windows-handles, alligevel har du til WebView2 i sidste ende brug for et ægte parent-window (HWND) og kontrolleret resize-/focus-videresendelse. Samtidig kører events ikke altid dér, hvor man forventer dem i FMX. Hvis du her starter „quick and dirty“, får du typisk:

  • sporadiske AV’er ved form-lukning (callbacks rammer ind efter Destroy)
  • navigation-events fra en forkert trådkontekst
  • upålidelig persistence/cache-problemer pga. uklar UserDataFolder-strategi
  • ingen downloads eller „fastlåste“ download-dialoger
  • debugging kun ved held i stedet for målrettet remote-debug-konfiguration

Modgiften er en klar lifecycle: Create → InitializeAsync → Attach → Navigate → Detach/Dispose – og en defineret grænse mellem UI og browser-engine.

Source-udsnit: WebView2Host til Delphi WebView2 FMX

Den følgende kode skitserer en indkapslet host-klasse, der (1) opretter en WebView2-environment-konfiguration, (2) binder controller-objektet til et HWND, (3) ledningsfører navigation- og download-events og (4) tilbyder en JSON-baseret JS-bridge via WebMessageReceived. Koden er bevidst arkitektur-egnet: den indkapsler COM-referencer, forhindrer callback-efterløbere efter Destroy, og tillader driftskant-scenarier som separate UserDataFolder for „pro User“ eller „pro Maschine“.

unit WebView2Host;

interface

uses
System.SysUtils, System.Classes, System.IOUtils, System.JSON,
Winapi.Windows, Winapi.ActiveX,
FMX.Types,
WebView2, WebView2_TLB; // je nach Setup: WebView2.pas oder Import-TLB

type
TWebView2JsonMessage = record
Name: string;
CorrelationId: string;
Payload: TJSONObject;
class function TryParse(const Json: string; out Msg: TWebView2JsonMessage): Boolean; static;
end;

TOnWebMessage = reference to procedure(const Msg: TWebView2JsonMessage);
TOnDownload = reference to procedure(const FileName, MimeType: string; TotalBytes: Int64);

TWebView2Host = class
private
FParentHwnd: HWND;
FUserDataFolder: string;
FEnvironment: ICoreWebView2Environment;
FController: ICoreWebView2Controller;
FWebView: ICoreWebView2;
FDestroyed: Boolean;
FOnWebMessage: TOnWebMessage;
FOnDownload: TOnDownload;

procedure EnsureNotDestroyed;
function MakeUserDataFolder: string;

// Event handler
procedure HookEvents;
procedure UnhookEvents;

procedure OnWebMessageReceived(
const sender: ICoreWebView2;
const args: ICoreWebView2WebMessageReceivedEventArgs);

procedure OnDownloadStarting(
const sender: ICoreWebView2;
const args: ICoreWebView2DownloadStartingEventArgs);

public
constructor Create(AParentHwnd: HWND; const AUserDataFolder: string = “);
destructor Destroy; override;

procedure InitializeAsync;
procedure Navigate(const Url: string);
procedure Resize(const Bounds: TRect);

procedure PostJsonToWeb(const Obj: TJSONObject);
procedure SetDevToolsEnabled(const Enabled: Boolean);

property WebView: ICoreWebView2 read FWebView;
property OnWebMessage: TOnWebMessage read FOnWebMessage write FOnWebMessage;
property OnDownload: TOnDownload read FOnDownload write FOnDownload;
end;

implementation

{ TWebView2JsonMessage }

class function TWebView2JsonMessage.TryParse(const Json: string; out Msg: TWebView2JsonMessage): Boolean;
var
V: TJSONValue;
O: TJSONObject;
begin
Result := False;
Msg.Name := “;
Msg.CorrelationId := “;
Msg.Payload := nil;

V := TJSONObject.ParseJSONValue(Json);
try
if not (V is TJSONObject) then Exit;
O := TJSONObject(V);

Msg.Name := O.GetValue(’name‘, “);
Msg.CorrelationId := O.GetValue(‚cid‘, “);

// Payload kann fehlen oder null sein
if O.TryGetValue(‚payload‘, Msg.Payload) then
Msg.Payload := TJSONObject(Msg.Payload.Clone)
else
Msg.Payload := TJSONObject.Create;

Result := Msg.Name <> “;
finally
V.Free;
end;
end;

{ TWebView2Host }

constructor TWebView2Host.Create(AParentHwnd: HWND; const AUserDataFolder: string);
begin
inherited Create;
FParentHwnd := AParentHwnd;
FUserDataFolder := AUserDataFolder;
FDestroyed := False;
end;

destructor TWebView2Host.Destroy;
begin
FDestroyed := True;

// Events lösen, bevor COM-Objekte freigegeben werden
UnhookEvents;

FWebView := nil;
FController := nil;
FEnvironment := nil;

inherited;
end;

procedure TWebView2Host.EnsureNotDestroyed;
begin
if FDestroyed then
raise EInvalidOperation.Create(‚WebView2Host ist bereits zerstört.‘);
end;

function TWebView2Host.MakeUserDataFolder: string;
begin
if FUserDataFolder <> “ then
Exit(FUserDataFolder);

// Praxis: pro App + pro Windows-User, nicht in Programmverzeichnis
Result := TPath.Combine(TPath.GetHomePath, ‚AppDataLocalMyCompanyMyAppWebView2‘);
ForceDirectories(Result);
end;

procedure TWebView2Host.InitializeAsync;
var
UserData: string;
Opt: ICoreWebView2EnvironmentOptions;
begin
EnsureNotDestroyed;

UserData := MakeUserDataFolder;

// Options: hier können zusätzliche Browser-Argumente rein, z.B. Remote-Debug
Opt := TCoreWebView2EnvironmentOptions.Create;

// Async CreateEnvironment
OleCheck(CreateCoreWebView2EnvironmentWithOptions(
nil, PWideChar(UserData), Opt,
TCoreWebView2CreateCoreWebView2EnvironmentCompletedHandler.Create(
procedure (errorCode: HRESULT; const createdEnvironment: ICoreWebView2Environment)
begin
if FDestroyed then Exit;
OleCheck(errorCode);

FEnvironment := createdEnvironment;

// Controller an Parent HWND binden
OleCheck(FEnvironment.CreateCoreWebView2Controller(
FParentHwnd,
TCoreWebView2CreateCoreWebView2ControllerCompletedHandler.Create(
procedure (errorCode2: HRESULT; const createdController: ICoreWebView2Controller)
begin
if FDestroyed then Exit;
OleCheck(errorCode2);

FController := createdController;
OleCheck(FController.get_CoreWebView2(FWebView));

HookEvents;

// Initial sichtbar machen
FController.put_IsVisible(1);
end)));
end)));
end;

procedure TWebView2Host.HookEvents;
var
TokenMsg, TokenDl: EventRegistrationToken;
begin
if (FWebView = nil) then Exit;

// WebMessageReceived (JS->Delphi)
TokenMsg.value := 0;
OleCheck(FWebView.add_WebMessageReceived(
TCoreWebView2WebMessageReceivedEventHandler.Create(
procedure(const sender: ICoreWebView2; const args: ICoreWebView2WebMessageReceivedEventArgs)
begin
if FDestroyed then Exit;
OnWebMessageReceived(sender, args);
end), TokenMsg));

// DownloadStarting
TokenDl.value := 0;
OleCheck(FWebView.add_DownloadStarting(
TCoreWebView2DownloadStartingEventHandler.Create(
procedure(const sender: ICoreWebView2; const args: ICoreWebView2DownloadStartingEventArgs)
begin
if FDestroyed then Exit;
OnDownloadStarting(sender, args);
end), TokenDl));

// Hinweis: Für robustes Unhooking sollten Sie die Tokens speichern.
// In vielen Projekten ist das ausreichend, wenn der Host nur mit dem Form lebt.
end;

procedure TWebView2Host.UnhookEvents;
begin
// Robust-Variante: Tokens merken und remove_* aufrufen.
// Hier als Kommentar, weil das Import-Unit-Setup und Token-Verwaltung je nach Wrapper variiert.
end;

procedure TWebView2Host.OnWebMessageReceived(
const sender: ICoreWebView2;
const args: ICoreWebView2WebMessageReceivedEventArgs);
var
Json: PWideChar;
S: string;
Msg: TWebView2JsonMessage;
begin
Json := nil;
OleCheck(args.TryGetWebMessageAsString(Json));
try
S := Json;
finally
CoTaskMemFree(Json);
end;

if Assigned(FOnWebMessage) and TWebView2JsonMessage.TryParse(S, Msg) then
begin
try
FOnWebMessage(Msg);
finally
Msg.Payload.Free;
end;
end;
end;

procedure TWebView2Host.OnDownloadStarting(
const sender: ICoreWebView2;
const args: ICoreWebView2DownloadStartingEventArgs);
var
Dl: ICoreWebView2DownloadOperation;
Uri, Mime, ResultFile: PWideChar;
Total: Int64;
FileName: string;
begin
Uri := nil;
Mime := nil;
ResultFile := nil;

OleCheck(args.get_DownloadOperation(Dl));
OleCheck(Dl.get_TotalBytesToReceive(Total));

// In der Praxis: ResultFileName initial leer, je nach Quelle.
OleCheck(Dl.get_ResultFilePath(ResultFile));
OleCheck(Dl.get_MimeType(Mime));
OleCheck(Dl.get_Uri(Uri));

try
FileName := ExtractFileName(string(ResultFile));
if FileName = “ then
FileName := ‚download.bin‘;

if Assigned(FOnDownload) then
FOnDownload(FileName, string(Mime), Total);

// Optional: eigene Download-UI, dann Handled setzen
// args.put_Handled(1);
finally
CoTaskMemFree(Uri);
CoTaskMemFree(Mime);
CoTaskMemFree(ResultFile);
end;
end;

procedure TWebView2Host.Navigate(const Url: string);
begin
EnsureNotDestroyed;
if FWebView = nil then
raise EInvalidOperation.Create(‚WebView2 ist noch nicht initialisiert.‘);

OleCheck(FWebView.Navigate(PWideChar(Url)));
end;

procedure TWebView2Host.Resize(const Bounds: TRect);
var
R: tagRECT;
begin
if FController = nil then Exit;
R.Left := Bounds.Left;
R.Top := Bounds.Top;
R.Right := Bounds.Right;
R.Bottom := Bounds.Bottom;
OleCheck(FController.put_Bounds(R));
end;

procedure TWebView2Host.PostJsonToWeb(const Obj: TJSONObject);
var
S: string;
begin
EnsureNotDestroyed;
if FWebView = nil then Exit;

S := Obj.ToJSON;
OleCheck(FWebView.PostWebMessageAsString(PWideChar(S)));
end;

procedure TWebView2Host.SetDevToolsEnabled(const Enabled: Boolean);
var
Settings: ICoreWebView2Settings;
begin
if (FWebView = nil) then Exit;
OleCheck(FWebView.get_Settings(Settings));
OleCheck(Settings.put_AreDevToolsEnabled(Ord(Enabled)));
end;

end.

  • Livscyklus-indkapsling: FMX-formularen kender kun „Initialize/Navigate/Resize“, ikke COM-detaljer.
  • Bridge med kontrakt: JSON-meddelelser med name, valgfrit cid (Correlation-ID) og payload er vedligeholdelses- og testbare.
  • Driftssikker persistens: en kontrolleret UserDataFolder forhindrer cache-kollisioner, rettighedsproblemer og »kører på udviklermaskiner, ikke i drift«.

JS↔Delphi-Bridge: hvorfor WebMessage er mere stabil end ExecuteScript

WebView2 tilbyder flere kommunikationsveje. I praksis er ExecuteScript fristende, men svært at versionere: Man skubber strenge ind i en fortolker uden klare svar-kanaler og uden robust error-mapping. PostWebMessageAsString / WebMessageReceived er derimod en defineret kanal.

Et grænsetilfælde, som ofte optræder i virksomheds-miljøer: Du skal fra et web-frontend (fx et internt portal) starte en Delphi-workflow (tryk, enhedsadgang, legacy-integration). Så har du brug for:

  • en whitelist af message-navne
  • Correlation-IDs til asynkrone svar
  • et centralt sted, der validerer payloads (fx obligatoriske felter, størrelsesbegrænsninger)

I hosten er det punktet OnWebMessageReceived. Den egentlige validering hører hjemme i et lag ovenfor (fx Application-Service), så du holder UI/WebView2-teknik og forretningslogik adskilt (klassisk Layer-arkitektur: UI → Application → Domain → Infrastruktur).

Downloads og filhåndtering: hvad der ofte overrasker i drift

Downloads i WebView2 styres af ICoreWebView2DownloadOperation. Afhængig af kilde kan ResultFilePath tidligt være tom eller først blive sat senere. Derudover ønsker mange virksomheder ikke, at slutbrugere gemmer i ukontrollerede mapper.

Anbefalede mønstre:

  • Affang DownloadStarting og overtag UI selv via args.put_Handled(1) (egen sti, navnekonvention, karantænemappe).
  • Grænser for filstørrelse og MIME-type-tjek for at undgå »ved et uheld 4 GB logfil«.
  • Auditing: skriv download-metadata (URI, MIME, bytes) til dit logging, ikke indholdet.

Hvis du har regulerede processer (fx godkendelser, sporbarhed), er håndteringen via events det eneste sted, hvor du kan integrere browser-verdenen i dine driftregler.

Debugging: DevTools, Remote Debug Port og reproducerbare tilstande

WebView2-debugging fejler ofte, fordi tilstande ikke er reproducerbare. To justeringspunkter hjælper:

  • Aktiver/deaktiver DevTools via ICoreWebView2Settings (i koden: SetDevToolsEnabled) – typisk slået fra i release, men målrettet aktiveret i support-situationer.
  • Stabilt UserDataFolder: Hvis dit supportteam skal reproducere en fejl, er en defineret sti meget værd. Du kan arkivere/zippe mappen (obs: databeskyttelse/PII) og sammenligne tilstande målrettet.

Valgfrit (afhængig af wrapper) kan du give EnvironmentOptions yderligere browser-argumenter, fx en Remote-Debug-Port. Det er nyttigt, når du skal analysere en applikation på et testsystem uden lokale udviklerværktøjer. Grænser: I produktionsmiljøer skal det frigives og dokumenteres ordentligt, ellers introducerer du en unødvendig angrebsflade.

Stolperfælder i Delphi WebView2 FMX: COM, tråde og form-lifecycle

1) Callbacks efter lukning

De asynkrone CompletedHandler kan ankomme, efter at Form allerede er ved at lukke. I eksemplet forhindrer FDestroyed adgang til frigivne objekter. Endnu mere robust er desuden:

  • Gem tokens for events og kald i Destroy rent remove_*
  • Tillad kun InitializeAsync én gang (State-Machine: Created/Initializing/Ready/Disposed)

2) Tråd-kontekst

Mange handlers kommer „UI-nært“, men stol ikke på, at du kan skrive direkte i FMX-Controls. Hvis du opdaterer UI i OnWebMessage, er TThread.Queue(nil, ...) den sikre variant. Jeg deler gerne ansvaret: Host indsamler hændelsen, Application-Service træffer beslutningen, UI opdateres udelukkende via Queue.

3) DPI/Resize og FMX-layouts

FMX regner i logiske enheder, WebView2 forventer pixel-rects. I praksis har du brug for et entydigt sted, hvor du oversætter bounds fra FMX-Controls til faktiske pixels. Snippetet antager en TRect; i din Form bør du aflede WinAPI-koordinaterne derfra (f.eks. via FMX.Platform.Win og Handle-APIs). Hvis appen skaleres per Monitor-DPI, test overgangen mellem skærme: WebView2 er her mere følsom end rene FMX-Controls.

Hvornår WebView2 i FMX giver mening — og hvornår ikke

WebView2 giver mening, når du i en etableret Delphi-client-applikation vil anvende webteknologi målrettet: indlejrede admin-views, OAuth/OIDC-login-flows, HTML-rapporter, interne portaler eller kontrollerede „Micro-Frontends“. Også som en moderniseringsbro er det praktisk, så længe ansvarsområderne skæres klart, og broen ikke bliver en ukontrolleret bagdør til business-logic.

Begrænsninger ved tilgangen:

  • Platform: Mønstret er Windows-centreret. FMX er multiplatform, WebView2 er det ikke. For macOS/iOS/Android skal du bruge andre WebViews eller et abstraktionslag.
  • Sikkerhed/Hardening: Så snart eksternt indhold indlæses, skal du indskrænke navigation, tilladte domæner og download-destinationer strengere. Det hører hjemme i kravspecifikationen, ikke „senere“.
  • Support: UserDataFolder og runtime-afhængigheder (WebView2 Runtime) skal være en del af dit drift-/rollout-koncept.

Konklusion

Delphi WebView2 FMX er mindre et UI-gadget og mere en integrationskomponent med egen livscyklus. Hvis du kapsler initialisering, eventing, UserDataFolder og JS-Bridge struktureret, bliver WebView2 en stabil byggesten i digitale virksomheds‑løsninger: Web‑UI der, hvor det giver mening, og Delphi‑logik der, hvor den hører hjemme. Hvis du derimod affyrer scripts ukontrolleret, overlader stier til tilfældet og ikke afkobler events, får du præcis den slags „sporadisk i felten“‑fejl, som spiser tid og koster tillid.

Hvis du vil integrere WebView2 korrekt i en eksisterende Delphi-applikation eller teknisk vurdere en moderniseringskants muligheder, så tal med os:

I det faglige miljø spiller også Webview2 Firemonkey og Delphi Fmx Edge Browser en vigtig rolle, når integrationer, dataflow og videreudvikling skal spille sammen på en ren måde.

Drøft projekt eller moderniseringsinitiativ med Net-Base.

Næste trin

Når et emne bliver til et reelt projekt, bør arkitektur, eksisterende systemer og drift tidligt vurderes samlet.

Vi støtter ikke kun ved enkeltspørsmål, men også når kildekodeudsnit, legacy-komponenter eller portalidéer skal udvikles til et robust virksomhedsprojekt.

  • Eksisterende tilstand, målbillede og tekniske risici vurderes samlet.
  • REST, dataadgang, portaler og idrulning bliver ikke udskudt som eftertanker.
  • I ser tidligt, hvilken vej der er økonomisk og driftsmæssigt holdbar.

Del indlæg

Del dette indlæg direkte

LinkedIn, X, XING, Facebook, WhatsApp og e-mail er straks tilgængelige. Til Instagram forbereder vi link og kort tekst med det samme.

E-mail

Instagram åbner i en ny fane. Linket og kortteksten kopieres på forhånd til udklipsholderen.