Net-Base Časopis

14.06.2026

Delphi WebView2 u FMX-u: put odlučivanja za životni ciklus, Bridge, preuzimanja i stabilnu Dispose-logiku

Delphi WebView2 FMX djeluje jednostavno, ali u stvarnim Windows klijentima često zakaže zbog životnog ciklusa, Dispose, UserDataFoldera, Bridge-ugovora i rukovanja preuzimanjima. Izvadak izvornog koda sa State-Machine i Token-Unhookingom.

14.06.2026

Od teme magazina do projektne prakse

Povezane stranice usluga i tehnologije za članak

„Treba nam samo kratko ugraditi Web-UI – login, jedan izvještaj, mali interni portal.“ Upravo na tom mjestu Delphi WebView2 FMX u pravim Windows klijentima redovito dovodi do problema: ne pri renderiranju prve URL, nego pri lifecycleu (asinkrono, COM-bazirano), pri zatvaranju forme, kod preuzimanja bez kontrole i kod JS↔Delphi komunikacije koja mora ostati rekonstruabilna u slučaju podrške.

Konstruirana, ali tipična situacija iz modernizacija: rastući FMX-klijent (Windows) treba prikazati OAuth/OIDC login-flow i potom React-bazirani „Admin“ dio. Paralelno postoje naslijeđeni workflowi (ispis, izvoz datoteka, pristup uređajima) koje treba pokretati iz web-frontenda. Prva iteracija radi na developerovom računalu. Dvije tjedna kasnije: sporadični AV-ovi pri zatvaranju, „preuzimanja nestaju“, a podrška ne može reproducirati grešku jer su cache/storage različiti po strojevima.

Ovaj izrezak izvornog koda je strukturiran kao put odlučivanja: svjesno odaberete koliko duboko želite integrirati WebView2 u svoju FMX-aplikaciju. U srži je enkapsulirani host sa State-Machine, definiranoj UserDataFolder-strategiji, JSON-Bridge preko WebMessage i Download-hookom koji ne iznenađuje ni u radu u produkciji.

Put odlučivanja: Koliko „puno“ Delphi WebView2 FMX vam stvarno treba?

Prije nego što nastane kod, isplati se jasna odluka. WebView2 se može kretati od „ugrađenog prikaza“ do „potpune integracijske slojeve“.

  1. Samo prikaz (Read-only): Navigirate na URL i trebate minimalnu interakciju. U tom slučaju često je dovoljna minimalna inicijalizacija, ali i dalje biste trebali postaviti stabilan UserDataFolder i čist Dispose.
  2. Interakcija (Commands, prijenos podataka): Web-UI treba pokretati Delphi-workflove. Tada trebate Bridge s ugovorom (shema poruka, whitelist, korelacija) i odvajanje od poslovne logike (slojevi: UI → Aplikacija → Domen → Infrastruktura).
  3. Betriebsrelevante Features (Downloads, Debugging, Support): Čim postanu važni datoteke, DevTools, Remote-Debug ili reprodukcija za podršku, morate dizajnirati evente, puteve, politike i logging tako da budu održivi na terenu.
Entscheidung Wenn Sie das ignorieren, passiert meist… Minimum-Maßnahme
UserDataFolder nicht gesetzt Problemi s pravima, nereproducibilna stanja, podrška ne može rekonstruirati bugove Put po Windows-korisniku izvan instalacijskog direktorija
Keine State-Machine/Dispose-Logik AV-ovi pri zatvaranju forme, handleri se triggeraju nakon Destroy Disposed-Flag + odspojiti tokene + redoslijed postavljanja na nil
ExecuteScript als „Bridge“ Neverzionirani string-skripti, teško rukovanje greškama, nuspojave „negdje“ WebMessage s JSON-ugovorom i whitelistom
Downloads nicht abgefangen Nekontrolirano spremanje, „zaglavljeni“ dijalozi, pitanja usklađenosti Pretplatiti se na DownloadStarting, opcionalno označiti kao handled + vlastita putanja

WebView2 in FMX: warum es kein „Component drop“ ist

WebView2 je COM-blizu API (Component Object Model: referentno brojanje, sučelja, događaji) s asinkronom inicijalizacijom. FMX (FireMonkey) apstrahira Window-Handles, ali WebView2 na kraju zahtijeva Parent-HWND i jasne granice u pikselima. Uz to: događaji ne dolaze nužno u kontekst u kojem možete sigurno pristupiti FMX kontrolama.

