Net-Base Revista

14.06.2026

Delphi WebView2 en FMX: inicializar correctamente, implementar JS-Bridge, gestionar descargas y depuración

WebView2 en FireMonkey suena a 'simplemente incrustar un navegador', pero en la práctica falla durante la inicialización, los eventos de navegación, el puente JS↔Delphi, el manejo de descargas y la depuración. Este fragmento de código fuente muestra un patrón robusto con responsabilidades claramente definidas...

14.06.2026

Del tema de la revista a la práctica del proyecto

Páginas de servicios y técnicas relacionadas

Quien en un software empresarial existente de repente quiere «rápidamente» incrustar contenidos web modernos, acaba en Windows con WebView2. En Delphi WebView2 FMX el problema básico rara vez es mostrar una URL; lo crítico es la integración limpia en una interfaz FireMonkey (FMX), la inicialización fiable (asíncrona y basada en COM), así como las particularidades de Edge en torno a los directorios de datos de usuario, descargas, depuración y una comunicación JS↔Delphi robusta.

Este fragmento de código muestra un patrón que prefiero para aplicaciones mantenibles: un objeto “host” encapsulado que controla el ciclo de vida de WebView2, así como un puente definido mediante WebMessage (JSON), en lugar de «ExecuteScript en cualquier lugar». El objetivo no es código de demostración, sino un bloque reutilizable que sobreviva en clientes ya existentes.

Por qué WebView2 en FMX es distinto a «soltar un componente de navegador»

WebView2 es una API cercana a COM/WinRT con inicialización asíncrona. FireMonkey abstrae los handles de Windows, no obstante al final necesita para WebView2 una ventana padre real (HWND) y una reenvío controlado de redimensionamiento/foco. Al mismo tiempo, los eventos no siempre se ejecutan donde se esperan en FMX. Si comienza aquí «quick and dirty», normalmente obtendrá:

  • violaciones de acceso (AVs) esporádicas al cerrar el formulario (callbacks que llegan después de Destroy)
  • eventos de navegación desde un contexto de hilo incorrecto
  • persistencia/problemas de caché poco fiables debido a una estrategia de UserDataFolder poco clara
  • ausencia de descargas o diálogos de descarga «colgados»
  • depuración por azar en lugar de una configuración remota de depuración controlada

El antídoto es un ciclo de vida claro: Create → InitializeAsync → Attach → Navigate → Detach/Dispose – y un límite definido entre la UI y el motor del navegador.

Fragmento de código: WebView2Host para Delphi WebView2 FMX

El siguiente código esboza una clase host encapsulada que (1) crea una configuración de WebView2-Environment, (2) vincula el objeto Controller a un HWND, (3) cablea los eventos de navegación y descarga, y (4) ofrece un puente JS basado en JSON mediante WebMessageReceived. El código es deliberadamente «apto para arquitectura»: encapsula referencias COM, evita callbacks rezagados tras Destroy y permite escenarios operativos como carpetas UserDataFolder separadas «por usuario» o «por máquina».

unit WebView2Host;

interface

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

// El payload puede faltar o ser 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;

// Desvincular eventos antes de liberar objetos COM
UnhookEvents;

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

inherited;
end;

procedure TWebView2Host.EnsureNotDestroyed;
begin
if FDestroyed then
raise EInvalidOperation.Create(‚WebView2Host ya ha sido destruido.‘);
end;

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

// Práctica: por app y por Windows-usuario, no en el directorio del programa
Result := TPath.Combine(TPath.GetHomePath, ‚AppDataLocalMyCompanyMyAppWebView2‘);
ForceDirectories(Result);
end;

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

UserData := MakeUserDataFolder;

// Opciones: aquí pueden ir argumentos adicionales del navegador, p. ej. 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;

// Vincular el controller al 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;

// Hacer visible inicialmente
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: para un desenganche (unhooking) robusto debería almacenar los tokens.
// En muchos proyectos esto es suficiente si el host solo existe junto con el Form.
end;

