Do tema da revista à prática do projeto
Páginas de serviços e técnicas correspondentes ao artigo
Quem, numa aplicação empresarial existente, pretende simplesmente incorporar conteúdos web modernos, acaba, em Windows, por recorrer ao WebView2. Em Delphi WebView2 FMX o problema fundamental raramente é a exibição de uma URL, mas a incorporação limpa numa interface FireMonkey (FMX), a inicialização fiável (assíncrona e baseada em COM), assim como as armadilhas do Edge relativas a diretórios de User-Data, downloads, depuração e uma comunicação JS↔Delphi robusta.
Este trecho de código mostra um padrão que prefiro para aplicações manuteníveis: um objeto “Host” encapsulado que controla o ciclo de vida do WebView2, e uma ponte definida através de WebMessage (JSON), em vez de executar “ExecuteScript” indiscriminadamente. O objetivo não é código de demonstração, mas um bloco construtivo que sobreviva em clientes já estabelecidos.
Por que o WebView2 em FMX é diferente de um „Browser-Component drop“
O WebView2 é uma API próxima de COM/WinRT com inicialização assíncrona. O FireMonkey abstrai handles Windows, contudo, para o WebView2 acaba a ser necessário uma janela pai real (HWND) e um encaminhamento controlado de resize/focus. Ao mesmo tempo, eventos nem sempre são disparados onde se espera no FMX. Se iniciar aqui de forma “quick and dirty”, tipicamente obterá:
- violação de acesso (Access Violations, AVs) esporádicas ao fechar o formulário (callbacks chegam após Destroy)
- eventos de navegação vindos de um contexto de thread incorreto
- persistência/cache pouco confiável devido a uma estratégia de UserDataFolder indefinida
- ausência de downloads ou diálogos de download “presos”
- depuração dependente de sorte em vez de uma configuração de depuração remota direcionada
O contrapeso é um lifecycle claro: Create → InitializeAsync → Attach → Navigate → Detach/Dispose – e uma fronteira definida entre UI e engine do navegador.
Source-Schnipsel: WebView2Host für Delphi WebView2 FMX
O código a seguir esboça uma classe host encapsulada que (1) cria uma configuração de WebView2-Environment, (2) vincula o objeto controller a um HWND, (3) faz o wiring de eventos de navegação e download, e (4) fornece uma JS-bridge baseada em JSON via WebMessageReceived. O código é deliberadamente “pronto para arquitetura”: encapsula referências COM, evita callbacks remanescentes após Destroy, e permite cenários operacionais como UserDataFolder separados “por usuário” ou “por máquina”.
unit WebView2Host;
interface
uses
System.SysUtils, System.Classes, System.IOUtils, System.JSON,
Winapi.Windows, Winapi.ActiveX,
FMX.Types,
WebView2, WebView2_TLB; // dependendo da configuração: WebView2.pas ou 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‘, “);
// O payload pode faltar ou 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 já foi destruído.‘);
end;
function TWebView2Host.MakeUserDataFolder: string;
begin
if FUserDataFolder <> “ then
Exit(FUserDataFolder);
// Prática: por app + por utilizador Windows, não no diretório do programa
Result := TPath.Combine(TPath.GetHomePath, ‚AppDataLocalMyCompanyMyAppWebView2‘);
ForceDirectories(Result);
end;
procedure TWebView2Host.InitializeAsync;
var
UserData: string;
Opt: ICoreWebView2EnvironmentOptions;
begin
EnsureNotDestroyed;
UserData := MakeUserDataFolder;
// Opções: aqui podem ser inseridos argumentos adicionais do browser, ex.: 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;
// Controller an Parent HWND binden
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;
// Tornar visível 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));
// Observação: para remover eventos de forma robusta, deve armazenar os tokens.
// Em muitos projetos é suficiente se o host viver apenas enquanto o Form existir.
end;
procedure TWebView2Host.UnhookEvents;
begin
// Variante robusta: armazenar os tokens e chamar remove_*
// Mantido como comentário: o setup da unit de import e a gestão dos tokens variam conforme o 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));
// Na prática: ResultFileName inicialmente vazio, dependendo da origem.
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 download própria, então definir 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 ainda não foi 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 da abordagem
- Encapsulamento do ciclo de vida: o formulário FMX conhece apenas „Initialize/Navigate/Resize“, não detalhes de COM.
- Bridge com contrato: mensagens JSON com
name, opcionalcid(Correlation-ID) epayloadsão manuteníveis e testáveis. - Persistência segura para produção: um
UserDataFoldercontrolado previne colisões de cache, problemas de permissões e „funciona no computador do desenvolvedor, não em produção“.
JS↔Delphi-Bridge: por que WebMessage é mais estável que ExecuteScript
WebView2 oferece vários caminhos de comunicação. Na prática, ExecuteScript é tentador, mas difícil de versionar: você envia strings para um interpretador, sem canais claros de resposta e sem um mapeamento de erros robusto. PostWebMessageAsString / WebMessageReceived, por outro lado, é um canal definido.
Cenário limite que aparece frequentemente em ambientes empresariais: você precisa iniciar um workflow Delphi a partir de um front-end web (p.ex. portal interno) (impressão, acesso a dispositivos, integração com legados). Então você precisa de:
- uma lista de nomes de mensagem autorizados
- Correlation-IDs para respostas assíncronas
- um ponto central que valide payloads (p.ex. campos obrigatórios, limites de tamanho)
No host isso é o ponto OnWebMessageReceived. A validação real pertence a uma camada acima (p.ex. serviço de aplicação), para manter separadas a técnica UI/WebView2 e a lógica de negócio (arquitetura em camadas clássica: UI → Aplicação → Domínio → Infraestrutura).
Downloads e armazenamento de arquivos: o que frequentemente surpreende em produção
Downloads em WebView2 são realizados via ICoreWebView2DownloadOperation. Dependendo da origem, ResultFilePath pode estar vazio inicialmente ou só ser definido depois. Além disso, muitas empresas não querem que usuários finais gravem em pastas não controladas.
Padrões recomendados:
- Interceptar DownloadStarting e assumir a UI via
args.put_Handled(1)(caminho próprio, convenção de nomes, pasta de quarentena). - Limites de tamanho de arquivo e verificações de tipo MIME, para evitar „acidentalmente um logfile de 4 GB“.
- Auditoria: registrar metadados de download (URI, MIME, bytes) no seu logging, não o conteúdo.
Se você tem processos regulados (p.ex. aprovações, rastreabilidade), o tratamento via eventos é o único ponto onde a esfera do navegador pode ser integrada às suas regras operacionais.
Debugging: DevTools, Remote Debug Port e estados reproduzíveis
O debugging do WebView2 costuma falhar porque estados não são reproduzíveis. Duas alavancas ajudam:
- Ativar/desativar DevTools via
ICoreWebView2Settings(no código:SetDevToolsEnabled) – frequentemente desligado em builds de release, ligado pontualmente em casos de suporte. - UserDataFolder estável: quando o suporte precisa reproduzir um erro, um caminho definido vale ouro. Você pode proteger/comprimir a pasta (Atenção: proteção de dados/PII) e comparar estados de forma dirigida.
Opcionalmente (dependendo do wrapper) você pode adicionar argumentos de navegador em EnvironmentOptions, p.ex. um Remote-Debug-Port. Isso é útil quando precisa analisar uma aplicação em um sistema de testes sem ferramentas locais de desenvolvedor. Limites: em ambientes produtivos isso deve ser claramente autorizado e documentado, caso contrário você cria uma superfície de ataque desnecessária.
Stolperfallen in Delphi WebView2 FMX: COM, Threads und Form-Lifecycle
1) Callbacks após o fechamento
Os CompletedHandler assíncronos podem chegar depois que o formulário já está sendo fechado. No snippet, FDestroyed impede o acesso a objetos liberados. Mais robusto é, adicionalmente:
- Armazenar tokens dos eventos e, em
Destroy, chamar corretamente osremove_* - Permitir
InitializeAsyncapenas uma vez (máquina de estados: Created/Initializing/Ready/Disposed)
2) Thread-Kontext
Muitos Handler são “próximos à UI”, mas não confie que possa escrever diretamente nos controles FMX. Se atualizar a UI em OnWebMessage, TThread.Queue(nil, ...) é a opção segura. Eu costumo separar responsabilidades: o host coleta o evento, o Application-Service decide, a UI é atualizada exclusivamente via Queue.
3) DPI/Resize und FMX-Layouts
FMX calcula em unidades lógicas, WebView2 espera Pixel-Rects. Na prática, você precisa de um ponto claro onde converte os Bounds dos controles FMX para pixels reais. O snippet assume um TRect; na sua form você deve derivar a partir dele as coordenadas WinAPI (por exemplo via FMX.Platform.Win e APIs de handle). Se o app escala por DPI do monitor, teste a troca entre monitores: o WebView2 é mais sensível aqui do que controles FMX puros.
Quando vale a pena usar WebView2 no FMX — e quando não
WebView2 compensa quando, numa aplicação cliente Delphi já consolidada, você quer empregar tecnologia web de forma dirigida: views administrativas embutidas, fluxos de login OAuth/OIDC, relatórios HTML, portais internos ou “micro-frontends” controlados. Também funciona como ponte de modernização, desde que as responsabilidades sejam claramente separadas e a bridge não vire uma porta dos fundos incontrolada para lógica de negócio.
Limitações da abordagem:
- Plattform: o padrão é centrado em Windows. FMX é multiplataforma, WebView2 não. Para macOS/iOS/Android você precisa de outros WebViews ou de uma camada de abstração.
- Security/Hardening: sempre que conteúdos externos são carregados, é preciso restringir mais rigidamente navegação, domínios permitidos e destinos de download. Isso deve constar nos requisitos, não ser deixado “para depois”.
- Support: UserDataFolder e dependências de runtime (WebView2 Runtime) precisam fazer parte do seu conceito de operação/rollout.
Conclusão
Delphi WebView2 FMX é menos um gadget de UI e mais um componente de integração com lifecycle próprio. Se você encapsular de forma estruturada inicialização, eventing, UserDataFolder e JS-Bridge, o WebView2 se torna um bloco estável para soluções empresariais digitais: Web-UI onde for apropriado e lógica Delphi onde ela pertence. Se, em vez disso, você disparar scripts de forma descontrolada, deixar caminhos ao acaso e não desacoplar eventos, terá exatamente o tipo de erro “esporádico em produção” que consome tempo e mina confiança.
Se quiser integrar o WebView2 de forma limpa em uma aplicação Delphi existente ou avaliar tecnicamente uma borda de modernização, fale conosco:
No contexto técnico, Webview2 Firemonkey e Delphi Fmx Edge Browser também desempenham um papel relevante quando integrações, fluxos de dados e evolução precisam atuar de forma coerente.
Discutir projeto ou iniciativa de modernização com Net-Base.
Próximo passo
Quando um tema se torna um projeto real, arquitetura, sistemas existentes e operação devem ser considerados em conjunto desde o início.
Não apenas apoiamos questões pontuais, mas também quando fragmentos de código-fonte, temas legados ou ideias de portais precisam evoluir para um projeto empresarial robusto.
- Estado atual, estado-alvo e riscos técnicos são avaliados em conjunto.
- REST, o acesso a dados, os portais e o Rollout não são adiados para uma fase posterior.
- Você vê cedo qual caminho é economicamente e operacionalmente viável.