Ovdje su važna dva središnja, robusna uporišta: prvo službena WebView2-arkitektura kao runtime komponenta (Edge Runtime + SDK), uključujući koncept User Data Foldera, koji Microsoft opisuje kao spremište za persistenciju i profile (Microsoft Learn: WebView2). Drugo, model događaja i poruka (WebMessageReceived, PostWebMessageAsString), predviđen kao definirani kanal za komunikaciju aplikacije↔web (Microsoft Learn: Communication between host and web content).

Izvadak izvornog koda: Host s state-machine, Token-Unhook i JSON-Bridge

Sljedeći kod nije namjerno „najkraći mogući“, već je prilagođen pouzdanom radu. Neobična, ali u praksi presudna točka: spremiti EventRegistrationToken i pri Dispose eksplicitno pozvati remove_*. To značajno smanjuje klasične slučajeve „callback nakon Destroy“.

Napomena: Ovisno o Delphi-postavu, koristite uvoz WebView2-TLB-a ili Wrapper-Unit. Potpisi (Signaturen) su vrlo slični, ali pojedinačna imena tipova mogu varirati.

unit NbWebView2Host;

interface

uses
System.SysUtils, System.Classes, System.IOUtils, System.JSON,
Winapi.Windows, Winapi.ActiveX,
FMX.Types,
WebView2, WebView2_TLB;

type
TWebView2State = (wvsCreated, wvsInitializing, wvsReady, wvsDisposed);

TWebView2JsonMessage = record
Name: string;
Cid: string;
Payload: TJSONObject;
class function TryParse(const S: string; out M: TWebView2JsonMessage): Boolean; static;
end;

TOnWebMessage = reference to procedure(const M: TWebView2JsonMessage);
TOnDownloadMeta = reference to procedure(const Uri, MimeType, SuggestedFileName: string; TotalBytes: Int64);

TNbWebView2Host = class
private
FParentHwnd: HWND;
FState: TWebView2State;

FUserDataFolder: string;
FEnvironment: ICoreWebView2Environment;
FController: ICoreWebView2Controller;
FWebView: ICoreWebView2;

FTokenWebMsg: EventRegistrationToken;
FTokenDownload: EventRegistrationToken;

FOnWebMessage: TOnWebMessage;
FOnDownloadMeta: TOnDownloadMeta;

procedure RequiRESTate(const Allowed: array of TWebView2State);
function EffectiveUserDataFolder: string;

procedure HookEvents;
procedure UnhookEvents;

procedure HandleWebMessage(const args: ICoreWebView2WebMessageReceivedEventArgs);
procedure HandleDownloadStarting(const args: ICoreWebView2DownloadStartingEventArgs);

public
constructor Create(AParentHwnd: HWND; const AUserDataFolder: string = “);
destructor Destroy; override;

procedure InitializeAsync;
procedure Dispose;

procedure Navigate(const Url: string);
procedure ResizePx(const BoundsPx: TRect);

procedure PostJson(const Obj: TJSONObject);

property State: TWebView2State read FState;
property WebView: ICoreWebView2 read FWebView;

property OnWebMessage: TOnWebMessage read FOnWebMessage write FOnWebMessage;
property OnDownloadMeta: TOnDownloadMeta read FOnDownloadMeta write FOnDownloadMeta;
end;

implementation

class function TWebView2JsonMessage.TryParse(const S: string; out M: TWebView2JsonMessage): Boolean;
var
V: TJSONValue;
O: TJSONObject;
P: TJSONValue;
begin
Result := False;
M.Name := “;
M.Cid := “;
M.Payload := nil;

V := TJSONObject.ParseJSONValue(S);
try
if not (V is TJSONObject) then Exit;
O := TJSONObject(V);

M.Name := O.GetValue(’name‘, “);
M.Cid := O.GetValue(‚cid‘, “);

if O.TryGetValue(‚payload‘, P) and (P is TJSONObject) then
M.Payload := TJSONObject(P.Clone)
else
M.Payload := TJSONObject.Create;

Result := M.Name <> “;
finally
V.Free;
end;
end;

