Net-Base Magasin

14.06.2026

Delphi WebView2 i FMX: initiera korrekt, bygga JS-Bridge, hantera nedladdningar och felsökning

WebView2 i FireMonkey låter som ’bara att inbädda en webbläsare’, men i praktiken fallerar det vid initialisering, navigationshändelser, JS↔Delphi-bridge, nedladdningshantering och felsökning. Denna kodsnutt visar ett robust mönster med tydliga ansvarsområden...

14.06.2026

Från magasinets tema till projektpraxis

Passande tjänste- och tekniksidor för inlägget

Den som plötsligt vill infoga moderna webb-innehåll „snabbt“ i en befintlig affärsprogramvara hamnar med Windows ofta hos WebView2. I Delphi WebView2 FMX är grundproblemet sällan att visa en URL, utan en ren integration i en FireMonkey-yta (FMX), pålitlig initialisering (asynkron och COM-baserad) samt Edge-fällorna kring User-Data-kataloger, nedladdningar, felsökning och en robust JS↔Delphi-kommunikation.

Denna kodsnutt visar ett mönster jag föredrar för underhållbara applikationer: ett kapslat „Host“-objekt som kontrollerar WebView2-livscykeln, samt en definierad bridge över WebMessage (JSON), istället för godtycklig „ExecuteScript överallt“. Målet är inte demo-kod utan en byggsten som överlever i befintliga klienter.

Varför WebView2 i FMX skiljer sig från ett „Browser-Component drop“

WebView2 är ett COM/WinRT-nära API med asynkron initialisering. FireMonkey abstraherar Windows-handles, men för WebView2 behöver du i slutändan ett riktigt parent-fönster (HWND) och kontrollerad vidarebefordran av resize-/focus-händelser. Samtidigt körs händelser inte alltid där du i FMX förväntar dig dem. Om du börjar „quick and dirty“ här får du typiskt:

  • sporadiska AVs vid formulärstängning (callbacks anropas efter Destroy)
  • navigationshändelser som körs i fel trådkontext
  • opålitlig persistens/cache-problem på grund av oklar UserDataFolder-strategi
  • inga nedladdningar eller „hängande“ nedladdningsdialoger
  • felsökning endast via tur istället för en målinriktad remote-debug-konfiguration

Motmedlet är en tydlig livscykel: Create → InitializeAsync → Attach → Navigate → Detach/Dispose – och en definierad gräns mellan UI och browsermotorn.

Källkodsexempel: WebView2Host för Delphi WebView2 FMX

Följande kod skissar en kapslad host-klass som (1) skapar en WebView2-environment-konfiguration, (2) binder controller-objektet till ett HWND, (3) kopplar ihop navigation- och nedladdningshändelser och (4) erbjuder en JSON-baserad JS-bridge via WebMessageReceived. Koden är medvetet „arkitekturanpassad“: den kapslar COM-referenser, förhindrar callback-eftersläntrare efter Destroy och tillåter driftskonfigurationer som separata UserDataFolder för „per user“ eller „per machine“.

unit WebView2Host;

interface

uses
System.SysUtils, System.Classes, System.IOUtils, System.JSON,
Winapi.Windows, Winapi.ActiveX,
FMX.Types,
WebView2, WebView2_TLB; // beroende på setup: WebView2.pas eller 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 kan saknas eller vara null
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;

// Avregistrera händelser innan COM-objekt frigörs
UnhookEvents;

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

inherited;
end;

procedure TWebView2Host.EnsureNotDestroyed;
begin
if FDestroyed then
raise EInvalidOperation.Create(‚WebView2Host har redan förstörts.‘);
end;

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

// I praktiken: per app + per Windows-användare, inte i programkatalogen
Result := TPath.Combine(TPath.GetHomePath, ‚AppDataLocalMyCompanyMyAppWebView2‘);
ForceDirectories(Result);
end;

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

UserData := MakeUserDataFolder;

// Options: här kan ytterligare webbläsarargument anges, t.ex. fjärrdebugging
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;

// Binda controllern till förälderns HWND
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;

// Gör initialt synlig
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));

// Observera: För robust avregistrering bör du spara token.
// I många projekt är det tillräckligt om hosten bara lever så länge formuläret.
end;

