Net-Base Magazine

14.06.2026

Delphi WebView2 in FMX: juist initialiseren, JS-Bridge implementeren, downloads en debugging onder controle

WebView2 in FireMonkey klinkt alsof je gewoon een browser insluit, maar faalt in de praktijk bij initialisatie, navigatie-events, JS↔Delphi-Bridge, downloadafhandeling en debugging. Dit broncodefragment laat een robuust patroon zien met duidelijke verantwoordelijkheden...

14.06.2026

Van magazinethema naar projectpraktijk

Relevante dienst- en technische pagina's bij het artikel

Wie in bestaande bedrijfssoftware plotseling ‚even snel‘ moderne webinhoud wil insluiten, stuit bij Windows op WebView2. In Delphi WebView2 FMX is het fundamentele probleem zelden het tonen van een URL, maar de nette integratie in een FireMonkey‑interface (FMX), het betrouwbare initialiseren (asynchroon en COM‑gebaseerd), en de Edge-valkuilen rond User‑Data‑mappen, downloads, debugging en een robuuste JS↔Delphi‑communicatie.

Dit broncodefragment toont een patroon dat ik voor onderhoudbare toepassingen prefereer: een ingekapseld ‚Host‘-object dat de WebView2‑lifecycle regelt, en een gedefinieerde bridge via WebMessage (JSON), in plaats van willekeurig ‚ExecuteScript‘ overal. Het doel is geen demo‑code, maar een bouwsteen die in gegroeide clients overleeft.

Waarom WebView2 in FMX anders is dan een „Browser-Component drop”

WebView2 is een COM/WinRT‑nauwkeurige API met asynchrone initialisatie. FireMonkey abstraheert Windows‑handles, maar uiteindelijk heeft u voor WebView2 een echt parent‑window (HWND) en gecontroleerde resize-/focus‑doorgave nodig. Tegelijkertijd lopen events niet altijd op de plekken waarop men ze in FMX verwacht. Als u hier ‚quick and dirty‘ begint, krijgt u typisch:

  • sporadische AVs bij het sluiten van het formulier (callbacks komen na Destroy binnen)
  • navigatie‑events vanuit een verkeerde thread‑context
  • onbetrouwbare persistentie/cache‑problemen door een onduidelijke UserDataFolder‑strategie
  • geen downloads of ‚hangende‘ downloaddialogen
  • debugging alleen toevallig in plaats van via een gerichte remote‑debugconfiguratie

Het tegengif is een duidelijke lifecycle: Create → InitializeAsync → Attach → Navigate → Detach/Dispose – en een gedefinieerde grens tussen UI en browser‑engine.

Broncodefragment: WebView2Host voor Delphi WebView2 FMX

De volgende code schetst een ingekapselde host‑klasse die (1) een WebView2‑Environment‑configuratie aanmaakt, (2) het Controller‑object aan een HWND bindt, (3) navigatie‑ en downloadevents bedrading geeft en (4) een op JSON gebaseerde JS‑bridge via WebMessageReceived aanbiedt. De code is bewust „architectuurgeschikt“: hij kapselt COM‑referenties, voorkomt callback‑nazwermen na Destroy, en maakt bedrijfsgrenzen mogelijk zoals gescheiden UserDataFolder voor ‚pro User‘ of ‚pro Maschine‘.

Delphi
unit WebView2Host;

interface

uses
  System.SysUtils, System.Classes, System.IOUtils, System.JSON,
  Winapi.Windows, Winapi.ActiveX,
  FMX.Types,
  WebView2, WebView2_TLB; // afhankelijk van de setup: WebView2.pas of 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 ontbreken of null zijn
    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 ontkoppelen voordat COM-objecten worden vrijgegeven
  UnhookEvents;

  FWebView := nil;
  FController := nil;
  FEnvironment := nil;

  inherited;
end;

procedure TWebView2Host.EnsureNotDestroyed;
begin
  if FDestroyed then
    raise EInvalidOperation.Create('WebView2Host is al vernietigd.');
end;

function TWebView2Host.MakeUserDataFolder: string;
begin
  if FUserDataFolder <> '' then
    Exit(FUserDataFolder);

  // Praktijk: per app + per Windows-gebruiker, niet in de programmamap
  Result := TPath.Combine(TPath.GetHomePath, 'AppDataLocalMyCompanyMyAppWebView2');
  ForceDirectories(Result);
end;

procedure TWebView2Host.InitializeAsync;
var
  UserData: string;
  Opt: ICoreWebView2EnvironmentOptions;
