Fra magasinetema til prosjektpraksis
Egnede tjeneste- og tekniske sider for innlegget
Den som i eksisterende forretningsprogramvare plutselig vil «raskt» integrere moderne web-innhold, ender på Windows med WebView2. I Delphi WebView2 FMX er grunnproblemet sjelden å vise en URL, men heller ren integrering i et FireMonkey-grensesnitt (FMX), pålitelig initialisering (asynkron og COM-basert), samt Edge-fallgruver rundt User-Data-kataloger, nedlastinger, debugging og en robust JS↔Delphi-kommunikasjon.
Denne kodesnutten viser et mønster jeg foretrekker for vedlikeholdsvennlige applikasjoner: et innkapslet «Host»-objekt som kontrollerer WebView2-livssyklusen, samt en definert bridge over WebMessage (JSON), i stedet for vilkårlig «ExecuteScript overalt». Målet er ikke demo-kode, men en byggekloss som overlever i modne klienter.
Hvorfor WebView2 i FMX er annerledes enn «Browser-Component drop»
WebView2 er en COM/WinRT-nær API med asynkron initialisering. FireMonkey abstraherer Windows-handles, likevel trenger du for WebView2 til slutt et ekte parent-window (HWND) og kontrollert resize-/focus-videreformidling. Samtidig kjører hendelser ikke alltid der man forventer i FMX. Hvis du starter «quick and dirty» her, får du typisk:
- sporadiske AV-er ved Form-lukking (Callbacks treffer etter Destroy)
- navigasjons-hendelser fra feil trådkontekst
- upålitelige persistens-/cache-problemer på grunn av uklar UserDataFolder-strategi
- ingen nedlastinger eller «hengende» nedlastingsdialoger
- debugging kun ved flaks i stedet for målrettet Remote-Debug-konfigurasjon
Motmiddelet er en klar livssyklus: Create → InitializeAsync → Attach → Navigate → Detach/Dispose – og en definert grense mellom UI og nettlesermotor.
Kodesnutt: WebView2Host for Delphi WebView2 FMX
Følgende kode skisserer en innkapslet host-klasse som (1) oppretter en WebView2-environment-konfigurasjon, (2) binder controller-objektet til et HWND, (3) kopler navigasjons- og nedlastingshendelser, og (4) tilbyr en JSON-basert JS-bridge via WebMessageReceived. Koden er bevisst «arkitektur-egnet»: den kapsler COM-referanser, hindrer callback-etterslengere etter Destroy, og tillater driftsvarianter som separate UserDataFolder «per bruker» eller «per maskin».
unit WebView2Host;
interface
uses
System.SysUtils, System.Classes, System.IOUtils, System.JSON,
Winapi.Windows, Winapi.ActiveX,
FMX.Types,
WebView2, WebView2_TLB; // avhengig av oppsett: 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 mangle eller være 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;
// Koble fra hendelser før COM-objekter frigis
UnhookEvents;
FWebView := nil;
FController := nil;
FEnvironment := nil;
inherited;
end;
procedure TWebView2Host.EnsureNotDestroyed;
begin
if FDestroyed then
raise EInvalidOperation.Create('WebView2Host er allerede ødelagt.');
end;
function TWebView2Host.MakeUserDataFolder: string;
begin
if FUserDataFolder <> '' then
Exit(FUserDataFolder);
// Praksis: per app + per Windows-bruker, ikke i programkatalogen
Result := TPath.Combine(TPath.GetHomePath, 'AppDataLocalMyCompanyMyAppWebView2');
ForceDirectories(Result);
end;
procedure TWebView2Host.InitializeAsync;
var
UserData: string;
Opt: ICoreWebView2EnvironmentOptions;
begin
EnsureNotDestroyed;
UserData := MakeUserDataFolder;
// Options: her kan du legge til ekstra nettleser-argumenter, f.eks. Remote-Debug
Opt := TCoreWebView2EnvironmentOptions.Create;
// Opprett miljøet asynkront
OleCheck(CreateCoreWebView2EnvironmentWithOptions(
nil, PWideChar(UserData), Opt,
TCoreWebView2CreateCoreWebView2EnvironmentCompletedHandler.Create(
procedure (errorCode: HRESULT; const createdEnvironment: ICoreWebView2Environment)
begin
if FDestroyed then Exit;
OleCheck(errorCode);
FEnvironment := createdEnvironment;
// Bind controller til parent 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;
// Gjør synlig initialt
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));
// Merk: For robust avregistrering bør du lagre tokenene.
// I mange prosjekter er dette tilstrekkelig hvis hosten lever bare så lenge formen gjør.
end;
procedure TWebView2Host.UnhookEvents;
begin
// Robust variant: husk tokenene og kall remove_*.
// Her som kommentar, fordi import-enhet-oppsett og token-håndtering varierer med 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 praksis: ResultFileName kan være tomt i starten, avhengig av kilde.
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);
// Valgfritt: egen nedlastings-UI, sett da 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 er ikke initialisert ennå.');
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.
Formålet med tilnærmingen
- Livssyklus-innkapsling: FMX-formen kjenner bare «Initialize/Navigate/Resize», ikke COM-detaljer.
- Bro med kontrakt: JSON-meldinger med
name, valgfricid(Correlation-ID) ogpayloader vedlikeholdbare og testbare. - Driftssikker persistens: en kontrollert
UserDataFolderforhindrer cache-kollisjoner, tilgangsproblemer og «kjører på utviklermaskinen, ikke i produksjon».
JS↔Delphi-bro: hvorfor WebMessage er mer stabil enn ExecuteScript
WebView2 tilbyr flere kommunikasjonsveier. I praksis er ExecuteScript fristende, men vanskelig å versjonere: man sender strenger til en tolker uten klare svarkanaler og uten robust feilmapping. PostWebMessageAsString / WebMessageReceived er derimot en definert kanal.
Et grensefall som ofte oppstår i bedriftsmiljøer: man må starte en Delphi-arbeidsflyt fra et web-frontend (f.eks. internt portal) (utskrift, enhetstilgang, legacy-integrasjon). Da trenger man:
- en hvitliste over meldingsnavn
- Correlation-IDer for asynkrone svar
- et sentralt sted som validerer payloads (f.eks. obligatoriske felt, størrelsesbegrensninger)
I hosten er det punktet OnWebMessageReceived. Den egentlige valideringen hører til i et overliggende lag (f.eks. Application-Service), slik at UI-/WebView2-teknikk og forretningslogikk holdes atskilt (klassisk lagdelt arkitektur: UI → Application → Domain → Infrastruktur).
Nedlastinger og fillagring: hva som ofte overrasker i drift
Nedlastinger håndteres i WebView2 via ICoreWebView2DownloadOperation. Avhengig av kilde kan ResultFilePath være tom tidlig eller først bli satt senere. I tillegg vil mange virksomheter ikke at sluttbrukere skal lagre i ukontrollerte mapper.
Velprøvde mønstre:
- Avbryt DownloadStarting og ta over UI selv med
args.put_Handled(1)(egen sti, navnekonvensjon, karantennemappe). - Filstørrelsesgrenser og MIME-type-sjekk for å unngå «utilsiktet 4 GB loggfil».
- Auditing: skriv nedlastingsmetadata (URI, MIME, byteantall) til loggen deres, ikke innholdet.
Hvis dere har regulerte prosesser (f.eks. godkjenninger, sporbarhet), er håndteringen via hendelsene det eneste stedet hvor dere kan integrere nettleserverdenen i deres driftsregler.
Feilsøking: DevTools, Remote Debug Port og reproducerbare tilstander
WebView2-feilsøking svikter ofte fordi tilstander ikke er reproducerbare. To tiltak hjelper:
- Aktivere/deaktivere DevTools via
ICoreWebView2Settings(i koden:SetDevToolsEnabled) – ofte slått av i release, og målrettet slått på i supporttilfeller. - Stabilt UserDataFolder: Hvis supporten deres skal gjenskape en feil, er en definert sti gull verdt. Dere kan ta backup/zippe mappen (Merk: personvern/PII) og sammenligne tilstander målrettet.
Valgfritt (avhengig av wrapper) kan man legge til EnvironmentOptions med ekstra nettleser-argumenter, f.eks. en Remote-Debug-Port. Det er nyttig når man må analysere en applikasjon på et testsystem uten lokale utviklerverktøy. Begrensninger: i produksjonsmiljøer må dette åpnes og dokumenteres ryddig, ellers oppretter man en unødvendig angrepsflate.
Fallgruver i Delphi WebView2 FMX: COM, tråder og form-livssyklus
1) Callbacks etter lukking
De asynkrone CompletedHandler kan dukke opp etter at formen allerede er i ferd med å lukke. I eksemplet forhindrer FDestroyed tilgang til frigjorte objekter. Mer robust er i tillegg:
- Lagre tokener for hendelser og kall
remove_*ryddig iDestroy - Tillat
InitializeAsyncbare én gang (tilstandsmaskin: Created/Initializing/Ready/Disposed)
2) Trådkontekst
Mange handler kommer riktignok «UI-nært», men stol ikke på at du kan skrive direkte i FMX-kontroller. Hvis du oppdaterer UI i OnWebMessage, er TThread.Queue(nil, ...) den sikre varianten. Jeg deler gjerne ansvar slik: Host samler hendelsen, Application-Service avgjør, og UI oppdateres utelukkende via Queue.
3) DPI/Endring av størrelse og FMX-layouts
FMX regner i logiske enheter, WebView2 forventer pixel-rects. I praksis trenger du et klart sted der du oversetter bounds fra FMX-kontroller til ekte piksler. Snippetet antar en TRect; i din form bør du avlede WinAPI-koordinater fra dette (f.eks. via FMX.Platform.Win og handle-APIer). Hvis applikasjonen skaleres per monitor-DPI, test bytte mellom skjermer: WebView2 er her mer følsom enn rene FMX-kontroller.
Når WebView2 i FMX lønner seg – og når ikke
WebView2 lønner seg når du i en moden Delphi-klientapplikasjon vil bruke webteknologi målrettet: innebygde admin-views, OAuth/OIDC-login-flows, HTML-rapporter, interne portaler eller kontrollerte «Micro-Frontends». Også som en moderniseringsbro er det praktisk, så lenge dere deler ansvarsområdene klart og ikke gjør broen til en ukontrollert bakdør for forretningslogikk.
Begrensninger ved tilnærmingen:
- Plattform: Mønsteret er Windows-sentrert. FMX er flerplattform, WebView2 er det ikke. For macOS/iOS/Android trenger dere andre WebViews eller et abstraksjonslag.
- Sikkerhet/Hardening: Når eksternt innhold lastes, må dere begrense navigasjon, tillatte domener og nedlastingsmål strengere. Dette hører hjemme i kravene, ikke «senere».
- Support: UserDataFolder og runtime-avhengigheter (WebView2 Runtime) må være del av deres drift-/utrullingskonsept.
Konklusjon
Delphi WebView2 FMX er mindre et UI-gadget enn en integrasjonskomponent med egen livssyklus. Hvis dere kapsler initialisering, eventing, UserDataFolder og JS-Bridge strukturert, blir WebView2 en stabil byggestein for digitale virksomhetsløsninger: Web-UI der det gir mening, og Delphi-logikk der den hører hjemme. Hvis dere derimot fyrer av skript ukontrollert, overlater stier til tilfeldighetene og ikke entkobler hendelser, får dere nøyaktig den typen ‘sporadisk i feltet’-feil som spiser tid og koster tillit.
Hvis dere ønsker å integrere WebView2 ryddig i en eksisterende Delphi-applikasjon eller teknisk vurdere en moderniseringskant, ta kontakt med oss:
I faglig sammenheng spiller også Webview2 Firemonkey og Delphi Fmx Edge Browser en viktig rolle når integrasjoner, dataflyter og videreutvikling må spille godt sammen.
Neste steg
Når et tema blir et reelt prosjekt, bør arkitektur, eksisterende systemer og drift tidlig vurderes samlet.
Vi bistår ikke bare med enkeltspørsmål, men også når kodesnutter, legacy-temaer eller portalideer skal utvikles til et robust virksomhetsprosjekt.
- Eksisterende tilstand, målbildet og tekniske risikoer vurderes samlet.
- REST, datatilgang, portaler og utrulling blir ikke utsatt som sene følger.
- Dere ser tidlig hvilken vei som er økonomisk og driftsmessig levedyktig.