constructor TNbWebView2Host.Create(AParentHwnd: HWND; const AUserDataFolder: string);
begin
inherited Create;
FParentHwnd := AParentHwnd;
FUserDataFolder := AUserDataFolder;
FState := wvsCreated;
FTokenWebMsg.value := 0;
FTokenDownload.value := 0;
end;

destructor TNbWebView2Host.Destroy;
begin
Dispose;
inherited;
end;

procedure TNbWebView2Host.RequiRESTate(const Allowed: array of TWebView2State);
var
S: TWebView2State;
Ok: Boolean;
I: Integer;
begin
Ok := False;
for I := Low(Allowed) to High(Allowed) do
if FState = Allowed[I] then
Ok := True;

if not Ok then
begin
for S := Low(TWebView2State) to High(TWebView2State) do begin end; // samo da bi Enum ostao vidljiv u debuggeru
raise EInvalidOperation.CreateFmt(‚WebView2Host: nevažeće stanje (%d).‘, [Ord(FState)]);
end;
end;

function TNbWebView2Host.EffectiveUserDataFolder: string;
begin
if FUserDataFolder <> “ then
Exit(FUserDataFolder);

// Stabilniji, po Windows-korisniku; namjerno ne u direktoriju instalacije.
Result := TPath.Combine(TPath.GetHomePath, ‚MyCompany\MyApp\WebView2UserData‘);
ForceDirectories(Result);
end;

procedure TNbWebView2Host.InitializeAsync;
var
Opt: ICoreWebView2EnvironmentOptions;
UserData: string;
begin
RequiRESTate([wvsCreated]);
FState := wvsInitializing;

UserData := EffectiveUserDataFolder;
Opt := TCoreWebView2EnvironmentOptions.Create;

OleCheck(CreateCoreWebView2EnvironmentWithOptions(
nil,
PWideChar(UserData),
Opt,
TCoreWebView2CreateCoreWebView2EnvironmentCompletedHandler.Create(
procedure(errorCode: HRESULT; const createdEnvironment: ICoreWebView2Environment)
begin
if FState = wvsDisposed then Exit;
OleCheck(errorCode);
FEnvironment := createdEnvironment;

OleCheck(FEnvironment.CreateCoreWebView2Controller(
FParentHwnd,
TCoreWebView2CreateCoreWebView2ControllerCompletedHandler.Create(
procedure(errorCode2: HRESULT; const createdController: ICoreWebView2Controller)
begin
if FState = wvsDisposed then Exit;
OleCheck(errorCode2);

FController := createdController;
OleCheck(FController.get_CoreWebView2(FWebView));

HookEvents;
OleCheck(FController.put_IsVisible(1));
FState := wvsReady;
end)));
end)));
end;

procedure TNbWebView2Host.HookEvents;
begin
if FWebView = nil then Exit;

OleCheck(FWebView.add_WebMessageReceived(
TCoreWebView2WebMessageReceivedEventHandler.Create(
procedure(const sender: ICoreWebView2; const args: ICoreWebView2WebMessageReceivedEventArgs)
begin
if FState = wvsDisposed then Exit;
HandleWebMessage(args);
end),
FTokenWebMsg));

OleCheck(FWebView.add_DownloadStarting(
TCoreWebView2DownloadStartingEventHandler.Create(
procedure(const sender: ICoreWebView2; const args: ICoreWebView2DownloadStartingEventArgs)
begin
if FState = wvsDisposed then Exit;
HandleDownloadStarting(args);
end),
FTokenDownload));
end;

procedure TNbWebView2Host.UnhookEvents;
begin
// Važno: remove_* smanjuje naknadne pozive nakon Dispose.
if FWebView <> nil then
begin
if FTokenWebMsg.value <> 0 then
FWebView.remove_WebMessageReceived(FTokenWebMsg);
if FTokenDownload.value <> 0 then
FWebView.remove_DownloadStarting(FTokenDownload);
end;
FTokenWebMsg.value := 0;
FTokenDownload.value := 0;
end;

procedure TNbWebView2Host.Dispose;
begin
if FState = wvsDisposed then Exit;
FState := wvsDisposed;

UnhookEvents;

// Redoslijed: prvo osloboditi WebView/Controller, zatim Environment.
FWebView := nil;
FController := nil;
FEnvironment := nil;
end;