begin
  EnsureNotDestroyed;

  UserData := MakeUserDataFolder;

  // Opties: hier kunnen extra browserargumenten worden toegevoegd, bijv. Remote-Debug
  Opt := TCoreWebView2EnvironmentOptions.Create;

  // Asynchroon 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 aan parent-HWND koppelen
        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 zichtbaar maken
              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));

  // Opmerking: voor robuust unhooken moet u de tokens opslaan.
  // In veel projecten is dat voldoende als de host slechts zo lang leeft als het formulier.
end;

procedure TWebView2Host.UnhookEvents;
begin
  // Robuuste variant: tokens onthouden en remove_* aanroepen.
  // Hier als commentaar, omdat de import-unit-setup en tokenbeheer per wrapper kunnen verschillen.
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 de praktijk: ResultFileName aanvankelijk leeg, afhankelijk van de bron.
  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);

    // Optioneel: eigen download-UI, dan Handled instellen
    // 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 is nog niet geïnitialiseerd.');

  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.

Doel van de aanpak

  • Levenscyclus-inkapseling: Het FMX-formulier kent alleen „Initialize/Navigate/Resize“, geen COM-details.
  • Bridge met contract: JSON-berichten met name, optioneel cid (Correlation-ID) en payload zijn onderhoudbaar en testbaar.
  • Bedrijfsklare persistentie: een gecontroleerd UserDataFolder voorkomt cache-collisies, rechtenproblemen en het probleem dat iets „op de ontwikkelaarsmachine werkt, maar niet in productie”.

JS↔Delphi-Bridge: waarom WebMessage stabieler is dan ExecuteScript

WebView2 biedt meerdere communicatieroutes. In de praktijk is ExecuteScript verleidelijk, maar lastig te versioneren: u duwt strings in een interpreter zonder duidelijke antwoordkanalen en zonder robuuste foutmapping. PostWebMessageAsString / WebMessageReceived is daarentegen een gedefinieerd kanaal.

Randgeval dat in bedrijfsomgevingen vaak voorkomt: u moet vanuit een web-frontend (bijv. intern portaal) een Delphi-workflow starten (print, apparaattoegang, legacy-integratie). Dan heeft u nodig:

  • een whitelist van message-namen
  • Correlation-IDs voor asynchrone antwoorden
  • een centrale plek die payloads valideert (bijv. verplichte velden, groottebeperkingen)

In de host is dat de handler OnWebMessageReceived. De daadwerkelijke validatie hoort in een daarbovenliggende laag (bijv. Application-Service), zodat u UI-/WebView2-techniek en businesslogica gescheiden houdt (klassieke lagenarchitectuur: UI → Application → Domain → Infrastructuur).

Downloads en bestandsopslag: wat in productie vaak verrast

Downloads lopen in WebView2 via ICoreWebView2DownloadOperation. Afhankelijk van de bron kan ResultFilePath in het begin leeg zijn of pas later worden gezet. Bovendien willen veel bedrijven niet dat eindgebruikers in ongecontroleerde mappen opslaan.

Beproefde patronen:

  • DownloadStarting onderscheppen en met args.put_Handled(1) de UI zelf afhandelen (eigen pad, naamsconventie, quarantaine-map).
  • Bestandsgrootte-limieten en MIME-type-controles, om per ongeluk een 4 GB logbestand te voorkomen.
  • Auditing: schrijf download-metadata (URI, MIME, bytes) naar uw logging, niet de inhoud.

Als u gereguleerde processen heeft (bijv. vrijgaven, traceerbaarheid), is de event-gebaseerde afhandeling de enige plek waar u de browserwereld in uw operationele regels kunt integreren.

Debugging: DevTools, Remote Debug Port en reproduceerbare toestanden

WebView2-debugging faalt vaak doordat toestanden niet reproduceerbaar zijn. Twee instelpunten helpen:

  • DevTools in-/uitschakelen via ICoreWebView2Settings (in de code: SetDevToolsEnabled) – in de release vaak uit, in supportgevallen gericht aan.
  • Stabiel UserDataFolder: als uw support een fout wil reproduceren, is een gedefinieerd pad goud waard. U kunt de map veiligstellen/zippen (let op: privacy/PII) en toestanden doelgericht vergelijken.

Optioneel (afhankelijk van de wrapper) kunt u EnvironmentOptions van extra browserargumenten voorzien, bijvoorbeeld een remote-debug-port. Dat is zinvol als u een applicatie op een testsysteem zonder lokale ontwikkelaarstools moet analyseren. Grenzen: in productieomgevingen moet dit zorgvuldig worden vrijgegeven en gedocumenteerd, anders creëert u een onnodig aanvalsoppervlak.

Stolperfallen in Delphi WebView2 FMX: COM, Threads und Form-Lifecycle