procedure TWebView2Host.UnhookEvents;
begin
// Variante robusta: recordar los tokens y llamar a remove_*.
// Aquí como comentario, porque la configuración de la unidad de importación y la gestión de tokens varía según 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));

// En la práctica: ResultFileName inicialmente vacío, según la fuente.
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 descarga propia, entonces establecer 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 aún no está inicializado.‘);

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.

Propósito del enfoque

  • Encapsulamiento del ciclo de vida: el formulario FMX conoce solo «Initialize/Navigate/Resize», no los detalles COM.
  • Puente con contrato: mensajes JSON con name, opcional cid (Correlation-ID) y payload son mantenibles y verificables.
  • Persistencia segura en producción: un UserDataFolder controlado evita colisiones de caché, problemas de permisos y el clásico «funciona en el equipo del desarrollador, no en producción».

JS↔Delphi-Bridge: por qué WebMessage es más estable que ExecuteScript

WebView2 ofrece varios caminos de comunicación. En la práctica, ExecuteScript resulta tentador, pero es difícil de versionar: se envían cadenas a un intérprete sin canales de respuesta claros y sin un mapeo robusto de errores. PostWebMessageAsString / WebMessageReceived, en cambio, es un canal definido.

Caso límite, que aparece con frecuencia en entornos empresariales: debe iniciar desde un frontend web (p. ej. portal interno) un flujo de trabajo Delphi (impresión, acceso a dispositivos, integración con legacy). Entonces necesita:

  • una lista blanca de nombres de mensajes
  • IDs de correlación para respuestas asíncronas
  • un punto central que valide los payloads (p. ej. campos obligatorios, límites de tamaño)

En el host ese es el punto OnWebMessageReceived. La validación real pertenece a una capa superior (p. ej. servicio de aplicación), para mantener separadas la tecnología UI/WebView2 y la lógica de negocio (arquitectura por capas clásica: UI → Servicio de aplicación → Dominio → Infraestructura).

Descargas y almacenamiento de archivos: lo que suele sorprender en producción

Las descargas en WebView2 utilizan ICoreWebView2DownloadOperation. Según la fuente, ResultFilePath puede estar vacío al principio o establecerse más tarde. Además, muchas empresas no desean que los usuarios finales guarden en carpetas no controladas.

Patrones recomendados:

  • Interceptar DownloadStarting y, con args.put_Handled(1), asumir la UI usted mismo (ruta propia, convención de nombres, carpeta de cuarentena).
  • Límites de tamaño de archivo y comprobaciones de tipo MIME, para evitar acabar con un archivo de registro de 4 GB por accidente.
  • Auditoría: escriba metadatos de la descarga (URI, MIME, bytes) en su logging, no el contenido.

Si dispone de procesos regulados (p. ej. aprobaciones, trazabilidad), el manejo mediante los eventos es el único punto en el que puede integrar el mundo del navegador en sus reglas operativas.

Depuración: DevTools, Remote Debug Port y estados reproducibles

La depuración de WebView2 suele fallar porque los estados no son reproducibles. Dos ajustes ayudan:

  • Activar/desactivar DevTools mediante ICoreWebView2Settings (en el código: SetDevToolsEnabled) — en release suele estar desactivado; en casos de soporte, activarlo puntualmente.
  • UserDataFolder estable: si su equipo de soporte debe reproducir un error, una ruta definida vale su peso en oro. Puede respaldar/comprimir la carpeta (Atención: protección de datos/PII) y comparar estados de forma dirigida.

Opcionalmente (según el wrapper) puede proporcionar EnvironmentOptions con argumentos adicionales para el navegador, p. ej. un puerto de depuración remota. Es útil cuando necesita analizar una aplicación en un sistema de pruebas sin herramientas de desarrollo locales. Límites: en entornos productivos debe activarse y documentarse con cuidado; de lo contrario crea una superficie de ataque innecesaria.

Trampas en Delphi WebView2 FMX: COM, hilos y ciclo de vida del formulario

1) Callbacks después del cierre

