Net-Base Revista

14.06.2026

Delphi WebView2 en FMX: inicialitzar correctament, implementar una JS-Bridge i tenir les descàrregues i la depuració sota control

WebView2 a FireMonkey sembla «només incrustar un navegador», però a la pràctica es desmunta en la inicialització, els esdeveniments de navegació, el pont JS↔Delphi, la gestió de descàrregues i la depuració. Aquest fragment de codi font mostra un patró robust amb responsabilitats ben definides...

14.06.2026

Del tema de la revista a la pràctica del projecte

Pàgines de serveis i tècniques pertinents per a l'article

Qui en un programari empresarial existent de sobte vol incorporar «rapidament» continguts web moderns, arriba a Windows amb WebView2. En Delphi WebView2 FMX el problema fonamental rarament és mostrar una URL, sinó la integració neta dins d’una interfície FireMonkey (FMX), la inicialització fiable (assíncrona i basada en COM), així com les trampes d’Edge al voltant dels directoris User-Data, les descàrregues, el depurat i una comunicació JS↔Delphi robusta.

Aquest fragment de codi mostra un patró que prefereixo per a aplicacions mantenibles: un objecte “Host” encapsulat que controla el lifecycle de WebView2, i un pont definit via WebMessage (JSON), en lloc d’executar arbitràriament «ExecuteScript» per tot arreu. L’objectiu no és codi demo, sinó un component que sobrevisqui en clients desenvolupats al llarg del temps.

Per què WebView2 en FMX és diferent d’un «Browser-Component drop»

WebView2 és una API propera a COM/WinRT amb inicialització assíncrona. FireMonkey abstrau els manejadors de Windows, però al final per a WebView2 necessiteu una finestra pare real (HWND) i una derivació de redimensionament/focus controlada. Al mateix temps, els esdeveniments no sempre s’executen on s’esperaria dins FMX. Si comenceu aquí de forma «quick and dirty», normalment obtindreu:

  • AVs esporàdiques en tancar el formulari (les callbacks arriben després del Destroy)
  • esdeveniments de navegació des d’un context de fil incorrecte
  • problemes de persistència/caché poc fiables a causa d’una estratègia poc clara de UserDataFolder
  • cap descàrrega o diàlegs de descàrrega «penjats»
  • depuració només per sort en lloc d’una configuració de Remote-Debug dirigida

El remei és un cicle de vida clar: Create → InitializeAsync → Attach → Navigate → Detach/Dispose – i un límit definit entre la UI i la Browser-Engine.

Fragment de codi: WebView2Host per Delphi WebView2 FMX

El codi següent esquematitza una classe Host encapsulada que (1) crea una configuració d’Environment per a WebView2, (2) associa l’objecte Controller a un HWND, (3) connecta els esdeveniments de navegació i descàrrega i (4) ofereix una JS-Bridge basada en JSON via WebMessageReceived. El codi és deliberadament «apte per a arquitectura»: encapsula referències COM, evita callbacks que arriben després del Destroy, i permet estratègies d’operació com separar UserDataFolder «per usuari» o «per màquina».

unit WebView2Host;

interface

uses
System.SysUtils, System.Classes, System.IOUtils, System.JSON,
Winapi.Windows, Winapi.ActiveX,
FMX.Types,
WebView2, WebView2_TLB; // segons la configuració: 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;

// Gestor d’esdeveniments
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 kann fehlen oder null sein
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;

// Desenllaçar els esdeveniments abans d’alliberar els objectes COM
UnhookEvents;

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

inherited;
end;

procedure TWebView2Host.EnsureNotDestroyed;
begin
if FDestroyed then
raise EInvalidOperation.Create(‚WebView2Host ja ha estat destruït.‘);
end;

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

// Pràctica: per aplicació i per usuari Windows, no al directori del programa
Result := TPath.Combine(TPath.GetHomePath, ‚AppDataLocalMyCompanyMyAppWebView2‘);
ForceDirectories(Result);
end;

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

UserData := MakeUserDataFolder;

// Opcions: aquí es poden afegir arguments addicionals del navegador, p. ex. Remote-Debug
Opt := TCoreWebView2EnvironmentOptions.Create;