1) Callbacks na het sluiten

De asynchrone CompletedHandler kunnen binnenkomen nadat de Form al sluit. In het snippet voorkomt FDestroyed de toegang tot vrijgegeven objecten. Robuuster is daarnaast:

  • Tokens voor events opslaan en in Destroy netjes remove_* aanroepen
  • InitializeAsync slechts één keer toelaten (State-Machine: Created/Initializing/Ready/Disposed)

2) Thread-context

Veel Handler komen wel „UI-nabij“, maar vertrouw er niet op dat u direct in FMX-Controls kunt schrijven. Als u in OnWebMessage de UI bijwerkt, is TThread.Queue(nil, ...) de veilige variant. Ik scheid graag: de Host verzamelt het event, de Application-Service beslist, de UI wordt uitsluitend via Queue bijgewerkt.

3) DPI/Resize und FMX-Layouts

FMX rekent in logische eenheden, WebView2 verwacht Pixel-Rects. In de praktijk heeft u een duidelijke plek nodig waar u uit FMX-Controls Bounds in echte pixels vertaalt. Het Snippet gaat uit van een TRect; in uw Form moet u daaruit de WinAPI-coördinaten afleiden (bijv. via FMX.Platform.Win en Handle-APIs). Als de app per monitor-DPI schaalt, test dan het wisselen tussen monitoren: WebView2 is hier gevoeliger dan pure FMX-Controls.

Wanneer WebView2 in FMX zinvol is — en wanneer niet

WebView2 loont als u in een gegroeide Delphi-clientapplicatie webtechniek doelgericht wilt inzetten: ingebedde Admin-Views, OAuth/OIDC-login-flows, HTML-Reports, interne portalen of gecontroleerde „Micro-Frontends“. Ook als moderniseringsbrug is het praktisch, zolang u de verantwoordelijkheden scherp snijdt en de bridge niet tot een ongecontroleerde achterdeur voor business-logica maakt.

Beperkingen van de aanpak:

  • Platform: Het patroon is Windows-gecentreerd. FMX is multiplatformfähig, WebView2 is dat niet. Voor macOS/iOS/Android heeft u andere WebViews of een abstractielaag nodig.
  • Security/Hardening: Zodra externe inhoud geladen wordt, moet u navigatie, toegestane domeinen en downloaddoelen strikter beperken. Dat hoort in de Requirements, niet „later“.
  • Support: UserDataFolder en runtime-afhankelijkheden (WebView2 Runtime) moeten deel uitmaken van uw beheer-/Rollout-Konzept.

Conclusie

Delphi WebView2 FMX is minder een UI-gadget dan een integratiecomponent met een eigen lifecycle. Als u initialisatie, eventing, UserDataFolder en JS-Bridge gestructureerd kapselt, wordt WebView2 een stabiele bouwsteen voor digitale bedrijfsoplossingen: Web-UI waar het zin heeft, en Delphi-logica waar die thuishoort. Als u daarentegen onverantwoord scripts afvuurt, paden aan het toeval overlaat en events niet ontkoppelt, krijgt u precies het soort „sporadisch im Feld“-fouten dat tijd vreet en vertrouwen schaadt.

Als u bij een bestaande Delphi-applicatie WebView2 netjes wilt integreren of een moderniseringsknoop technisch wilt beoordelen, neem contact met ons op:

In het vakgebied spelen ook Webview2 Firemonkey en Delphi Fmx Edge Browser een belangrijke rol wanneer integraties, datastromen en doorontwikkeling goed op elkaar moeten aansluiten.

Project of moderniseringsinitiatief met Net-Base bespreken.

Volgende stap

Wanneer het onderwerp een echt project wordt, zouden architectuur, bestaande omgeving en beheer in een vroeg stadium gezamenlijk moeten worden bekeken.

We ondersteunen niet alleen bij individuele vragen, maar ook wanneer uit broncodefragmenten, legacy-onderwerpen of portalideeën een robuust bedrijfsproject moet ontstaan.

  • Huidige situatie, doelbeeld en technische risico's worden gezamenlijk beoordeeld.
  • REST, gegevens‑toegang, portalen en uitrol worden niet als latere gevolgen uitgesteld.
  • U ziet vroeg welke weg economisch en operationeel houdbaar is.

Bericht delen

Dit bericht direct delen

LinkedIn, X, XING, Facebook, WhatsApp en e-mail zijn direct beschikbaar. Voor Instagram bereiden we de link en een korte tekst direct voor.

E-mail

Instagram opent in een nieuw tabblad. Link en korte tekst worden van tevoren naar het klembord gekopieerd.