procedure TWebView2Host.UnhookEvents;
begin
// Robust variant: spara token och anropa remove_*.
// Här som kommentar eftersom import-unit-setup och tokenhantering varierar beroende på wrapper.
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));

// I praktiken: ResultFileName initialt tom, beroende på källa.
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);

// Valfritt: egen nedladdnings-UI, sätt då Handled
// 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 är ännu inte initialiserad.‘);

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.

Syftet med angreppssättet

  • Lifecycle-Kapselung: FMX-formuläret känner endast till „Initialize/Navigate/Resize“, inte COM‑detaljer.
  • Bridge mit Vertrag: JSON‑meddelanden med name, valfritt cid (Correlation‑ID) och payload är underhållsbara och testbara.
  • Betriebssichere Persistenz: en kontrollerad UserDataFolder förhindrar cachekollisioner, behörighetsproblem och att det „fungerar på utvecklarens dator men inte i drift“.

JS↔Delphi-Bridge: warum WebMessage stabiler ist als ExecuteScript

WebView2 erbjuder flera kommunikationsvägar. I praktiken är ExecuteScript frestande, men svårt att versionshantera: man skickar strängar till en tolk utan tydliga svarskanaler och utan robust felmappning. PostWebMessageAsString / WebMessageReceived är däremot en definierad kanal.

Ett randfall som ofta uppstår i företagsmiljöer: ni måste från ett webbfrontend (t.ex. internt portal) starta en Delphi‑arbetsflöde (utskrift, enhetsåtkomst, legacy‑integration). Då behöver ni:

  • en vitlista över meddelandenamn
  • Correlation‑ID:er för asynkrona svar
  • en central plats som validerar payloads (t.ex. obligatoriska fält, storleksgränser)

I hosten är det platsen OnWebMessageReceived. Den egentliga valideringen hör hemma i ett överliggande lager (t.ex. Application‑Service), så att ni håller UI/WebView2‑teknik och affärslogik separata (klassisk lagerarkitektur: UI → Application → Domain → Infrastruktur).

Nedladdningar och filhantering: vad som ofta överraskar i drift

Nedladdningar i WebView2 hanteras via ICoreWebView2DownloadOperation. Beroende på källa kan ResultFilePath vara tomt tidigt eller sättas först senare. Dessutom vill många företag inte att slutanvändare sparar till okontrollerade mappar.

Beprövade mönster:

  • Fånga DownloadStarting och överta UI:n via args.put_Handled(1) (egen sökväg, namngivningskonvention, karantänmapp).
  • Gränser för filstorlek och MIME‑typkontroller för att undvika att av misstag ladda ner en 4 GB loggfil.
  • Auditering: skriv nedladdningsmetadata (URI, MIME, byteantal) till er loggning, inte innehållet.

Om ni har reglerade processer (t.ex. godkännanden, spårbarhet) är hanteringen via eventen den enda platsen där ni kan integrera webbläsarens värld i era driftregler.

Felsökning: DevTools, Remote Debug Port och reproducerbara tillstånd

WebView2‑felsökning fallerar ofta därför att tillstånd inte är reproducerbara. Två justeringar hjälper:

  • Aktivera/inaktivera DevTools via ICoreWebView2Settings (i kod: SetDevToolsEnabled) – i release ofta avstängt, vid supportfall aktiverat selektivt.
  • Stabilt UserDataFolder: Om er support ska återskapa ett fel är en definierad sökväg ovärderlig. Ni kan säkra/zippa mappen (observera: dataskydd/PII) och jämföra tillstånd målmedvetet.

Valfritt (beroende på wrapper) kan ni ge EnvironmentOptions ytterligare browserargument, t.ex. en Remote‑Debug‑Port. Det är vettigt när ni behöver analysera en applikation på ett testsystem utan lokala utvecklarverktyg. Begränsningar: i produktionsmiljö måste detta aktiveras och dokumenteras ordentligt, annars skapar ni en onödig angreppsyta.

Stolperfallen in Delphi WebView2 FMX: COM, Threads und Form-Lifecycle

1) Callbacks efter stängning

