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“.
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, opzionalmentecid(Correlation-ID) epayloadsono manutenibili e testabili. - Persistenza operativa sicura: un
UserDataFoldercontrollato 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
Destroyrichiamare correttamenteremove_* - Consentire
InitializeAsyncsolo 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.