Frå magasinetema til prosjektpraksis
Passande teneste- og tekniske sider til innlegget
Den som i ei eksisterande bedriftsprogramvare plutseleg vil «kjapt» integrere moderne webinnhald, endar på Windows med WebView2. I Delphi WebView2 FMX er grunnproblemet sjeldan å vise ei URL, men den reine integrasjonen i ei FireMonkey-overflate (FMX), påliteleg initialisering (asynkront og COM-basert), samt Edge-fallar knytte til User-Data-Verzeichnisse, nedlastingar, debugging og robust JS↔Delphi-kommunikasjon.
Denne kjeldekode-snutt viser eit mønster eg føretrekk for vedlikehaldne applikasjonar: eit innkapsla «Host»-objekt som kontrollerer WebView2-lifecycle, samt ei definert bro over WebMessage (JSON), i staden for vilkårleg «ExecuteScript» overalt. Målet er ikkje demo-kode, men ein byggekloss som overlever i vaksne klientar.
Kvifor WebView2 i FMX er annleis enn «Browser-Component drop»
WebView2 er eit COM/WinRT-nært API med asynkron initialisering. FireMonkey abstrakterer Windows-handles, likevel treng du for WebView2 til slutt eit ekte parent-Window (HWND) og kontrollert vidareføring av resize-/focus-hendingar. Samtidig køyrer eventer ikkje alltid der du ventar i FMX. Dersom du startar «quick and dirty» her, får du typisk:
- sporadiske AV-ar ved Form-lukking (Callbacks blir kalla etter Destroy)
- Navigasjons-Events frå feil tråd-kontekst
- upålitelege persistens-/cache-problem på grunn av uklar UserDataFolder-strategi
- ingen nedlastingar eller «hengande» nedlastingsdialogar
- Debugging berre via flaks i staden for målretta remote-debug-konfigurasjon
Motmiddelet er ein klar lifecycle: Create → InitializeAsync → Attach → Navigate → Detach/Dispose – og ei definert grense mellom UI og nettlesar-enginen.
Source-Schnipsel: WebView2Host für Delphi WebView2 FMX
Følgjande kode skisserer ei innkapsla Host-klasse som (1) opprettar ei WebView2-Environment-konfigurasjon, (2) binder Controller-objektet til eit HWND, (3) koplar opp Navigation- og Download-Events, og (4) tilbyr ei JSON-basert JS-bridge via WebMessageReceived. Koden er medvite arkitekturvennleg: han innkapslar COM-referansar, forhindrar callback-etterslepp etter Destroy, og tillet driftsmodellar 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 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 er allereie øydelagt.');
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 er enno ikkje initialisert.');
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ærminga
- Livssyklusinnkapsling: FMX-Form kjenner berre «Initialize/Navigate/Resize», ikkje COM-detaljar.
- Bridge med kontrakt: JSON-meldingar med
name, valfricid(Correlation-ID) ogpayloader lette å vedlikehalde og testbare. - Driftsikker persistens: eit kontrollert
UserDataFolderhindrar cache-kollisjonar, tilgangsrettarproblem og at det «køyrer på utviklarmaskinen, ikkje i drift».
JS↔Delphi-Bridge: kvifor WebMessage er meir stabil enn ExecuteScript
WebView2 tilbyr fleire kommunikasjonsvegar. I praksis er ExecuteScript freistande, men vanskeleg å versjonere: ein skyv strengar inn i ein tolkar utan tydelege svar-kanalar og utan robust feilkartlegging. PostWebMessageAsString / WebMessageReceived er derimot ein definert kanal.
Eit grense-tilfelle som ofte dukkar opp i bedriftsmiljø: du må starte ein Delphi-arbeidsflyt frå eit web-frontend (f.eks. internt portal) (utskrift, einhetstilgang, legacy-integrasjon). Då treng du:
- ein kvitelist over meldingsnamn
- Correlation-IDs for asynkrone svar
- eit sentralt punkt som validerer payloadar (f.eks. obligatoriske felt, storleiksgrenser)
I hosten er det punktet OnWebMessageReceived. Den egentlege valideringa høyrer til i eit overliggande lag (f.eks. Application-Service), slik at UI-/WebView2-teknikk og forretningslogikk blir haldne atskilt (klassisk lag-arkitektur: UI → Application → Domain → Infrastruktur).
Nedlastingar og fillagring: kva som ofte overraskar i drift
Nedlastingar i WebView2 går gjennom ICoreWebView2DownloadOperation. Avhengig av kjelde kan ResultFilePath tidleg vere tom eller først bli sett seinare. I tillegg ønskjer mange føretak ikkje at sluttbrukarar skal lagre i ukontrollerte mappar.
Anbefalte mønster:
- Fange opp
DownloadStartingog medargs.put_Handled(1)overta UI-en sjølv (eigen sti, namnekonvensjon, karantene-mappe). - Grenser for filstorleikar og MIME-type-kontrollar for å unngå «utilsikta 4 GB loggfil».
- Audit: skriv ned metadata for nedlasting (URI, MIME, byte-tal) i loggsystemet, ikkje innhaldet.
Dersom de har regulerte prosessar (f.eks. godkjenningar, sporbarheit), er handteringa via hendingane den einaste staden kor nettlesarverda kan integrerast i driftsreglane.
Debugging: DevTools, Remote Debug Port og reproduserbare tilstandar
WebView2-debugging feilar ofte fordi tilstandar ikkje er reproduserbare. To tiltak hjelper:
- Aktivere/deaktivere DevTools via
ICoreWebView2Settings(i koden:SetDevToolsEnabled) – i release ofte av, i support-tilfelle målretta på. - Stabilt
UserDataFolder: Når support skal gjenskape ein feil, er ein definert sti gull verdt. Ein kan sikkerheitskopiere/zippe mappa (Merk: personvern/PII) og samanlikne tilstandar målretta.
Valfritt (avhengig av wrapper) kan ein gi EnvironmentOptions ekstra nettlesarargument, t.d. ein Remote-Debug-Port. Dette er nyttig når ein må analysere ei applikasjon på eit testsystem utan lokale utviklarverktøy. Grensar: i produksjonsmiljø må dette verte ryddig godkjend og dokumentert, elles opnar det ei unødig angrepsflate.
Fallgruver i Delphi WebView2 FMX: COM, trådar og skjema-livssyklus
1) Callbackar etter at skjemaet er lukka
Asynkrone CompletedHandler kan kome inn etter at skjemaet allereie er i ferd med å lukkast. I snipppet hindrar FDestroyed tilgang til frigjorde objekt. Endå meir robust er i tillegg:
- Lagre token for hendingar og kall
remove_*ryddig iDestroy - Tillat InitializeAsync berre éin gong (state-machine: Created/Initializing/Ready/Disposed)
2) Tråd-kontekst
Mange handler kjem «UI-nært», men stol ikkje på at du kan skrive direkte i FMX-kontrollar. Når du oppdaterer UI i OnWebMessage, er TThread.Queue(nil, ...) den trygge varianten. Eg delar ofte opp: Host samlar hendinga, applikasjonstenesta avgjer, UI blir utelukkande oppdatert via Queue.
3) DPI/Resize og FMX-layouts
FMX reknar i logiske einingar, WebView2 forventar pixel-rects. I praksis treng du eit klart punkt der du omset bounds frå FMX-kontrollar til faktiske pikslar. Snippet tek eit TRect; i skjemaet ditt bør du avlede WinAPI-koordinatar frå dette (t.d. via FMX.Platform.Win og Handle-APIar). Når appen skalerer etter monitor-DPI, test overgangen mellom skjermar: WebView2 er her meir sensitiv enn reine FMX-kontrollar.
Når WebView2 i FMX lønner seg – og når ikkje
WebView2 løner seg når du i ei etablert Delphi-klientapplikasjon målretta vil bruke webteknologi: innbette admin-views, OAuth/OIDC-login-flows, HTML-rapportar, interne portalar eller kontrollerte «Micro-Frontends». Også som ei moderniseringsbru er det praktisk, så lenge du skiller ansvar klart og ikkje lèt brua bli ei ukontrollert bakdør for forretningslogikk.
Begrensningar ved tilnærminga:
- Plattform: Mønsteret er Windows-sentrert. FMX er multiplatform, WebView2 er det ikkje. For macOS/iOS/Android treng du andre WebViews eller eit abstraksjonslag.
- Security/Hardening: Så snart eksternt innhald blir lasta, må du strengare avgrense navigasjon, tillatne domener og nedlastingsmål. Dette høyrer heime i kravspesifikasjonen, ikkje «seinare».
- Support: UserDataFolder og runtime-avhengigheiter (WebView2 Runtime) må vere del av drifts-/utrullingskonseptet ditt.
Konklusjon
Delphi WebView2 FMX er mindre eit UI-gadget enn ein integrasjonskomponent med eigen livssyklus. Dersom du kapslar inn initialisering, eventing, UserDataFolder og JS-Bridge strukturert, blir WebView2 ein stabil byggestein for digitale bedriftsløysingar: Web-UI der det gir meining, og Delphi-logikk der den høyrer heime. Dersom du derimot fyrer av skript utan kontroll, overlèt vegar til tilfeldet og ikkje koplar frå hendingar, får du nett den typen «sporadisk i felt»-feil som et tid og svekkjer tillit.
Om du vil integrere WebView2 ryddig i ei eksisterande Delphi-applikasjon eller teknisk vurdere ein moderniseringskant, kontakt oss:
I fagmiljøet spelar òg Webview2 Firemonkey og Delphi Fmx Edge Browser ei viktig rolle når integrasjonar, dataflyt og vidareutvikling må fungere saman på ein ryddig måte.
Neste steg
Når temaet blir eit reelt prosjekt, bør arkitektur, eksisterande system og drift vurderast tidleg saman.
Vi støttar ikkje berre ved enkeltspørsmål, men òg når korte kildekodesnuttar, legacy-tema eller portalidéar skal utviklast til eit robust bedriftsprosjekt.
- Eksisterande tilstand, målbiletet og tekniske risikoar blir vurderast samla.
- REST, datatilgang, portalar og utrulling blir ikkje utsette til seinare som etterverknader.
- De ser tidleg kva veg som er økonomisk og driftsmessig berekraftig.