Net-Base Rivista

14.06.2026

Delphi WebView2 in FMX: inizializzare correttamente, implementare la JS-Bridge, download e debugging sotto controllo

WebView2 in FireMonkey sembra 'semplice incorporare un browser', ma nella pratica mostra limiti durante l'inizializzazione, negli eventi di navigazione, nella JS↔Delphi-Bridge, nella gestione dei download e nel debugging. Questo frammento di codice sorgente mostra un modello robusto con responsabilità chiaramente definite...

14.06.2026

Dal tema della rivista alla pratica di progetto

Pagine di servizi e tecniche correlate all'articolo

Chi in un software aziendale esistente vuole improvvisamente „mal eben“ inserire contenuti web moderni, arriva su Windows con WebView2. In Delphi WebView2 FMX il problema fondamentale raramente è la visualizzazione di un URL, ma l’integrazione pulita in un’interfaccia FireMonkey (FMX), l’inizializzazione affidabile (asincrona e basata su COM), nonché le insidie di Edge relative a directory User-Data, download, debugging e una comunicazione JS↔Delphi robusta.

Questo frammento di codice mostra un modello che preferisco per applicazioni manutenibili: un oggetto „Host“ incapsulato che controlla il ciclo di vita di WebView2, nonché un bridge definito tramite WebMessage (JSON), invece di un arbitrario „ExecuteScript ovunque“. L’obiettivo non è codice demo, ma un componente che sopravvive nei client maturi.

Perché WebView2 in FMX è diverso da un „Browser-Component drop“

WebView2 è un’API vicina a COM/WinRT con inizializzazione asincrona. FireMonkey astrae gli Windows-handle, tuttavia per WebView2 alla fine serve una vera finestra genitore (HWND) e un inoltro controllato di resize/focus. Allo stesso tempo gli eventi non sempre vengono eseguiti dove ci si aspetta in FMX. Se qui si parte „quick and dirty“, si ottiene tipicamente:

  • AV sporadiche alla chiusura della form (i callback arrivano dopo Destroy)
  • eventi di navigazione eseguiti in un contesto di thread errato
  • problemi di persistenza/cache inaffidabili a causa di una strategia UserDataFolder poco chiara
  • nessun download o dialoghi di download „bloccati“
  • debugging solo per fortuna invece di una configurazione mirata di debug remoto

La contromisura è un ciclo di vita chiaro: Create → InitializeAsync → Attach → Navigate → Detach/Dispose – e un confine definito tra UI e engine del browser.

Frammento di codice: WebView2Host per Delphi WebView2 FMX

Il codice seguente delinea una classe host incapsulata che (1) crea una configurazione di WebView2-Environment, (2) associa l’oggetto controller a un HWND, (3) cabla gli eventi di navigazione e download e (4) fornisce un JS-bridge basato su JSON tramite WebMessageReceived. Il codice è deliberatamente „adatto all’architettura“: incapsula riferimenti COM, previene callback residui dopo Destroy e consente aspetti operativi come UserDataFolder separati „per utente“ o „per macchina“.

Delphi
unit WebView2Host;

interface

uses
  System.SysUtils, System.Classes, System.IOUtils, System.JSON,
  Winapi.Windows, Winapi.ActiveX,
  FMX.Types,
  WebView2, WebView2_TLB; // a seconda della configurazione: WebView2.pas o 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', '');

    // Il payload può mancare o essere 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;

  // Disconnettere gli eventi prima di rilasciare gli oggetti COM
  UnhookEvents;

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

  inherited;
end;

procedure TWebView2Host.EnsureNotDestroyed;
begin
  if FDestroyed then
    raise EInvalidOperation.Create('WebView2Host è già stato distrutto.');
end;

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

  // Prassi: per app e per utente Windows, non nella cartella del programma
  Result := TPath.Combine(TPath.GetHomePath, 'AppDataLocalMyCompanyMyAppWebView2');
  ForceDirectories(Result);
end;

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

  UserData := MakeUserDataFolder;

  // Opzioni: qui è possibile inserire argomenti aggiuntivi per il browser, es. 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;

        // Collegare il controller all'HWND padre
        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;

              // Rendere inizialmente visibile
              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));

  // Nota: per rimuovere in modo robusto gli handler dovreste memorizzare i token.
  // In molti progetti è sufficiente se l'host ha la stessa durata del Form.
