Net-Base Magazín

14.06.2026

Delphi WebView2 vo FMX: správne inicializovať, vytvoriť JS-Bridge, sťahovanie a ladenie pod kontrolou

WebView2 v FireMonkey znie ako „jednoduché vloženie prehliadača“, no v praxi zlyháva pri inicializácii, navigačných udalostiach, JS↔Delphi-Bridge, spracovaní sťahovania a debugovaní. Tento úryvok zdrojového kódu ukazuje robustný vzor s jednoznačnými zodpovednosťami.

14.06.2026

Od témy magazínu k projektovej praxi

Súvisiace stránky služieb a technológií k príspevku

Kto chce v existujúcom firemnom softvéri náhle „mal eben“ vložiť moderný webový obsah, narazí na Windows pri WebView2. V Delphi WebView2 FMX hlavný problém zriedka spočíva v zobrazení URL, ale v čistom vložení do rozhrania FireMonkey (FMX), spoľahlivom inicializovaní (asynchrónne a na COM založenom), ako aj úskaliach súvisiacich s User-Data-Verzeichnisse, sťahovaniami, ladením a robustnou JS↔Delphi-komunikáciou.

Tento útržok zdrojového kódu ukazuje vzor, ktorý uprednostňujem pre udržiavateľné aplikácie: zapuzdrený „Host“-objekt, ktorý kontroluje WebView2‑lifecycle, a definovaný most cez WebMessage (JSON), namiesto ľubovoľného „ExecuteScript überall“. Cieľom nie je demo‑kód, ale stavebný blok, ktorý prežije v rastúcich klientoch.

Warum WebView2 in FMX anders ist als „Browser-Component drop“

WebView2 je API blízka COM/WinRT s asynchrónnou inicializáciou. FireMonkey abstrahuje Windows-Handles, napriek tomu pre WebView2 nakoniec potrebujete skutočné parent‑window (HWND) a kontrolované presmerovanie Resize-/Focus‑událostí. Zároveň udalosti nebežia vždy tam, kde by ste ich v FMX očakávali. Ak tu začnete „quick and dirty“, typicky dostanete:

  • sporadické AVs pri zatváraní formu (Callbacks prichádzajú po Destroy)
  • Navigačné udalosti v nesprávnom kontexte vlákna
  • nespoľahlivú persistenciu/Cache‑problémy kvôli nejasnej UserDataFolder‑stratégií
  • žiadne Downloads alebo „zaseknuté“ dialógy sťahovania
  • Debugging len náhodou namiesto cielenej Remote‑Debug‑konfigurácie

Protiopatrením je jasný Lifecycle: Create → InitializeAsync → Attach → Navigate → Detach/Dispose – a definovaná hranica medzi UI a Browser‑Engine.

Source‑Schnipsel: WebView2Host für Delphi WebView2 FMX

Nasledujúci kód načrtáva zapuzdrenú host‑triedu, ktorá (1) vytvorí WebView2‑Environment konfiguráciu, (2) viaže Controller‑objekt na HWND, (3) prepojí Navigation a Download‑udalosti a (4) poskytuje JSON‑založený JS‑bridge cez WebMessageReceived. Kód je zámerne „architekturfähig“: zapuzdruje COM‑referencie, zabraňuje callback‑následníkom po Destroy a umožňuje prevádzkové režimy ako „pro User“ alebo „pro Maschine“ s oddelenými UserDataFolder.

unit WebView2Host;

interface

uses
System.SysUtils, System.Classes, System.IOUtils, System.JSON,
Winapi.Windows, Winapi.ActiveX,
FMX.Types,
WebView2, WebView2_TLB; // podľa nastavenia: WebView2.pas alebo 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 chýbať alebo byť 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;

// Uvoľniť udalosti skôr, než sa uvoľnia COM objekty
UnhookEvents;

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

inherited;
end;

procedure TWebView2Host.EnsureNotDestroyed;
begin
if FDestroyed then
raise EInvalidOperation.Create(‚WebView2Host už bol zničený.‘);
end;

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

// V praxi: pre aplikáciu + pre Windows používateľa, nie v adresári 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 možno pridať ďalšie argumenty pre prehliadač, napr. Remote-Debug
Opt := TCoreWebView2EnvironmentOptions.Create;