procedure TNbWebView2Host.Navigate(const Url: string);
begin
RequiRESTate([wvsReady]);
OleCheck(FWebView.Navigate(PWideChar(Url)));
end;

procedure TNbWebView2Host.ResizePx(const BoundsPx: TRect);
var
R: tagRECT;
begin
if (FState <> wvsReady) or (FController = nil) then Exit;

R.Left := BoundsPx.Left;
R.Top := BoundsPx.Top;
R.Right := BoundsPx.Right;
R.Bottom := BoundsPx.Bottom;
OleCheck(FController.put_Bounds(R));
end;

procedure TNbWebView2Host.PostJson(const Obj: TJSONObject);
var
S: string;
begin
if (FState <> wvsReady) or (FWebView = nil) then Exit;
S := Obj.ToJSON;
OleCheck(FWebView.PostWebMessageAsString(PWideChar(S)));
end;

procedure TNbWebView2Host.HandleWebMessage(const args: ICoreWebView2WebMessageReceivedEventArgs);
var
P: PWideChar;
S: string;
M: TWebView2JsonMessage;
begin
P := nil;
OleCheck(args.TryGetWebMessageAsString(P));
try
S := P;
finally
CoTaskMemFree(P);
end;

if not Assigned(FOnWebMessage) then Exit;
if not TWebView2JsonMessage.TryParse(S, M) then Exit;

try
// Ovdje ne prisiljavajte izravnu nadogradnju UI-a: u sloju aplikacije po potrebi koristiti TThread.Queue.
FOnWebMessage(M);
finally
M.Payload.Free;
end;
end;

procedure TNbWebView2Host.HandleDownloadStarting(const args: ICoreWebView2DownloadStartingEventArgs);
var
Dl: ICoreWebView2DownloadOperation;
Uri, Mime, ResultPath: PWideChar;
Total: Int64;
Suggested: string;
begin
Uri := nil;
Mime := nil;
ResultPath := nil;

OleCheck(args.get_DownloadOperation(Dl));
OleCheck(Dl.get_TotalBytesToReceive(Total));
OleCheck(Dl.get_Uri(Uri));
OleCheck(Dl.get_MimeType(Mime));
OleCheck(Dl.get_ResultFilePath(ResultPath));

try
Suggested := ExtractFileName(string(ResultPath));
if Suggested = “ then Suggested := ‚download.bin‘;

if Assigned(FOnDownloadMeta) then
FOnDownloadMeta(string(Uri), string(Mime), Suggested, Total);

// Varijanta: omogućiti vlastito rukovanje
// args.put_Handled(1);
finally
CoTaskMemFree(Uri);
CoTaskMemFree(Mime);
CoTaskMemFree(ResultPath);
end;
end;

end.

Što je na ovom isječku „neobično čisto”

  • State-Machine umjesto Bool: wvsCreated → wvsInitializing → wvsReady → wvsDisposed. To čini pogrešnu upotrebu (Navigate prerano, Dispose dvaput) vidljivom.
  • Eksplicitno odspajanje: EventRegistrationToken se pohranjuje i uklanja u Dispose. To je često izostavljena stavka koja u razvijenim klijentima donosi stvarnu stabilnost.
  • UserDataFolder svjesno odabran: po korisniku, izvan puta instalacije. To nije „nice to have”, nego poluga za podršku i operacije.

Rubni slučajevi: Threading, DPI i životni ciklus forme u FMX

Povratni pozivi i FMX-nit

Čak i kada mnogi WebView2-callbackovi izgledaju „blizu UI-ju”: tretirajte svaku Bridge-poruku kao da dolazi iz stranog konteksta. Sigurno pravilo: FMX-UI dirajte samo preko TThread.Queue(nil, ...). Arhitektonski pojam: afinitet niti znači da su UI-objekti vezani za određenu nit.

Resize i DPI-skaliranje

FMX radi u logičkim koordinatama, WebView2 želi pixel-rects. Ako vaša aplikacija skalira po DPI-ju monitora (prelazak između monitora 100%/150%), testirajte ciljano: resize pri promjeni monitora i pri maksimiziranju/RESTore. Tu često nastaju „treptanje od 1 piksela”, pogrešni hit-testovi ili odrezani rubovi ako se konverzija ne riješi centralno.

