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 trinn

Når et tema blir et reelt prosjekt, bør arkitektur, eksisterende systemer og drift vurderes samlet allerede tidlig i prosessen.

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 etterfølgende oppgaver.
  • 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 tilgjengelige. 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.