// Creació asíncrona de l’entorn
OleCheck(CreateCoreWebView2EnvironmentWithOptions(
nil, PWideChar(UserData), Opt,
TCoreWebView2CreateCoreWebView2EnvironmentCompletedHandler.Create(
procedure (errorCode: HRESULT; const createdEnvironment: ICoreWebView2Environment)
begin
if FDestroyed then Exit;
OleCheck(errorCode);

FEnvironment := createdEnvironment;

// Enllaçar el controller amb el HWND pare
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;

// Fer-ho visible inicialment
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 un desenllaç robust, cal desar els tokens.
// En molts projectes és suficient si el host només viu amb el formulari.
end;

procedure TWebView2Host.UnhookEvents;
begin
// Variante robusta: recordar els tokens i cridar remove_*.
// Aquí com a comentari, perquè la configuració de la unit d’importació i la gestió de tokens varia segons el 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));

// A la pràctica: ResultFileName sovint inicialment buit, depenent de la font.
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);

// Opcional: UI de descàrrega pròpia, llavors establir 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 encara no s\’ha inicialitzat.‘);

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.

Zweck des Ansatzes

  • Lifecycle-Kapselung: Die FMX-Form kennt nur „Initialize/Navigate/Resize“, nicht COM-Details.
  • Bridge mit Vertrag: JSON-Nachrichten mit name, optional cid (Correlation-ID) und payload sind wartbar und testbar.
  • Betriebssichere Persistenz: ein kontrolliertes UserDataFolder verhindert Cache-Kollisionen, Rechteprobleme und „läuft auf Entwicklerrechner, nicht im Betrieb“.

JS↔Delphi-Bridge: warum WebMessage stabiler ist als ExecuteScript

WebView2 bietet mehrere Wege der Kommunikation. In der Praxis ist ExecuteScript verführerisch, aber schlecht zu versionieren: Sie schieben Strings in einen Interpreter, ohne klare Antwort-Kanäle und ohne robustes Error-Mapping. PostWebMessageAsString / WebMessageReceived ist dagegen ein definierter Kanal.

Randfall, der in Unternehmensumgebungen häufig auftaucht: Sie müssen aus einem Web-Frontend (z. B. internes Portal) einen Delphi-Workflow starten (Druck, Gerätezugriff, Legacy-Integration). Dann brauchen Sie:

  • eine Whitelist von Message-Namen
  • Correlation-IDs für asynchrone Antworten
  • eine zentrale Stelle, die Payloads validiert (z. B. Pflichtfelder, Größenlimits)

Im Host ist das die Stelle OnWebMessageReceived. Die eigentliche Validierung gehört in eine darüber liegende Schicht (z. B. Application-Service), damit Sie UI-/WebView2-Technik und Business-Logik getrennt halten (klassische Layer-Architektur: UI → Application → Domain → Infrastruktur).

Downloads und Dateiablage: was im Betrieb oft überrascht

Downloads laufen in WebView2 über ICoreWebView2DownloadOperation. Je nach Quelle kann ResultFilePath früh leer sein oder erst später gesetzt werden. Zudem wollen viele Unternehmen nicht, dass Endanwender in unkontrollierte Ordner speichern.

Bewährte Muster:

  • DownloadStarting abfangen und per args.put_Handled(1) die UI selbst übernehmen (eigener Pfad, Namenskonvention, Quarantäne-Ordner).
  • Dateigrößen-Grenzen und MIME-Type-Checks, um „versehentlich 4 GB Logfile“ zu vermeiden.
  • Auditing: Download-Metadaten (URI, MIME, Bytes) in Ihr Logging schreiben, nicht den Inhalt.

Wenn Sie regulierte Prozesse haben (z. B. Freigaben, Nachvollziehbarkeit), ist das Handling über die Events die einzige Stelle, an der Sie die Browser-Welt in Ihre Betriebsregeln integrieren können.

Debugging: DevTools, Remote Debug Port und reproduzierbare Zustände

WebView2-Debugging kippt häufig daran, dass Zustände nicht reproduzierbar sind. Zwei Stellschrauben helfen:

  • DevTools aktivieren/deaktivieren über ICoreWebView2Settings (im Code: SetDevToolsEnabled) – im Release oft aus, im Support-Fall gezielt an.
  • Stabiles UserDataFolder: Wenn Ihr Support einen Fehler nachstellen soll, ist ein definierter Pfad Gold wert. Sie können den Ordner sichern/zippen (Achtung: Datenschutz/PII) und Zustände gezielt vergleichen.

Optional (je nach Wrapper) können Sie EnvironmentOptions mit zusätzlichen Browser-Argumenten versehen, z. B. einen Remote-Debug-Port. Das ist sinnvoll, wenn Sie eine Anwendung auf einem Testsystem ohne lokale Entwickler-Tools analysieren müssen. Grenzen: In produktiven Umgebungen muss das sauber freigeschaltet und dokumentiert sein, sonst schaffen Sie eine unnötige Angriffsfläche.

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

1) Callbacks després del tancament

Els CompletedHandler asíncrons poden arribar després que el formulari ja s’estigui tancant. Al fragment de codi FDestroyed evita l’accés a objectes ja alliberats. Més robust és, a més:

  • Emmagatzemar tokens per als esdeveniments i, a Destroy, cridar netament remove_*
  • Permetre InitializeAsync només una vegada (màquina d’estats: Created/Initializing/Ready/Disposed)

2) Thread-Kontext