end;

procedure TWebView2Host.UnhookEvents;
begin
  // Variante robusta: memorizzare i token e chiamare remove_*.
  // Qui come commento, perché la configurazione dell'unit di importazione e la gestione dei token variano a seconda del 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));

  // Nella pratica: ResultFileName inizialmente vuoto, a seconda della sorgente.
  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);

    // Opzionale: UI di download personalizzata, in tal caso impostare 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 non è ancora inizializzato.');

  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.

Scopo dell’approccio

  • Incapsulamento del ciclo di vita: il form FMX conosce solo „Initialize/Navigate/Resize“, non i dettagli COM.
  • Bridge con contratto: messaggi JSON con name, opzionalmente cid (Correlation-ID) e payload sono manutenibili e testabili.
  • Persistenza operativa sicura: un UserDataFolder controllato evita collisioni di cache, problemi di permessi e il caso «funziona sulla macchina dello sviluppatore, non in produzione».

JS↔Delphi-Bridge: perché WebMessage è più stabile di ExecuteScript

WebView2 offre più vie di comunicazione. In pratica ExecuteScript è seducente, ma difficile da versionare: si inviano stringhe in un interprete, senza canali di risposta chiari e senza un mapping robusto degli errori. PostWebMessageAsString / WebMessageReceived è invece un canale definito.

Caso limite, frequente in ambienti aziendali: dovete avviare da un front-end web (es. portale interno) un workflow Delphi (stampa, accesso a dispositivi, integrazione legacy). In tal caso servono:

  • una whitelist dei nomi dei messaggi
  • Correlation-IDs per risposte asincrone
  • un punto centrale che convalidi i payload (es. campi obbligatori, limiti di dimensione)

Nell’host questo è il punto OnWebMessageReceived. La validazione vera e propria appartiene a uno strato sovrastante (es. Application-Service), in modo da tenere separata la tecnologia UI/WebView2 dalla logica di business (architettura a layer classica: UI → Application → Domain → Infrastructure).

Download e gestione file: cosa sorprende spesso in produzione

I download in WebView2 passano tramite ICoreWebView2DownloadOperation. A seconda della sorgente ResultFilePath può essere vuoto all’inizio o impostato solo più tardi. Inoltre molte aziende non vogliono che gli utenti finali salvino in cartelle non controllate.

Pratiche consolidate:

  • Intercettare DownloadStarting e con args.put_Handled(1) gestire l’UI in proprio (path dedicato, convenzione di denominazione, cartella di quarantena).
  • Limiti di dimensione dei file e controlli sul MIME type, per evitare di salvare involontariamente un logfile da 4 GB.
  • Auditing: scrivere i metadati del download (URI, MIME, byte) nel logging, non il contenuto.

Se avete processi regolamentati (es. approvazioni, tracciabilità), l’handling tramite gli eventi è l’unico punto in cui potete integrare il mondo del browser nelle vostre regole operative.

Debugging: DevTools, Remote Debug Port e stati riproducibili

Il debugging di WebView2 fallisce spesso perché gli stati non sono riproducibili. Due leve aiutano:

  • Abilitare/disabilitare DevTools tramite ICoreWebView2Settings (nel codice: SetDevToolsEnabled) – in release spesso disabilitate, in caso di supporto attivate puntualmente.
  • UserDataFolder stabile: se il vostro support deve riprodurre un errore, un percorso definito vale oro. Potete eseguire il backup/zip della cartella (attenzione: protezione dei dati/PII) e confrontare gli stati in modo mirato.

Opzionalmente (a seconda del wrapper) potete configurare EnvironmentOptions con argomenti aggiuntivi per il browser, ad es. una porta di Remote Debug. Ha senso quando dovete analizzare un’applicazione su un sistema di test senza tool di sviluppo locali. Limiti: in ambienti produttivi deve essere abilitato e documentato con cura, altrimenti create una superficie d’attacco non necessaria.

Insidie in Delphi WebView2 FMX: COM, thread e lifecycle del form

1) Callback dopo la chiusura

