Net-Base Magasin

14.06.2026

Delphi WebView2 i FMX: ryddig initialisering, bygge JS-Bridge, kontroll over nedlastinger og feilsøking

WebView2 i FireMonkey høres ut som «bare å bygge inn en nettleser», men i praksis svikter det ved initialisering, navigasjons-hendelser, JS↔Delphi-bro, nedlastningshåndtering og feilsøking. Denne kodesnutten viser et robust mønster med klare ansvarsgrenser...

14.06.2026

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

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 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, valgfri cid (Correlation-ID) og payload er vedlikeholdbare og testbare.
  • Driftssikker persistens: en kontrollert UserDataFolder forhindrer 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 i Destroy
  • Tillat InitializeAsync bare é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.

Drøft prosjekt eller moderniseringsinitiativ med Net-Base.

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.

Del innlegg

Del dette innlegget direkte

LinkedIn, X, XING, Facebook, WhatsApp og e‑post er umiddelbart tilgjengelig. For Instagram forbereder vi lenke og kort tekst umiddelbart.

E-post

Instagram åpnes i en ny fane. Lenken og kortteksten kopieres først til utklippstavlen.