Net-Base Magazin

14.06.2026

Delphi WebView2 az FMX-ben: megbízható inicializálás, JS-Bridge megvalósítása, letöltések és hibakeresés kezelése

WebView2 a FireMonkey-ban egyszerűen „böngésző beágyazásának” hangzik, de a gyakorlatban az inicializációnál, a navigációs eseményeknél, a JS↔Delphi-hídnál, a letöltéskezelésnél és a hibakeresésnél megbicsaklik. Ez a forráskódrészlet egy robusztus mintát mutat be egyértelmű felelősségi határokkal...

14.06.2026

A magazintémától a projektgyakorlatig

A bejegyzéshez tartozó szolgáltatási és technikai oldalak

Ha egy meglévő üzleti szoftverbe hirtelen „csak úgy” modern webtartalmat szeretnének beágyazni, akkor a Windows esetén WebView2-höz jutnak el. A Delphi WebView2 FMX esetén a lényegi probléma ritkán egy URL megjelenítése; sokkal inkább a tiszta beágyazás egy FireMonkey-felületbe (FMX), a megbízható inicializálás (aszinkron és COM-alapú), valamint az Edge-specifikus buktatók a felhasználói adatmappák, letöltések, hibakeresés és egy robusztus JS↔Delphi-kommunikáció körül.

Ez a forrásrészlet egy mintát mutat, amelyet karbantartható alkalmazásokhoz részesítek előnyben: egy kapszulázott „Host” objektum, amely a WebView2 életciklusát vezérli, valamint egy definiált híd WebMessage (JSON) használatával a mindenhova szétszórt „ExecuteScript” helyett. A cél nem bemutató-kód, hanem egy olyan építőelem, amely hosszú távon megmarad egy meglévő kliensalkalmazásban.

Miért más a WebView2 FMX-ben, mint egy egyszerű „Browser-Component drop”

A WebView2 egy COM/WinRT-közeli API aszinkron inicializálással. A FireMonkey elrejti a Windows-fogantyúkat (Handles), mégis a WebView2 végső soron egy valódi szülőablakot (HWND) és kontrollált átméretezés-/fókusz-átirányítást igényel. Ugyanakkor az események nem mindig ott futnak, ahol azt FMX-ben várnánk. Ha itt „quick and dirty” megoldással indít, tipikusan az alábbiakat kapja:

  • sporadikus AV-k a Form bezárásakor (a visszahívások a Destroy után érkeznek)
  • navigációs események téves szálkontextusból
  • megbízhatatlan perzisztencia/cache problémák az egyértelmű UserDataFolder-stratégia hiánya miatt
  • nem indulnak letöltések, vagy a letöltés párbeszédablakai ‚beragadnak‘
  • hibakeresés csak szerencse dolga a célzott távoli debug konfiguráció helyett

Ellenszer egy világos életciklus: Create → InitializeAsync → Attach → Navigate → Detach/Dispose – és egy definiált határvonal a UI és a böngészőmotor között.

Forráskód-minta: WebView2Host a Delphi WebView2 FMX-hez

Az alábbi kód egy kapszulázott host-osztály vázlata, amely (1) létrehoz egy WebView2-environment konfigurációt, (2) hozzárendeli a Controller-objektumot egy HWND-hoz, (3) beköti a navigációs és letöltési eseményeket, és (4) kínál egy JSON-alapú JS-hidat a WebMessageReceived segítségével. A kód szándékosan „architektúrakész”: kapszulázza a COM-referenciákat, megakadályozza a Destroy utáni callback-utánfutásokat, és engedi, hogy az üzemeltetési határok, például „pro User” vagy „pro Maschine” szerint külön UserDataFolder-ek legyenek.

unit WebView2Host;

interface

uses
System.SysUtils, System.Classes, System.IOUtils, System.JSON,
Winapi.Windows, Winapi.ActiveX,
FMX.Types,
WebView2, WebView2_TLB; // a beállítástól függően: WebView2.pas vagy 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‘, “);

// A payload hiányozhat vagy null lehet
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;

// Események feloldása, mielőtt a COM-objektumok felszabadulnak
UnhookEvents;

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

inherited;
end;

procedure TWebView2Host.EnsureNotDestroyed;
begin
if FDestroyed then
raise EInvalidOperation.Create(‚A WebView2Host már megsemmisítve.‘);
end;

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

// Gyakorlatban: alkalmazásonként és Windows-felhasználónként, ne a programkönyvtárba
Result := TPath.Combine(TPath.GetHomePath, ‚AppDataLocalMyCompanyMyAppWebView2‘);
ForceDirectories(Result);
end;

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

UserData := MakeUserDataFolder;

// Opciók: ide további böngésző-argumentumok tehetők, pl. távoli hibakeresés
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;

// Kontroller hozzárendelése a szülő HWND-hoz
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;

