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, valgfritcid(Correlation-ID) ogpayloader vedligeholdelses- og testbare. - Driftssikker persistens: en kontrolleret
UserDataFolderforhindrer 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
DownloadStartingog overtag UI selv viaargs.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
Destroyrentremove_* - 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.
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.