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áliscid(Correlation-ID) mezővel éspayload-dal karbantarthatók és tesztelhetők. - Üzembiztos perzisztencia: egy kontrollált
UserDataFoldermegelő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
Destroysorán rendesen meghívni aremove_*-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ó.