Los CompletedHandler asíncronos pueden llegar después de que el formulario ya esté cerrándose. En el fragmento, FDestroyed evita el acceso a objetos liberados. Más robusto es además:

  • Almacenar tokens para eventos y en Destroy invocar limpiamente remove_*
  • Permitir InitializeAsync solo una vez (máquina de estados: Created/Initializing/Ready/Disposed)

2) Contexto de hilo

Muchos manejadores llegan «cerca de la UI», pero no confíe en que pueda escribir directamente en los controles FMX. Si actualiza la UI en OnWebMessage, TThread.Queue(nil, ...) es la variante segura. Yo separo: el host recopila el evento, el servicio de aplicación decide, la UI se actualiza exclusivamente mediante Queue.

3) DPI/Redimensionado y layouts FMX

FMX calcula en unidades lógicas, WebView2 espera rectángulos en píxeles. En la práctica necesita un punto claro donde convertir los bounds de los controles FMX a píxeles reales. El fragmento asume un TRect; en su formulario debería derivar de ello las coordenadas de la WinAPI (p. ej. mediante FMX.Platform.Win y las Handle-APIs). Si la app escala según el DPI del monitor, pruebe el cambio entre monitores: WebView2 es aquí más sensible que los controles FMX puros.

Cuándo vale la pena usar WebView2 en FMX — y cuándo no

WebView2 merece la pena cuando desea emplear tecnología web de forma selectiva en una aplicación cliente Delphi consolidada: vistas administrativas embebidas, flujos de inicio de sesión OAuth/OIDC, informes HTML, portales internos o «micro-frontends» controlados. También es práctico como puente de modernización, siempre que delimite claramente las responsabilidades y no convierta la bridge en una puerta trasera incontrolada para la lógica de negocio.

Límites del enfoque:

  • Plattform: El patrón está centrado en Windows. FMX es multiplataforma, WebView2 no lo es. Para macOS/iOS/Android necesitará otros WebViews o una capa de abstracción.
  • Security/Hardening: En cuanto se carguen contenidos externos, debe restringir con más rigor la navegación, los dominios permitidos y los destinos de descarga. Esto debe formar parte de los requisitos, no «más tarde».
  • Support: UserDataFolder y las dependencias de runtime (WebView2 Runtime) deben formar parte de su concepto de operación/despliegue.

Conclusión

Delphi WebView2 FMX es menos un gadget de UI y más un componente de integración con su propio ciclo de vida. Si encapsula de forma estructurada la inicialización, la gestión de eventos, el UserDataFolder y la JS-Bridge, WebView2 será un elemento estable para soluciones empresariales digitales: UI web donde tenga sentido, y la lógica Delphi donde corresponde. En cambio, si dispara scripts sin control, deja rutas al azar y no desacopla eventos, obtendrá precisamente el tipo de errores «esporádicos en campo» que consumen tiempo y minan la confianza.

Si desea integrar WebView2 de forma ordenada en una aplicación Delphi existente o evaluar técnicamente un borde de modernización, hable con nosotros:

En el ámbito técnico también juegan un papel importante Webview2 Firemonkey y Delphi Fmx Edge Browser, cuando las integraciones, los flujos de datos y la evolución deben encajar de forma ordenada.

Discutir un proyecto o iniciativa de modernización con Net-Base.

Siguiente paso

Cuando el tema se convierte en un proyecto real, la arquitectura, los sistemas existentes y la operación deben considerarse desde el principio.

No solo apoyamos en consultas puntuales, sino también cuando, a partir de fragmentos de código fuente, temas heredados o ideas de portales, debe consolidarse un proyecto empresarial robusto.

  • La situación actual, el estado objetivo y los riesgos técnicos se evalúan conjuntamente.
  • REST, el acceso a datos, los portales y el rollout no se posponen como consecuencias tardías.
  • Detecta con antelación qué enfoque es viable desde el punto de vista económico y operativo.

Compartir entrada

Compartir esta publicación directamente

LinkedIn, X, XING, Facebook, WhatsApp y correo electrónico están disponibles de inmediato. Para Instagram preparamos el enlace y un texto breve de inmediato.

Correo electrónico

Instagram se abre en una nueva pestaña. El enlace y el texto breve se copian previamente en el portapapeles.