Molts handlers arriben «propers a la UI», però no confieu en poder escriure directament contra controls FMX. Si actualitzeu la UI des de OnWebMessage, TThread.Queue(nil, ...) és la via segura. Jo separo: l’host recull l’esdeveniment, l’Application-Service pren la decisió, i la UI s’actualitza exclusivament via Queue.

3) DPI/Resize und FMX-Layouts

FMX calcula en unitats lògiques, mentre que WebView2 espera rects en píxels. A la pràctica cal un punt clar on convertir els bounds dels controls FMX a píxels reals. El fragment assumeix un TRect; a la vostra forma haureu d’obtenir a partir d’això les coordenades WinAPI (per exemple via FMX.Platform.Win i les APIs de handle). Si l’aplicació escala per la DPI del monitor, proveu el canvi entre monitors: WebView2 és en general més sensible que els controls FMX purs.

Quan compensa WebView2 a FMX — i quan no

WebView2 compensa quan voleu fer servir tecnologia web de manera acotada en una aplicació client creixent Delphi: vistes d’administració incrustades, fluxos de login OAuth/OIDC, informes HTML, portals interns o «micro-frontends» controlats. També funciona com a pont de modernització sempre que talleu clarament les responsabilitats i no convertiu el pont en una porta posterior incontrolada per la lògica de negoci.

Límits de l’enfocament:

  • Plattform: El patró està centrat en Windows. FMX és multiplataforma, WebView2 no ho és. Per macOS/iOS/Android necessitareu altres WebViews o una capa d’abstracció.
  • Seguretat/enduriment: Un cop carregueu contingut extern, cal restringir amb més força la navegació, els dominis permesos i els destins de descàrrega. Això ha d’estar definit als requirements, no «més endavant».
  • Support: UserDataFolder i les dependències de runtime (WebView2 Runtime) han de formar part del vostre concepte d’operació i desplegament.

Conclusió

Delphi WebView2 FMX és menys un gadget UI i més una component d’integració amb el seu propi lifecycle. Si encapsuleu estructuradament la inicialització, l’eventing, el UserDataFolder i la JS-Bridge, WebView2 pot ser un bloc estable per a solucions empresarials digitals: Web-UI allà on té sentit, i la lògica Delphi on correspongui. Si, en canvi, dispareu scripts de manera incontrolada, deixeu els camins a l’atzar i no desacopleu els esdeveniments, obtindreu precisament el tipus d’errors «esporàdics al camp» que consumeixen temps i minen la confiança.

Si voleu integrar WebView2 de manera neta en una aplicació Delphi existent o avaluar tècnicament una vora de modernització, parleu amb nosaltres:

En l’entorn tècnic també tenen un paper important Webview2 Firemonkey i Delphi Fmx Edge Browser quan la integració, els fluxos de dades i l’evolució han de funcionar conjuntament de manera neta.

Parlar del projecte o del pla de modernització amb Net-Base.

Pas següent

Quan un tema esdevé un projecte real, l'arquitectura, l'entorn existent i les operacions s'haurien de considerar conjuntament des de bon començament.

No només donem suport en qüestions puntuals, sinó també quan, a partir de fragments de codi font, temes de sistemes heredats o idees de portal, ha de sorgir un projecte empresarial sòlid.

  • L'estat actual, la visió objectiu i els riscos tècnics s'avaluen conjuntament.
  • REST, l'accés a les dades, els portals i el desplegament no es releguen a fases posteriors.
  • Vostè veurà aviat quin camí és econòmicament i operativament viable.

Comparteix la publicació

Comparteix aquesta publicació directament

LinkedIn, X, XING, Facebook, WhatsApp i E-Mail estan disponibles de forma immediata. Per a Instagram preparem directament l’enllaç i un text breu.

Correu electrònic

Instagram s'obre en una pestanya nova. L'enllaç i el text curt es copien prèviament al porta-retalls.