Od teme magazina do projektne prakse
Povezane stranice usluga i tehnologije za članak
Tko u postojećem poslovnom softveru iznenada želi „samo tako“ ugraditi moderne web-sadržaje, pri radu s WebView2 naići će na Windows. U Delphi WebView2 FMX osnovni problem rijetko je samo prikaz URL‑a, već čista integracija u FireMonkey sučelje (FMX), pouzdano inicijaliziranje (asinkrono i COM‑temeljeno), kao i zamke Edgea vezane uz UserDataFolder-direktorije, preuzimanja, debugiranje i robusnu JS↔Delphi‑komunikaciju.
Ovaj isječak izvornog koda prikazuje obrazac koji preferiram za održive aplikacije: enkapsulirani „Host“‑objekt koji kontrolira životni ciklus WebView2, te definirani most preko WebMessage (JSON), umjesto proizvoljnog „ExecuteScript svugdje“. Cilj nije demo‑kod, već komponenta koja preživi u etabliranim klijentima.
Warum WebView2 in FMX anders ist als „Browser-Component drop“
WebView2 je COM/WinRT‑bliska API s asinkronom inicijalizacijom. FireMonkey apstrahira Windows‑handleove, no za WebView2 ćete na kraju trebati pravo roditeljsko prozorsko okno (HWND) i kontrolirano prosljeđivanje promjena veličine/fokusa. Istovremeno se događaji ne izvršavaju uvijek tamo gdje biste ih očekivali u FMX‑u. Ako ovdje krenete „quick and dirty“, tipično ćete dobiti:
- sporadične AVs pri zatvaranju forme (Callbacks stižu nakon Destroy)
- navigacijski događaji iz pogrešnog konteksta dretve
- nepouzdana persistencija/problemi s cacheom zbog nejasne UserDataFolder‑strategije
- nema preuzimanja ili „zaglavljeni“ dijalozi preuzimanja
- debugiranje samo preko sreće umjesto ciljane konfiguracije udaljenog debugiranja
Protivotrov je jasan lifecycle: Create → InitializeAsync → Attach → Navigate → Detach/Dispose – i definirana granica između UI‑ja i browser‑engine‑a.
Source‑Schnipsel: WebView2Host für Delphi WebView2 FMX
Slijedeći kod skicira enkapsuliranu host‑klasu koja (1) kreira konfiguraciju WebView2 okruženja, (2) veže controller‑objekt na HWND, (3) povezuje navigacijske i download‑evente i (4) nudi JSON‑baziranu JS‑bridge preko WebMessageReceived. Kod je namjerno „architekturfähig“: enkapsulira COM‑referencije, sprječava callback‑naknadnike nakon Destroy, i dopušta operativne granice poput odvojenih UserDataFoldera „po korisniku“ ili „po stroju“.
unit WebView2Host;
interface
uses
System.SysUtils, System.Classes, System.IOUtils, System.JSON,
Winapi.Windows, Winapi.ActiveX,
FMX.Types,
WebView2, WebView2_TLB; // ovisno o postavkama: WebView2.pas ili 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 može nedostajati ili biti 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;
// Odvezivanje događaja prije oslobađanja COM objekata
UnhookEvents;
FWebView := nil;
FController := nil;
FEnvironment := nil;
inherited;
end;
procedure TWebView2Host.EnsureNotDestroyed;
begin
if FDestroyed then
raise EInvalidOperation.Create('WebView2Host je već uništen.');
end;
function TWebView2Host.MakeUserDataFolder: string;
begin
if FUserDataFolder <> '' then
Exit(FUserDataFolder);
// U praksi: po aplikaciji i po Windows korisniku, ne u direktoriju programa
Result := TPath.Combine(TPath.GetHomePath, 'AppDataLocalMyCompanyMyAppWebView2');
ForceDirectories(Result);
end;
procedure TWebView2Host.InitializeAsync;
var
UserData: string;
Opt: ICoreWebView2EnvironmentOptions;
begin
EnsureNotDestroyed;
UserData := MakeUserDataFolder;
// Opcije: ovdje mogu ići dodatni argumenti preglednika, npr. Remote-Debug
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;
// Povezivanje kontrolera s roditeljskim 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;
// Učiniti inicijalno vidljivim
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));
// Napomena: Za robusno uklanjanje veza trebali biste spremiti tokene.
// U mnogim projektima to je dovoljno ako host živi samo s Formom.
end;
procedure TWebView2Host.UnhookEvents;
begin
// Robusna varijanta: zapamtite tokene i pozovite remove_*.
// Ovdje kao komentar, jer se setup import-jedinice i upravljanje tokenima razlikuju ovisno o 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));
// U praksi: ResultFileName inicijalno prazan, ovisno o izvoru.
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);
// Opcionalno: vlastiti UI za preuzimanje, tada postavite 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 još nije inicijaliziran.');
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.
Svrha pristupa
- Kapsuliranje životnog ciklusa: FMX-forma poznaje samo „Initialize/Navigate/Resize“, ne COM-detalje.
- Bridge s ugovornim sučeljem: JSON-poruke s
name, opcionalnocid(Correlation-ID) ipayloadsu održive i moguće za testiranje. - Operativno sigurna perzistencija: kontrolirani
UserDataFoldersprječava kolizije cache-a, probleme s dozvolama i situacije „radi na računalu developera, ne u proizvodnji“.
JS↔Delphi-Bridge: zašto je WebMessage stabilniji od ExecuteScript
WebView2 nudi više kanala komunikacije. U praksi je ExecuteScript primamljiv, ali teško ga je verzionirati: ubacujete stringove u interpreter bez jasnih kanala za odgovore i bez robusnog mapiranja pogrešaka. PostWebMessageAsString / WebMessageReceived je za razliku od toga definirani kanal.
Rubni slučaj koji se često pojavljuje u korporativnim okruženjima: morate iz web‑frontenda (npr. internog portala) pokrenuti Delphi-workflow (ispis, pristup uređajima, integracija sa legacy sustavima). Tada trebate:
- bijelu listu imena poruka
- Correlation-ID-ove za asinkrone odgovore
- središnje mjesto koje validira payloadove (npr. obavezna polja, ograničenja veličine)
U hostu je to mjesto OnWebMessageReceived. Stvarna validacija treba biti u sloju iznad (npr. Application-Service), kako biste držali UI/WebView2-tehnologiju odvojeno od poslovne logike (klasična slojevita arhitektura: UI → Application → Domain → Infrastruktur).
Preuzimanja i pohrana datoteka: što često iznenadi u produkciji
Preuzimanja u WebView2 koriste ICoreWebView2DownloadOperation. Ovisno o izvoru, ResultFilePath može biti prazan na početku ili biti postavljen tek kasnije. Osim toga, mnoge tvrtke ne žele da krajnji korisnici spremaju u nekontrolirane mape.
Provjereni obrasci:
- Presresti DownloadStarting i pozivom
args.put_Handled(1)preuzeti rukovanje u UI-ju (vlastita putanja, konvencija imenovanja, karantenski direktorij). - Ograničenja veličine datoteka i provjere MIME‑tipa, kako biste izbjegli slučajno preuzimanje 4 GB log datoteke.
- Auditing: zapisati metapodatke preuzimanja (URI, MIME, broj bajtova) u logove, ne sadržaj.
Ako imate regulirane procese (npr. odobrenja, mogućnost revizije), rukovanje putem događaja je jedino mjesto na kojem možete integrirati svijet preglednika u svoje operativne procedure.
Debugiranje: DevTools, Remote Debug Port i reproducibilna stanja
Debugiranje WebView2 često zapinje jer se stanja ne mogu reproducirati. Dva podešavanja pomažu:
- Aktivacija/deaktivacija DevTools preko
ICoreWebView2Settings(u kodu:SetDevToolsEnabled) – u releaseu često isključeno, u slučaju podrške ciljano uključeno. - Stabilan UserDataFolder: ako vaš support treba reproducirati grešku, definirana putanja vrijedi zlata. Možete direktorij sigurnosno kopirati/zipati (Pažnja: zaštita podataka/PII) i ciljano usporediti stanja.
Opcionalno (ovisno o wrapperu) možete EnvironmentOptions opremiti dodatnim argumentima za preglednik, npr. Remote-Debug-Port. To ima smisla kada morate analizirati aplikaciju na testnom sustavu bez lokalnih developerskih alata. Ograničenja: u produkcijskim okruženjima to mora biti uredno odobreno i dokumentirano, inače otvarate nepotreban napadni vektor.
Zamke u Delphi WebView2 FMX: COM, niti i životni ciklus forme
1) Povratni pozivi nakon zatvaranja
Asinkroni CompletedHandleri mogu stizati nakon što se forma već zatvara. U primjeru FDestroyed sprječava pristup oslobođenim objektima. Robusnije je dodatno:
- Pohraniti tokene za događaje i u
Destroyuredno pozvatiremove_* - Dopustiti InitializeAsync samo jednom (stroj stanja: Created/Initializing/Ready/Disposed)
2) Kontekst niti
Mnogi handleri dolaze „blizu UI“, ali ne oslanjajte se na to da možete izravno pisati u FMX-kontrolama. Ako u OnWebMessage ažurirate UI, TThread.Queue(nil, ...) je sigurna varijanta. Volim razdvojiti odgovornosti: host prikuplja događaj, aplikacijski servis odlučuje, a UI se ažurira isključivo putem Queue.
3) DPI/Resize und FMX-Layouts
FMX računa u logičkim jedinicama, WebView2 očekuje pravokutnike u pikselima. U praksi trebate jasno mjesto na kojem prevodite Bounds FMX-kontrola u stvarne piksele. Primjer očekuje TRect; u vašoj formi trebali biste iz njega izvesti WinAPI-koordinate (npr. preko FMX.Platform.Win i Handle-API-ja). Ako se aplikacija skaluje prema DPI monitora, testirajte prelazak između monitora: WebView2 je ovdje osjetljiviji od čistih FMX-kontrola.
Kada se WebView2 u FMX isplati – a kada ne
WebView2 se isplati kad u zreloj Delphi-klijentskoj aplikaciji želite ciljano koristiti web-tehnologiju: ugrađeni admin-prikazi, OAuth/OIDC login-tokovi, HTML-izvještaji, interni portali ili kontrolirani „Micro-Frontends“. Također je praktičan kao most za modernizaciju, sve dok jasno razgraničite odgovornosti i ne dopustite da most postane nekontrolirana stražnja vrata za poslovnu logiku.
Ograničenja pristupa:
- Platforma: Uzorak je Windows-centriran. FMX je multiplatforman, WebView2 nije. Za macOS/iOS/Android trebate druge WebView-e ili sloj apstrakcije.
- Security/Hardening: Kad se učitavaju vanjski sadržaji, morate strože ograničiti navigaciju, dopuštene domene i ciljeve preuzimanja. To treba biti dio zahtjeva, ne „kasnije“.
- Support: UserDataFolder i runtime-ovisnosti (WebView2 Runtime) moraju biti dio vašeg koncepta za operacije/rollout.
Zaključak
Delphi WebView2 FMX nije toliko UI-gadget koliko komponenta integracije s vlastitim životnim ciklusom. Ako inicijalizaciju, eventing, UserDataFolder i JS-Bridge strukturirano uokvirite, WebView2 će postati stabilan građevni blok za digitalna rješenja u poduzeću: Web-UI tamo gdje ima smisla, i Delphi-logika tamo gdje pripada. Ako pak nekontrolirano pokrećete skripte, ostavljate puteve na milost slučaja i ne razdvajate događaje, dobivate upravo onu vrstu „sporadičnih u polju“ pogrešaka koja troši vrijeme i narušava povjerenje.
Ako želite u postojećoj Delphi-aplikaciji uredno integrirati WebView2 ili tehnički procijeniti rub modernizacije, razgovarajte s nama:
U stručnom okruženju važnu ulogu igraju i Webview2 Firemonkey i Delphi Fmx Edge Browser, kad je potrebno da se integracije, tokovi podataka i daljnji razvoj uredno usklade.
Sljedeći korak
Kad se tema pretvori u stvarni projekt, arhitektura, postojeći sustav i operativni rad trebaju se rano sagledati zajedno.
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 kasne posljedice.
- Vidite rano koji je put ekonomski i operativno održiv.