De asynkrona CompletedHandler kan anlända efter att formuläret redan stängts. I utdraget hindrar FDestroyed åtkomst till frigjorda objekt. Robustare är dessutom:

  • Lagra tokens för events och anropa i Destroy konsekvent remove_*
  • Tillåt InitializeAsync endast en gång (tillståndsmaskin: Created/Initializing/Ready/Disposed)

2) Trådkontext

Många handlers kommer visserligen „UI-nära“, men förlita er inte på att ni kan skriva direkt i FMX-kontroller. Om ni i OnWebMessage uppdaterar UI är TThread.Queue(nil, ...) den säkrare varianten. Jag delar gärna ansvar: Host samlar händelsen, applikationsservicen avgör, UI uppdateras uteslutande via Queue.

3) DPI/Resize och FMX-layouter

FMX räknar i logiska enheter, WebView2 förväntar sig pixel-rects. I praktiken behöver ni en tydlig plats där ni översätter bounds från FMX-kontroller till faktiska pixlar. Utdraget förutsätter ett TRect; i er form bör ni härleda WinAPI-koordinater därifrån (t.ex. via FMX.Platform.Win och handle-API:er). Om appen skalas per monitor-DPI, testa att byta mellan skärmar: WebView2 är här mer känsligt än rena FMX-kontroller.

När WebView2 i FMX är motiverat – och när inte

WebView2 är motiverat när ni i en befintlig Delphi-klientapplikation vill använda webteknik riktat: inbäddade admin-vyer, OAuth/OIDC-inloggningsflöden, HTML-rapporter, interna portaler eller kontrollerade „micro-frontends“. Det är också praktiskt som en moderniseringsbro, så länge ni tydligt avgränsar ansvarsområden och inte låter bron bli en okontrollerad bakdörr för affärslogik.

Begränsningar i tillvägagångssättet:

  • Plattform: Mönstret är Windows-centrerat. FMX är multiplattformsdugligt, WebView2 är det inte. För macOS/iOS/Android behöver ni andra WebViews eller ett abstraktionslager.
  • Säkerhet/Hardening: När externa innehåll laddas måste ni begränsa navigation, tillåtna domäner och nedladdningsmål striktare. Det hör hemma i kravspecifikationen, inte „senare“.
  • Support: UserDataFolder och runtimeberoenden (WebView2 Runtime) måste vara en del av er drift-/rolloutplan.

Slutsats

Delphi WebView2 FMX är mindre ett UI-gadget än en integrationskomponent med egen livscykel. Om ni kapslar initialisering, eventing, UserDataFolder och JS-Bridge strukturerat, blir WebView2 en stabil byggsten för digitala företagslösningar: Web-UI där det är motiverat, och Delphi-logik där den hör hemma. Om ni istället okontrollerat kör skript, låter sökvägar vara slumpmässiga och inte avkopplar events, får ni precis den typ av „sporadiska fel i fält“ som äter tid och undergräver förtroendet.

Om ni vill integrera WebView2 på ett strukturerat sätt i en befintlig Delphi-applikation eller tekniskt utvärdera en moderniseringskant, prata med oss:

I tekniska sammanhang spelar också Webview2 Firemonkey och Delphi Fmx Edge Browser en viktig roll när integrationer, dataflöden och vidareutveckling måste samspela.

Diskutera projekt eller moderniseringsinitiativ med Net-Base.

Nästa steg

När ett ämne blir ett verkligt projekt bör arkitektur, befintliga system och drift behandlas gemensamt redan i ett tidigt skede.

Vi stöder inte bara vid enstaka frågor, utan även när kodsfragment, legacy-frågor eller portalidéer ska utvecklas till ett robust företagsprojekt.

  • Nuläge, målbild och tekniska risker bedöms tillsammans.
  • REST, dataåtkomst, portaler och utrullning skjuts inte upp som sena följder.
  • Ni ser tidigt vilken väg som är ekonomiskt och driftsmässigt bärkraftig.

Dela inlägg

Dela det här inlägget direkt

LinkedIn, X, XING, Facebook, WhatsApp och e‑post är omedelbart tillgängliga. För Instagram förbereder vi länken och en kort text direkt.

E-post

Instagram öppnas i en ny flik. Länken och korttexten kopieras till urklipp först.