// Kezdetben láthatóvá tesszük
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));

// Megjegyzés: a megbízható eseményfeloldáshoz mentse el a tokeneket.
// Sok projektben ez elegendő, ha a host csak a form élettartamáig él.
end;

procedure TWebView2Host.UnhookEvents;
begin
// Robusztus változat: tárolja a tokeneket és hívja a remove_*-ot.
// Itt kommentként hagyva, mert az import-unit beállítása és a token-kezelés a wrappertől függ.
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));

// Gyakorlatban: ResultFileName kezdetben üres lehet, forrástól függően.
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);

// Opcionális: saját letöltés-UI, ebben az esetben állítsa be a Handled-et
// 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(‚A WebView2 még nincs inicializálva.‘);

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.

A megközelítés célja

  • Élettartam-kapszulázás: Az FMX-forma csak az „Initialize/Navigate/Resize” ismeri, nem a COM-részleteket.
  • Szerződésalapú híd: A JSON-üzenetek name-mel, opcionális cid (Correlation-ID) mezővel és payload-dal karbantarthatók és tesztelhetők.
  • Üzembiztos perzisztencia: egy kontrollált UserDataFolder megelőzi a cache-ütközéseket, jogosultsági problémákat és azt, hogy „a fejlesztőgépen fut, nem pedig az éles üzemben”.

JS↔Delphi-híd: miért stabilabb a WebMessage, mint az ExecuteScript

A WebView2 több kommunikációs utat kínál. A gyakorlatban a ExecuteScript csábító, de nehezen verziózható: stringeket tolunk egy interpreterbe, válaszcsatornák és robusztus hibamapping nélkül. Ezzel szemben a PostWebMessageAsString / WebMessageReceived egy meghatározott csatorna.

Olyan eset, amely vállalati környezetben gyakran előfordul: egy webes front-endről (pl. belső portál) el kell indítani egy Delphi-workflow-t (nyomtatás, eszközhozzáférés, legacy-integráció). Ilyenkor szüksége lesz:

  • üzenetnevek fehérlistájára
  • Correlation-ID-kre az aszinkron válaszokhoz
  • egy központi helyre, amely validálja a payloadokat (pl. kötelező mezők, méretkorlátok)

A hoszt oldalon ez a OnWebMessageReceived. Az érdemi validálás egy feljebb lévő rétegbe (pl. Application-Service) tartozik, hogy a UI/WebView2-technika és az üzleti logika szét legyen választva (klasszikus rétegarchitektúra: UI → Application → Domain → Infrastruktur).

Letöltések és fájltárolás: mi szokta meglepni az üzemeltetésben

A WebView2-ben a letöltések a ICoreWebView2DownloadOperation-ön keresztül mennek. Forrástól függően a ResultFilePath korán üres lehet vagy csak később töltődik ki. Továbbá sok vállalat nem szeretné, ha a végfelhasználók kontrollálatlan mappákba mentenének.

Bevált minták:

  • DownloadStarting elkapása és args.put_Handled(1) használatával a UI átvétele (saját elérési út, névadási konvenció, quarantén mappa).
  • Fájlméret-korlátok és MIME-típus ellenőrzések, hogy elkerülje a „véletlenül 4 GB-os logfájl” problémát.
  • Auditálás: a letöltés metaadatait (URI, MIME, bájtszám) írja a logba, ne a fájl tartalmát.

Ha szabályozott folyamatai vannak (pl. jóváhagyások, nyomonkövethetőség), az események kezelése az egyetlen pont, ahol a böngésző világát be tudja illeszteni az üzemeltetési szabályaiba.

Debugging: DevTools, Remote Debug Port és reprodukálható állapotok

A WebView2 hibakeresését gyakran az borítja meg, hogy az állapotok nem reprodukálhatók. Két állítható pont segít:

  • DevTools engedélyezése/tiltása a ICoreWebView2Settings-en keresztül (kódban: SetDevToolsEnabled) – release-ben gyakran ki van kapcsolva, support esetén célzottan bekapcsolható.
  • Stabil UserDataFolder: ha a supportnak hibát kell reprodukálnia, egy definiált útvonal aranyat ér. A mappát le lehet menteni/zipelni (adatvédelem/PII figyelembevételével) és az állapotokat céltudatosan összehasonlíthatja.

Opcionálisan (wrappertől függően) az EnvironmentOptions-hoz további böngésző-argumentumokat rendelhet, pl. távoli hibakeresési port. Hasznos, ha egy tesztrendszeren kell elemezni az alkalmazást, ahol nincs helyi fejlesztői eszköz. Korlátok: éles környezetben ezeket rendesen jóvá kell hagyatni és dokumentálni, különben felesleges támadási felületet hoz létre.

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

1) Callbacks nach dem Schließen