// Asynchrónne CreateEnvironment
OleCheck(CreateCoreWebView2EnvironmentWithOptions(
nil, PWideChar(UserData), Opt,
TCoreWebView2CreateCoreWebView2EnvironmentCompletedHandler.Create(
procedure (errorCode: HRESULT; const createdEnvironment: ICoreWebView2Environment)
begin
if FDestroyed then Exit;
OleCheck(errorCode);

FEnvironment := createdEnvironment;

// Pripojiť kontrolér 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;

// Nastaviť počiatočnú viditeľnosť
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: Pre robustné odregistrovanie udalostí by ste mali uložiť tokeny.
// V mnohých projektoch to stačí, ak host žije len s formulárom.
end;

procedure TWebView2Host.UnhookEvents;
begin
// Robustná verzia: zapamätať si tokeny a zavolať remove_*.
// Tu ako komentár, pretože nastavenie importnej jednotky a správa tokenov sa líši podľa 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 je spočiatku prázdne, v závislosti od zdroja.
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);

// Voliteľné: vlastné UI pre sťahovanie, potom nastaviť 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 ešte nebol inicializovaný.‘);

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

  • Kapsulácia životného cyklu: FMX-formulár pozná len „Initialize/Navigate/Resize“, nie COM‑detaily.
  • Bridge s kontraktom: JSON‑správy s name, voliteľne cid (Correlation‑ID) a payload sú ľahko udržiavateľné a testovateľné.
  • Prevádzková spoľahlivá perzistencia: kontrolovaný UserDataFolder zabraňuje kolíziám cache, problémom s právami a situáciám „beží na vývojárskych počítačoch, nie v prevádzke“.

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

WebView2 poskytuje viaceré kanály komunikácie. V praxi je ExecuteScript lákavé, ale ťažko verzovateľné: vložíte stringy do interpretera bez jasných kanálov odpovedí a bez robustného mapovania chýb. PostWebMessageAsString / WebMessageReceived je naopak definovaný kanál.

Okrajový prípad, ktorý sa v podnikových prostrediach často vyskytuje: potrebujete zo web‑frontendu (napr. interné portál) spustiť Delphi‑workflow (tlač, prístup k zariadeniam, integrácia s legacy). Vtedy potrebujete:

  • bielu listinu mien správ
  • Correlation‑ID pre asynchrónne odpovede
  • centrálne miesto, ktoré validuje payloady (napr. povinné polia, limity veľkosti)

V hostovi je to bod OnWebMessageReceived. Skutočná validácia patrí do vyššej vrstvy (napr. Application‑Service), aby ste techniku UI/WebView2 a business logiku držali oddelene (klasická vrstvená architektúra: UI → Application → Domain → Infrastructure).

Downloads und Dateiablage: was im Betrieb oft überrascht

Sťahovanie v WebView2 beží cez ICoreWebView2DownloadOperation. Podľa zdroja môže byť ResultFilePath na začiatku prázdny alebo sa nastaviť až neskôr. Okrem toho mnohé firmy nechcú, aby koncoví používatelia ukladali do nekontrolovaných priečinkov.

Osvedčené postupy:

  • Zachytiť DownloadStarting a pomocou args.put_Handled(1) prevziať spracovanie v UI (vlastná cesta, konvencia pomenovania, karanténny priečinok).
  • Limity veľkosti súborov a kontroly MIME‑typov, aby ste predišli „náhodnému 4 GB log súboru“.
  • Auditovanie: zapisujte metadáta sťahovania (URI, MIME, bajty) do vášho logovania, nie obsah súboru.

Ak máte regulované procesy (napr. schválenia, sledovateľnosť), je spracovanie cez eventy jediné miesto, kde môžete prepojiť svet prehliadača s vašimi prevádzkovými pravidlami.

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

WebView2‑debugging často zlyháva preto, že stavy nie sú reprodukovateľné. Pomôžu dve páky:

  • Aktivovať/deaktivovať DevTools cez ICoreWebView2Settings (v kóde: SetDevToolsEnabled) – v release často vypnuté, v prípade podpory cielene zapnúť.
  • Stabilný UserDataFolder: ak má support reprodukovať chybu, definovaná cesta má veľkú hodnotu. Priečinok môžete zálohovať/zipnúť (Pozor: Datenschutz/PII) a cielene porovnávať stavy.

Voliteľne (v závislosti od wrapperu) môžete do EnvironmentOptions pridať ďalšie argumenty pre prehliadač, napr. Remote‑Debug‑Port. Má to zmysel, keď analyzujete aplikáciu na testovacom systéme bez lokálnych dev‑nástrojov. Obmedzenia: v produkčných prostrediach to musí byť riadne povolené a zdokumentované, inak vytvárate zbytočný útokový povrch.

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

1) Callbacks po zatvorení

Asynchrónne CompletedHandler môžu prísť až po tom, čo sa formulár už zatvára. V ukážke zabraňuje FDestroyed prístupu k uvoľneným objektom. Robustnejšie je navyše:

  • Uložiť tokeny pre udalosti a v Destroy spoľahlivo volať remove_*
  • Povoliť InitializeAsync len raz (stavový automat: Created/Initializing/Ready/Disposed)

2) Kontext vlákna

