Net-Base Magasin

14.06.2026

Delphi WebView2 i FMX: initialisere skikkeleg, bygge JS-Bridge, ha kontroll på nedlastingar og feilsøking

WebView2 i FireMonkey høyrast ut som «berre å bygge inn ein nettlesar», men sviktar i praksis ved initialisering, navigasjonshendingar, JS↔Delphi-Bridge, handtering av nedlastingar og feilsøking. Denne kodesnutten visar eit robust mønster med tydelege ansvarsområde...

14.06.2026

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-engi­nen.

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».

Delphi
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, valfri cid (Correlation-ID) og payload er lette å vedlikehalde og testbare.
  • Driftsikker persistens: eit kontrollert UserDataFolder hindrar 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 DownloadStarting og med args.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 i Destroy
  • 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.

Prosjekt eller moderniseringsprosjekt med Net-Base drøfte.

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.

Del innlegg

Del dette innlegget direkte

LinkedIn, X, XING, Facebook, WhatsApp og e-post er tilgjengelege med ein gong. For Instagram klargjer vi lenke og kort tekst med det same.

E-post

Instagram opnar i ein ny fane. Lenkje og kort tekst blir kopiert til utklippstavla på førehand.