Asynchrone CompletedHandler können eintreffen, nachdem die Form schon schließt. Im Snippet verhindert FDestroyed den Zugriff auf freigegebene Objekte. Robuster ist zusätzlich:

  • Esemény-tokeneket tárolni és a Destroy során rendesen meghívni a remove_*-okat
  • InitializeAsync csak egyszer engedélyezni (State-Machine: Created/Initializing/Ready/Disposed)

2) Thread-Kontext

Bár sok handler „UI-közeli”, ne számítson arra, hogy közvetlenül FMX-vezérlőkbe írhat. Ha az OnWebMessage-ben frissít UI-t, a TThread.Queue(nil, ...) a biztonságos megoldás. Én szétválasztom a felelősségeket: a host gyűjti az eseményt, az Application-Service dönt, az UI kizárólag Queue-val frissül.

3) DPI/Resize und FMX-Layouts

Az FMX logikai egységekben számol, a WebView2 pixel-recteket vár. A gyakorlatban szüksége van egy egyértelmű pontra, ahol az FMX-vezérlők bounds-át valós pixelekké fordítja. A példakód egy TRect-et feltételez; a Formban ebből kell levezetni a WinAPI-koordinátákat (pl. FMX.Platform.Win és handle-API-k használatával). Ha az alkalmazás monitor-DPI alapján skáláz, tesztelje a monitorok közötti váltást: a WebView2 ebben érzékenyebb, mint a tiszta FMX-vezérlők.

Mikor érdemes WebView2-t FMX-ben – és mikor nem

A WebView2 akkor éri meg, ha egy meglévő Delphi-kliensalkalmazásban célzottan szeretne webes technológiát alkalmazni: beágyazott admin-nézetek, OAuth/OIDC beléptetési folyamatok, HTML-jelentések, belső portálok vagy kontrollált „micro-frontends”. Modernizációs hídként is praktikus, amennyiben a felelősségeket tisztán elválasztja, és a híd nem válik a business-logika ellenőrizetlen hátsó ajtajává.

A megközelítés korlátai:

  • Plattform: A minta Windows-központú. Az FMX többplatformos, a WebView2 nem. macOS/iOS/Android esetén más WebView-kat vagy egy absztrakciós réteget kell alkalmazni.
  • Security/Hardening: Amint külső tartalmak töltődnek be, szigorúbban kell korlátozni a navigációt, az engedélyezett domaineket és a letöltési célokat. Ez a követelmények közé tartozik, nem „később”.
  • Support: A UserDataFolder és a runtime-függőségek (WebView2 Runtime) a működtetési/rollout-koncepció részét kell, hogy képezzék.

Fazit

Delphi WebView2 FMX kevésbé UI-gadget, inkább egy integrációs komponens saját lifecycle-lal. Ha az inicializációt, az eventelést, a UserDataFolder-t és a JS-Bridge-et strukturáltan kapszulázza, a WebView2 stabil építőelem lesz vállalati digitális megoldásokhoz: Web-UI ott, ahol van értelme, és a Delphi-logika ott, ahol hozzá tartozik. Ha viszont kontrollálatlanul futtat szkripteket, a path-okat a véletlenre bízza és az eseményeket nem választja szét, pontosan azt a fajta, terepen sporadikusan előforduló hibát kapja, amely időt emészt és bizalmat ront.

Ha egy meglévő Delphi-alkalmazásba szeretné tisztán integrálni a WebView2-t, vagy technikailag értékelni egy modernizációs él lehetőségét, beszéljen velünk:

A szakmai környezetben a Webview2 Firemonkey és a Delphi Fmx Edge Browser is fontos szerepet játszanak, ha az integrációk, adatfolyamok és a továbbfejlesztés szoros együttműködése szükséges.

Projekt vagy modernizációs kezdeményezés megbeszélése Net-Base.

Következő lépés

Ha egy témából valós projekt lesz, az architektúrát, a meglévő rendszert és az üzemeltetést korai fázisban együtt kell vizsgálni.

Nemcsak egyedi kérdésekben támogatunk, hanem akkor is, amikor forráskódrészletekből, örökölt rendszerekkel kapcsolatos témákból vagy portálötletekből robusztus vállalati projektet kell kialakítani.

  • A jelenlegi állapotot, a célállapotot és a műszaki kockázatokat együttesen értékeljük.
  • REST, az adathozzáférést, a portálokat és a bevezetést nem halasztjuk későbbi fázisokra.
  • Ön korán látja, melyik út gazdaságilag és üzemeltetési szempontból tartható.

Bejegyzés megosztása

Ezt a bejegyzést közvetlenül megosztani

LinkedIn, X, XING, Facebook, WhatsApp és E-Mail azonnal elérhetők. Instagramra a linket és a rövid szöveget közvetlenül előkészítjük.

E-mail

Az Instagram egy új lapon nyílik meg. A link és a rövid szöveg előzetesen a vágólapra másolódik.