Mnohé handlery síce prichádzajú „blízko UI“, ale nespoliehajte sa na to, že do FMX-ovských ovládacích prvkov môžete zapisovať priamo. Ak v OnWebMessage aktualizujete UI, bezpečnou voľbou je TThread.Queue(nil, ...). Ja to rád rozdelím takto: host zhromažďuje udalosť, application-service rozhodne, UI sa aktualizuje výlučne cez Queue.

3) DPI/Resize a FMX-layouty

FMX počíta v logických jednotkách, WebView2 očakáva pixelové recty. V praxi potrebujete jasné miesto, kde z FMX-ových ovládacích prvkov prevediete bounds na skutočné pixely. Ukážka predpokladá TRect; vo vašom formulári by ste z neho mali odvodiť WinAPI-súradnice (napr. cez FMX.Platform.Win a handle-API). Ak aplikácia škáluje podľa DPI monitora, otestujte prepínanie medzi monitormi: WebView2 je v tomto citlivejší než čisté FMX-ovské ovládacie prvky.

Kedy sa WebView2 vo FMX oplatí — a kedy nie

WebView2 sa oplatí, ak chcete v rastúcej Delphi-klientskej aplikácii cielene využiť webové techniky: vložené administrátorské zobrazenia, OAuth/OIDC prihlasovacie toky, HTML-reporty, interné portály alebo kontrolované „micro-frontendy“. Rovnako je praktické ako most pri modernizácii, pokiaľ jasne oddelíte zodpovednosti a nedovolíte, aby sa bridge stal nekontrolovanou zadnými dverami pre business-logic.

Obmedzenia prístupu:

  • Platforma: Vzor je zameraný na Windows. FMX je multiplatformový, WebView2 nie. Pre macOS/iOS/Android potrebujete iné WebView alebo vrstvu abstrakcie.
  • Security/Hardening: Hneď ako načítavate externý obsah, musíte prísnejšie obmedziť navigáciu, povolené domény a ciele sťahovania. To patrí do požiadaviek, nie „neskôr“.
  • Support: UserDataFolder a runtime-závislosti (WebView2 Runtime) musia byť súčasťou vášho prevádzkového/rollout konceptu.

Záver

Delphi WebView2 FMX nie je len UI-gadget, ale integračná komponenta s vlastným lifecycle. Ak inicializáciu, eventing, UserDataFolder a JS-Bridge štruktúrovane zapuzdríte, stane sa WebView2 stabilným stavebným prvkom pre digitálne podnikové riešenia: Web-UI tam, kde má zmysel, a Delphi-logika tam, kam patrí. Ak však nekontrolovane spúšťate skripty, nechávate cesty náhode a neoddeľujete udalosti, dostanete presne ten typ „sporadicky v produkcii“ chýb, ktorý žerie čas a podkopáva dôveru.

Ak chcete WebView2 v existujúcej Delphi-aplikácii integrovať správne alebo technicky vyhodnotiť hranu modernizácie, porozprávajte sa s nami:

V odbornej praxi zohrávajú tiež dôležitú úlohu Webview2 Firemonkey a Delphi Fmx Edge Browser, keď integrácie, dátové toky a ďalší vývoj musia fungovať v súlade.

Projekt alebo modernizačné zadanie s Net-Base prediskutovať.

Ďalší krok

Keď sa téma stane reálnym projektom, architektúru, existujúci stav a prevádzku treba včas posudzovať spoločne.

Podporujeme nielen pri jednotlivých otázkach, ale aj vtedy, keď sa z fragmentov zdrojového kódu, tém súvisiacich s legacy systémami alebo nápadov na portál má stať robustný podnikový projekt.

  • Stav, cieľový obraz a technické riziká sa hodnotia spoločne.
  • REST, prístup k dátam, portály a Rollout nebudú odložené na neskôr.
  • Včas zistíte, ktorá cesta je ekonomicky a prevádzkovo životaschopná.

Zdieľať príspevok

Tento príspevok priamo zdieľať

LinkedIn, X, XING, Facebook, WhatsApp a e-mail sú ihneď k dispozícii. Pre Instagram pripravujeme priamo odkaz a krátky text.

E-mail

Instagram sa otvorí v novej karte. Odkaz a krátky text sa predtým skopírujú do schránky.