Od tématu magazínu k projektové praxi
Vhodné stránky služeb a technické stránky k příspěvku
Kdo v existujícím firemním softwaru náhle chce „mal eben“ vložit moderní webový obsah, narazí na Windows u WebView2. V Delphi WebView2 FMX není základním problémem zobrazení URL, ale čisté vložení do rozhraní FireMonkey (FMX), spolehlivá inicializace (asynchronní a na COM založená) a dále úskalí spojená s Edge kolem adresářů User-Data, stahování, ladění a robustní komunikace JS↔Delphi.
Tento úryvek zdrojového kódu ukazuje vzor, který upřednostňuji pro udržovatelné aplikace: inkapsulovaný objekt „Host“, který řídí životní cyklus WebView2, a definovaný most přes WebMessage (JSON) místo libovolného „ExecuteScript všude“. Cílem není demo-kód, ale stavební blok, který v existujících klientech přežije.
Proč je WebView2 ve FMX jiné než „Browser-Component drop“
WebView2 je API blízké COM/WinRT s asynchronní inicializací. FireMonkey abstrahuje Windows-Handles, přesto pro WebView2 nakonec potřebujete skutečné parent‑window (HWND) a kontrolované předávání změn velikosti a focusu. Současně se události ne vždy vyvolávají tam, kde byste je ve FMX očekávali. Pokud tady začnete „quick and dirty“, typicky narazíte na:
- sporadické AV při zavírání formuláře (callbacks přicházejí po Destroy)
- navigační události z nesprávného kontextu vlákna
- nespolehlivou perzistenci/problémy s cache kvůli nejasné strategii UserDataFolder
- žádná stahování nebo „zaseknuté“ dialogy stahování
- ladění funguje jen náhodou místo řízené konfigurace vzdáleného ladění
Prostředkem je jasný životní cyklus: Create → InitializeAsync → Attach → Navigate → Detach/Dispose – a definovaná hranice mezi UI a browser‑engine.
Source-Schnipsel: WebView2Host für Delphi WebView2 FMX
Následující kód nastiňuje inkapsulovanou host‑třídu, která (1) vytvoří WebView2‑Environment konfiguraci, (2) připojí controller‑objekt k HWND, (3) zapojí navigation a download události a (4) nabídne JSON‑založený JS‑most přes WebMessageReceived. Kód je záměrně „architekturfähig“: inkapsuluje COM‑referencí, zabraňuje následkům callbacků po Destroy a umožňuje provozní scénáře jako oddělené UserDataFolder „pro uživatele“ nebo „pro stroj“.
unit WebView2Host;
interface
uses
System.SysUtils, System.Classes, System.IOUtils, System.JSON,
Winapi.Windows, Winapi.ActiveX,
FMX.Types,
WebView2, WebView2_TLB; // dle nastavení: WebView2.pas nebo 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‘, “);
// Payload může chybět nebo být 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;
// Události odregistrovat dříve, než se uvolní COM-objekty
UnhookEvents;
FWebView := nil;
FController := nil;
FEnvironment := nil;
inherited;
end;
procedure TWebView2Host.EnsureNotDestroyed;
begin
if FDestroyed then
raise EInvalidOperation.Create(‚WebView2Host je již zničen.‘);
end;
function TWebView2Host.MakeUserDataFolder: string;
begin
if FUserDataFolder <> “ then
Exit(FUserDataFolder);
// V praxi: pro každou aplikaci a pro uživatele Windows, nikoli v adresáři programu
Result := TPath.Combine(TPath.GetHomePath, ‚AppDataLocalMyCompanyMyAppWebView2‘);
ForceDirectories(Result);
end;
procedure TWebView2Host.InitializeAsync;
var
UserData: string;
Opt: ICoreWebView2EnvironmentOptions;
begin
EnsureNotDestroyed;
UserData := MakeUserDataFolder;
// Možnosti: sem lze vložit další argumenty pro prohlížeč, např. vzdálené ladění
Opt := TCoreWebView2EnvironmentOptions.Create;
// Asynchronní vytvoření prostředí
OleCheck(CreateCoreWebView2EnvironmentWithOptions(
nil, PWideChar(UserData), Opt,
TCoreWebView2CreateCoreWebView2EnvironmentCompletedHandler.Create(
procedure (errorCode: HRESULT; const createdEnvironment: ICoreWebView2Environment)
begin
if FDestroyed then Exit;
OleCheck(errorCode);
FEnvironment := createdEnvironment;
// Připojit Controller k rodičovskému HWND
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;
// Inicialně zobrazit
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));
// Poznámka: Pro spolehlivé odregistrování událostí uložte tokeny.
// V mnoha projektech to stačí, když host žije pouze po dobu života formuláře.
end;
procedure TWebView2Host.UnhookEvents;
begin
// Robustní varianta: pamatovat tokeny a zavolat remove_*.
// Zde jako komentář, protože nastavení importní jednotky a správa tokenů se liší podle wrapperu.
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));
// V praxi: ResultFileName bývá zpočátku prázdné, záleží na zdroji.
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);
// Volitelně: vlastní download UI, pak nastavit 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 ještě není inicializován.‘);
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.
Účel přístupu
- Lifecycle-kapsle: Die FMX-Form kennt nur „Initialize/Navigate/Resize“, nicht COM-Details.
- Most s kontraktem: JSON-zprávy s
name, volitelněcid(Correlation-ID) apayloadjsou udržovatelné a testovatelné. - Provozově bezpečná perzistence: ein kontrolliertes
UserDataFolderverhindert Cache-Kollisionen, Rechteprobleme und „läuft auf Entwicklerrechner, nicht im Betrieb“.
JS↔Delphi-Bridge: warum WebMessage stabiler ist als ExecuteScript
WebView2 nabízí několik cest komunikace. V praxi je ExecuteScript lákavé, ale obtížně verzovatelné: posíláte řetězce do interpretru bez jasných kanálů pro odpovědi a bez robustního mapování chyb. PostWebMessageAsString / WebMessageReceived je oproti tomu definovaný kanál.
Okrajový případ, který se v podnikových prostředích často objevuje: z webového frontendu (např. interní portál) musíte spustit Delphi-workflow (tisk, přístup k zařízením, integrace s legacy). V takovém případě potřebujete:
- whitelistu názvů zpráv
- Correlation-IDs pro asynchronní odpovědi
- centrální místo, které validuje payloady (např. povinná pole, limity velikosti)
V hostiteli je to místo OnWebMessageReceived. Samotná validace patří do vrstvy nad tím (např. Application-Service), aby byla UI-/WebView2-technika oddělena od business logiky (klasická vrstvená architektura: UI → Aplikace → Doména → Infrastruktura).
Stahování a uložení souborů: co v provozu často překvapí
Stahování ve WebView2 probíhá přes ICoreWebView2DownloadOperation. V závislosti na zdroji může být ResultFilePath zpočátku prázdná nebo se vyplnit až později. Navíc mnoho podniků nechce, aby koncoví uživatelé ukládali do nekontrolovaných adresářů.
Ověřené vzory:
- zachytit událost DownloadStarting a pomocí
args.put_Handled(1)převzít UI (vlastní cesta, konvence pojmenování, karanténní adresář). - limity velikosti souborů a kontroly MIME typů, aby nedošlo k „nechtěnému 4 GB logu“.
- Audit: metadata stahování (URI, MIME, bajty) zapisujte do logu, ne samotný obsah.
Pokud máte regulované procesy (např. schvalování, auditovatelnost), je zpracování přes tyto události jediným místem, kde můžete svět prohlížeče integrovat do svých provozních pravidel.
Debugging: DevTools, Remote Debug Port und reproduzierbare Zustände
Debugování WebView2 často selhává proto, že stavy nejsou reprodukovatelné. Dvě páky pomáhají:
- DevTools zapnout/vypnout přes
ICoreWebView2Settings(v kódu:SetDevToolsEnabled) – ve finální verzi často vypnuto, v podpůrném režimu cíleně zapnout. - stabilní
UserDataFolder: pokud má podpora reprodukovat chybu, definovaná cesta je k nezaplacení. Můžete složku zálohovat/zipovat (pozor: ochrana osobních údajů/PII) a cíleně porovnávat stavy.
Volitelně (dle wrapperu) můžete EnvironmentOptions doplnit o další argumenty pro prohlížeč, např. port pro vzdálené ladění. To dává smysl, když analyzujete aplikaci na testovacím systému bez lokálních vývojářských nástrojů. Omezení: v produkci musí být taková funkce řádně povolena a zdokumentována, jinak vytvoříte zbytečné vstupní body pro útoky.
Úskalí v Delphi WebView2 FMX: COM, vlákna a životní cyklus formuláře
1) Callbacky po zavření
Asynchronní CompletedHandler mohou dorazit poté, co se formulář už zavírá. Ve snippet59e zabraňuje FDestroyed přístupu k uvolněným objektům. Robustnější je navíc:
- Ukládat tokeny pro události a v
Destroykorektně zavolatremove_* - Povolit
InitializeAsyncpouze jednou (stavový stroj: Created/Initializing/Ready/Disposed)
2) Kontext vlákna
Mnoho handlerů přijde sice „blízko UI“, ale nespoléhejte na to, že můžete přímo zapisovat do FMX ovládacích prvků. Když v OnWebMessage aktualizujete UI, TThread.Queue(nil, ...) je bezpečná varianta. Rád to odděluji: host nasbírá událost, application-service rozhodne, UI se aktualizuje výhradně přes Queue.
3) DPI/Resize a FMX-rozložení
FMX počítá v logických jednotkách, WebView2 očekává pixelové recty. V praxi potřebujete jasné místo, kde přeložíte bounds FMX ovládacích prvků na skutečné pixely. Snippet předpokládá TRect; ve své formě byste z něj měli odvodit WinAPI souřadnice (např. přes FMX.Platform.Win a Handle-API). Pokud se aplikace škáluje podle DPI monitoru, otestujte přechod mezi monitory: WebView2 je v tomto směru citlivější než čisté FMX ovládací prvky.
Kdy se WebView2 v FMX vyplatí – a kdy ne
WebView2 se vyplatí, pokud chcete v existující Delphi klientské aplikaci cíleně použít webovou technologii: zabudované admin views, OAuth/OIDC přihlašovací toky, HTML reporty, interní portály nebo kontrolovaná „micro-frontends“. Jako most pro modernizaci je to rovněž praktické, pokud jasně rozdělíte odpovědnosti a most se nestane nekontrolovanou zadní vrátka pro business logiku.
Omezení přístupu:
- Platforma: Vzor je Windows-orientovaný. FMX je multiplatformní, WebView2 nikoli. Pro macOS/iOS/Android potřebujete jiné WebView nebo vrstvu abstrakce.
- Security/Hardening: Jakmile se načítají externí obsahy, musíte přísněji omezit navigaci, povolené domény a cíle stahování. To patří do požadavků, ne „později“.
- Support: UserDataFolder a runtime závislosti (WebView2 Runtime) musí být součástí vašeho provozního/rollout konceptu.
Závěr
Delphi WebView2 FMX není tolik UI-gadget jako integrační komponenta s vlastním lifecycle. Pokud inicializaci, eventing, UserDataFolder a JS-Bridge strukturovaně zapouzdříte, stane se WebView2 stabilním stavebním kamenem pro digitální podniková řešení: webové UI tam, kde dává smysl, a Delphi logika tam, kde patří. Pokud naopak spouštíte skripty nekontrolovaně, necháváte cesty na náhodě a neoddělujete eventy, dostanete přesně ten typ „sporadicky v poli“ chyb, které žerou čas a oslabují důvěru.
Pokud chcete WebView2 v existující Delphi aplikaci čistě integrovat nebo technicky vyhodnotit modernizační hranu, kontaktujte nás:
V odborném kontextu hrají roli také Webview2 Firemonkey a Delphi Fmx Edge Browser, když musí integrace, datové toky a další rozvoj hrát čistě dohromady.
Další krok
Když se z tématu stane reálný projekt, měly by být architektura, stávající systém a provoz včas posuzovány společně.
Podporujeme nejen při jednotlivých otázkách, ale i v případě, že se z útržků zdrojového kódu, legacy témat nebo nápadů na portál má vyvinout robustní podnikový projekt.
- Současný stav, cílový stav a technická rizika jsou hodnoceny společně.
- REST, přístup k datům, portály a nasazení nebudou odkládány na později.
- Vidíte včas, která cesta je ekonomicky i provozně životaschopná.