Preuzimanja: usklađenost, pohrana i „zašto je ResultFilePath prazan?”

WebView2-preuzimanja idu preko ICoreWebView2DownloadOperation. U radu su relevantne dvije stvari: prvo, ResultFilePath nije zajamčeno rani i pouzdan pokazatelj. Drugo, mnoge tvrtke žele kontrolirati mjesta pohrane (mape projekta, karantena, bez razbacanih mapa „Downloads“).

U zrelim klijentima to su tipična pravila:

  • Ograničenje veličine i MIME-filter (npr. blokirati „application/x-msdownload” ovisno o politici).
  • Logiranje metapodataka (URI, MIME, bajtovi) kao audit-trag; sadržaj ne upisivati u log.
  • Opcionalno args.Handled = True i vlastiti put/ime ako procesi to zahtijevaju.

Jedno uredničko zapažanje

Iz perspektive uredništva i operacija najčešća greška nije „premalo znanja o WebView2”, nego nedostatak odluke: započne se s mini-prototipom i preuzme ga se neizmijenjenog u produktivnu poslovnu softversku rješenje. WebView2 tada nije problem — problem su nedostatna enkapsulacija i odsutna operativna strategija.

Granice i rizici: kada WebView2 u FMX nije pravi izbor

  • Višeplatformsko: WebView2 je Windows. Ako vaš FMX-klijent doista mora raditi na macOS/iOS/Android, planirajte apstrakciju WebView-a ili odvojite web-UI na drugi način.
  • Sigurnosni model: Čim se učitavaju vanjski sadržaji, trebate RESTrikcije domena, navigacijske politike i jasan koncept ažuriranja/runtimea. Inače u klijenta gradite neželjenu površinu za napad.
  • Mogućnost podrške: Definirani UserDataFolder pomaže, ali može sadržavati i osobne podatke (Cookies, Storage). Za slučajeve podrške moraju se razjasniti zaštita podataka i procesi (npr. kako izvesti, očistiti ili anonimizirati mape).

Zaključak: Delphi WebView2 FMX kao integracijska komponenta, a ne kao UI-gimik

Ako tretirate Delphi WebView2 FMX kao integracijsku komponentu – sa State-Machine, urednim Dispose, Token-Unhooking, kontroliranim WebView2 User Data Folder i verzioniranom JSON-Bridge – postaje robusna modernizacijska ivica za postojeće klijente. Posebno je korisno kad ciljano koristite web-tehnologiju za prijavne tokove, ugrađene portale ili administrativne prikaze, ali svjesno držite jezgru logike u Delphi.

Granica primjene: čim počnete preko bridgea „bilo koju“ poslovnu logiku iz weba gurati u Delphi, gubite mogućnost testiranja, upravljanja verzijama i, u slučaju sumnje, kontrolu nad sigurnošću i radom. Tada više nije pitanje WebView2, nego arhitektonske discipline.

Za ovu temu su također važni Delphi Fmx Edge Browser i Webview2 Firemonkey. Članak ove aspekte stavlja u razumljiv kontekst i pokazuje na što se treba obratiti u svakodnevnoj praksi.

Razgovarajte o projektu ili modernizacijskom zahvatu s Net-Base.

sljedeći korak

Ako se tema pretvori u stvarni projekt, arhitekturu, postojeće sustave i operacije trebalo bi rano zajednički razmotriti.

Podržavamo vas ne samo u pojedinačnim pitanjima, već i kada iz isječaka izvornog koda, naslijeđenih sustava ili ideja za portale treba nastati pouzdan poslovni projekt.

  • Postojeće stanje, ciljna slika i tehnički rizici procjenjuju se zajedno.
  • REST, pristup podacima, portali i rollout neće biti odgođeni kao naknadne posljedice.
  • Rano prepoznajete koji je put ekonomski i operativno održiv.

Podijeli objavu

Izravno proslijedite ovu objavu

LinkedIn, X, XING, Facebook, WhatsApp i e-pošta su odmah dostupni. Za Instagram odmah pripremamo poveznicu i kratak tekst.

E-pošta

Instagram se otvara u novoj kartici. Link i kratki tekst se prethodno kopiraju u međuspremnik.

Napišite komentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert