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“.
- 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.
- 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).
- 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 = Truei 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.