I CompletedHandler asincroni possono arrivare dopo che la Form si sta già chiudendo. Nello snippet FDestroyed impedisce l’accesso a oggetti liberati. Più robusto è inoltre:

  • Memorizzare i token per gli eventi e in Destroy richiamare correttamente remove_*
  • Consentire InitializeAsync solo una volta (State-Machine: Created/Initializing/Ready/Disposed)

2) Thread-Kontext

Molti handler arrivano purtroppo “vicini alla UI”, ma non affidatevi al fatto di poter scrivere direttamente nei controlli FMX. Se aggiornate la UI in OnWebMessage, TThread.Queue(nil, ...) è la variante sicura. Io preferisco separare: l’Host raccoglie l’evento, l’Application-Service decide, la UI viene aggiornata esclusivamente tramite Queue.

3) DPI/Resize e FMX-Layouts

FMX lavora in unità logiche, WebView2 si aspetta Pixel-Rects. In pratica serve un punto chiaro dove convertire dai Bounds dei controlli FMX ai pixel reali. Lo snippet presuppone un TRect; nella vostra Form dovreste derivarne le coordinate WinAPI (p. es. tramite FMX.Platform.Win e le Handle-APIs). Se l’app scala in base al Monitor-DPI, testate il passaggio tra monitor: WebView2 è qui più sensibile rispetto ai soli controlli FMX.

Quando conviene usare WebView2 in FMX – e quando no

WebView2 conviene quando volete impiegare tecnologia web in modo mirato in un’applicazione client Delphi consolidata: view amministrative integrate, flussi di login OAuth/OIDC, report HTML, portali interni o «Micro-Frontends» controllati. Anche come ponte di modernizzazione è praticabile, a condizione di definire chiaramente le responsabilità e di non trasformare il bridge in una backdoor incontrollata per la logica di business.

Limiti dell’approccio:

  • Piattaforma: Il pattern è centrato su Windows. FMX è multipiattaforma, WebView2 non lo è. Per macOS/iOS/Android servono altri WebView o uno strato di astrazione.
  • Security/Hardening: Appena vengono caricati contenuti esterni, dovete restringere con maggiore rigore navigazione, domini consentiti e destinazioni dei download. Questo deve essere nei Requirements, non “più tardi”.
  • Support: UserDataFolder e le dipendenze di runtime (WebView2 Runtime) devono far parte del vostro concetto operativo/di rollout.

Conclusione

Delphi WebView2 FMX non è tanto un gadget UI quanto una componente di integrazione con un proprio lifecycle. Se incapsulate in modo strutturato inizializzazione, eventing, UserDataFolder e JS-Bridge, WebView2 diventa un elemento stabile per soluzioni aziendali digitali: Web-UI dove ha senso, e logica Delphi dove le compete. Se invece scatenate script senza controllo, lasciate i percorsi al caso e non disaccoppiate gli eventi, otterrete proprio il tipo di errori “sporadici in produzione” che consumano tempo e minano la fiducia.

Se volete integrare pulitamente WebView2 in una applicazione Delphi esistente o valutare tecnicamente un intervento di modernizzazione, parlate con noi:

Nel contesto professionale hanno anche un ruolo importante Webview2 Firemonkey e Delphi Fmx Edge Browser, quando integrazioni, flussi di dati e sviluppo futuro devono interoperare in modo pulito.

Discutere progetto o iniziativa di modernizzazione con Net-Base.

Passo successivo

Quando un tema diventa un progetto reale, architettura, sistemi esistenti e gestione operativa dovrebbero essere considerati insieme fin dall'inizio.

Non forniamo solo supporto per questioni isolate, ma anche quando da frammenti di codice sorgente, tematiche legacy o idee di portale deve nascere un progetto aziendale solido.

  • Stato attuale, stato obiettivo e rischi tecnici vengono valutati insieme.
  • REST, l'accesso ai dati, i portali e il rollout non vengono rimandati a fasi successive.
  • Vede in anticipo quale percorso è economicamente ed operativamente sostenibile.

Condividi il post

Condividi direttamente questo articolo

LinkedIn, X, XING, Facebook, WhatsApp e e-mail sono immediatamente disponibili. Per Instagram prepariamo direttamente il link e un breve testo.

E-mail

Instagram si apre in una nuova scheda. Il link e il breve testo vengono copiati prima negli appunti.