Net-Base Магазин

14.06.2026

Delphi WebView2 у FMX: исправно иницијализовати, изградити JS-бридж, преузимања и отклањање грешака држати под контролом

WebView2 у FireMonkey звучи као „једноставно уградити прегледач“, али у пракси пада при иницијализацији, догађајима навигације, JS↔Delphi-Bridge, руковању преузимањима и отклањању грешака. Овај исечак изворног кода показује робустан образац са јасним одговорностима...

14.06.2026

Од теме часописа до пројектне праксе

Одговарајуће странице услуга и техничке странице за чланак

Ко год у постојећи бизнис‑софтвер изненада жели „укратко“ уткати модерни веб‑садржај, на Windows ће наићи на WebView2. У Delphi WebView2 FMX основни проблем ретко је само приказивање URL‑а, већ чиста интеграција у FireMonkey‑површину (FMX), поуздано иницијализовање (асинхроно и COM‑базирано), као и Edge‑замке везане за User‑Data директоријуме, преузимања, дебаговање и робусну JS↔Delphi комуникацију.

Овај исечак кода показује образац који предпочитам за одрживе апликације: енкапсулирани „Host“ објекat који контролише WebView2 животни циклус, као и дефинисани мост преко WebMessage (JSON), уместо произвољних „ExecuteScript“ позива на сваких пар редова. Циљ није демо‑код, већ грађевни блок који преживљава у развијеним клијентима.

Зашто је WebView2 у FMX другачији него „Browser-Component drop“

WebView2 је COM/WinRT‑близак API са асинхроним иницијализовањем. FireMonkey апстрахује Windows‑handle‑ове, ипак вам за WebView2 на крају треба прави parent‑window (HWND) и контролисано прослеђивање resize/ focus догађаја. Истовремено, догађаји не увек теку тамо где их у FMX очекујете. Ако овде стартујете „quick and dirty“, типично ћете добити:

  • спорадичне AV при затварању форме (callback‑ови стижу након Destroy)
  • навигациони догађаји који долазе из погрешног контекста нити
  • непоуздану перзистенцију/проблеме са кеширањем због нејасне UserDataFolder‑стратегије
  • нема преузимања или „заглављени“ дијалози за преузимање
  • Debugging више на срећу него с намерном Remote‑Debug конфигурацијом

Противотров је јасан lifecycle: Create → InitializeAsync → Attach → Navigate → Detach/Dispose – и дефинисана граница између UI‑ја и browser‑engine‑а.

Изворни исечак: WebView2Host за Delphi WebView2 FMX

Следећи код скицира енкапсулирану Host‑класу која (1) прави WebView2‑Environment конфигурацију, (2) везује Controller‑објекat за HWND, (3) ожичава навигационе и download догађаје и (4) пружа JSON‑базирану JS‑bridge преко WebMessageReceived. Код је намерно погодан за архитектуру: енкапсулира COM‑референце, спречава callback‑наследнике после Destroy и дозвољава оперативне варијанте као што су „по кориснику“ или „по машини“ са одвојеним UserDataFolder.unit WebView2Host;

interface

uses
System.SysUtils, System.Classes, System.IOUtils, System.JSON,
Winapi.Windows, Winapi.ActiveX,
FMX.Types,
WebView2, WebView2_TLB; // у зависности од подешавања: WebView2.pas или 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;

// Обрађивач догађаја
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 може да недостаје или да буде 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;

// Уклонити обрађиваче догађаја пре ослобађања COM објеката
UnhookEvents;

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

inherited;
end;

procedure TWebView2Host.EnsureNotDestroyed;
begin
if FDestroyed then
raise EInvalidOperation.Create(‚WebView2Host је већ уништен.‘);
end;

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

// Пракса: по апликацији и по Windows кориснику, не у директоријуму програма
Result := TPath.Combine(TPath.GetHomePath, ‚AppDataLocalMyCompanyMyAppWebView2‘);
ForceDirectories(Result);
end;

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

UserData := MakeUserDataFolder;

// Опције: овде се могу додати додатни аргументи за прегледач, нпр. Remote-Debug
Opt := TCoreWebView2EnvironmentOptions.Create;

// Асинхроно креирање окружења
OleCheck(CreateCoreWebView2EnvironmentWithOptions(
nil, PWideChar(UserData), Opt,
TCoreWebView2CreateCoreWebView2EnvironmentCompletedHandler.Create(
procedure (errorCode: HRESULT; const createdEnvironment: ICoreWebView2Environment)
begin
if FDestroyed then Exit;
OleCheck(errorCode);

FEnvironment := createdEnvironment;

// Везати контролер за родитељски 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;

// Почетно учинити видљивим
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));

// Напомена: за поуздано одјављивање догађаја требало би сачувати токене.
// У многим пројектима је то довољно ако хост живи исто колико и форма.
end;

procedure TWebView2Host.UnhookEvents;
begin
// Поуздана варијанта: сачувати токене и позвати remove_*.
// Овде као коментар, јер се подешавање import-юнита и руковање токенима разликује у зависности од wrapper-а.
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));

// У пракси: резултно име фајла може бити иницијално празно, у зависности од извора.
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);

// Опционо: сопствени интерфејс за преузимање; у том случају подесити 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 још није иницијализован.‘);

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.

Сврха приступа

  • Енкапсулација животног циклуса: FMX-форма познаје само Initialize/Navigate/Resize, не детаље COM-а.
  • Bridge са уговором: JSON-поруке са name, опционo cid (Correlation-ID) и payload могућe је одржавати и тестирати.
  • Поузданa persistencija у раду: контролисани UserDataFolder спречава колизије кеша, проблеме са правима и ситуације „ради на развојном рачунару, не у производњи“.

JS↔Delphi-Bridge: зашто је WebMessage стабилнији од ExecuteScript

WebView2 нуди више начина комуникације. У пракси је ExecuteScript примамљив, али тежак за верзионисање: гурате string-ове у интерпретатор без јасних канала за одговоре и без робусног мапирања грешака. PostWebMessageAsString / WebMessageReceived за разлику представљају дефинисан канал.

Гранични случај који се често јавља у корпоративним окружењима: морате из Web-Frontend-а (нпр. интерни портал) покренути Delphi-workflow (штампа, приступ уређајима, интеграција са legacy системима). Тада вам требају:

  • whitelista имена порука
  • Correlation-ID-е за асинхроне одговоре
  • централно место које валидира payload-ове (нпр. обавезна поља, лимити величине)

У хосту је то место OnWebMessageReceived. Стварна валидација треба да припада вишем слоју (нпр. Application-Service), како бисте одвојили UI/WebView2 технологију од бизнис-логике (класична слојевита архитектура: UI → Application → Domain → Infrastruktur).

Преузимања и складиштење фајлова: шта у раду често изненади

Преузимања у WebView2 обрађују се преко ICoreWebView2DownloadOperation. У зависности од извора, ResultFilePath може на почетку бити празан или се попунити тек касније. Поред тога, многе компаније не желе да крајњи корисници сачувавају у неконтролисане фасцикле.

Проверени обрасци:

  • Presretanje DownloadStarting и путем args.put_Handled(1) преузети контролу у UI-ју (сопствени пут, конвенција именовања, карантин-фасцикла).
  • Ограничења величине фајла и MIME-type провере, да бисте избегли „случајно 4 GB log файла“.
  • Auditing: записивати метаподатке преузимања (URI, MIME, байтови) у ваш лог, а не сам садржај.

Ако имате регулсане процесе (нпр. одобрења, слeдљивост), руковање кроз event-ове је једино место где можете интегрисати свет прегледача у ваше оперативне смернице.

Debugovanje: DevTools, Remote Debug Port и репродуктивна стања

WebView2 debug-овање често пати јер стања нису репродуктивна. Две полуге помажу:

  • Укључивање/исключивање DevTools преко ICoreWebView2Settings (у коду: SetDevToolsEnabled) – у release-у често искључено, у случају подршке укључити циљано.
  • Стабилан UserDataFolder: ако ваша подршка треба да пресели грешку, дефинисан пут вреди злата. Можете фасциклу сачувати/zip-овати (Пажња: заштита података/PII) и циљано упоредити стања.

Опционо (у зависности од wrapper-а) можете конфигурисати EnvironmentOptions са додатним аргументима за прегледач, нпр. Remote-Debug-Port. То је смислено када треба анализирати апликацију на тест-систему без локалних developerskih алата. Ограничења: у продуктивним окружењима то мора бити јасно овлашћено и документовано, иначе отварате непотребан нападни вектор.

Замке у Delphi WebView2 FMX: COM, нитови и животни циклус форме

1) Callbacks nakon zatvaranja

Asinhroni CompletedHandler-i mogu stići nakon što se forma već zatvara. U primeru FDestroyed sprečava pristup oslobođenim objektima. Robusnije je dodatno:

  • Čuvati tokene za događaje i u Destroy uredno pozvati remove_*
  • Dozvoliti InitializeAsync samo jednom (stanje: Created/Initializing/Ready/Disposed)

2) Kontekst niti

Mnogi handler-i dolaze „UI-blizu“, ali se ne oslanjajte na to da možete direktno pisati u FMX-Controls. Ako u OnWebMessage ažurirate UI, TThread.Queue(nil, ...) je sigurna varijanta. Ja volim da razdvojim: Host prikuplja događaj, Application-Service donosi odluku, UI se ažurira isključivo preko Queue.

3) DPI/Resize und FMX-Layouts

FMX računa u logičkim jedinicama, WebView2 očekuje pixel-rects. U praksi vam treba jasno mesto gde iz FMX-Controls prevodite bounds u prave piksele. Primer očekuje TRect; u vašoj formi iz njega treba izvesti WinAPI-koordinate (npr. preko FMX.Platform.Win i Handle-APIs). Ako se aplikacija skalira po monitor-DPI-ju, testirajte prelazak između monitora: WebView2 je ovde osetljiviji nego čisti FMX-Controls.

Kada se WebView2 u FMX isplati – a kada ne

WebView2 se isplati kada u već postojeću Delphi-klijent-aplikaciju ciljano želite da uključite web-tehnologiju: ugrađeni admin-views, OAuth/OIDC login-flow-ovi, HTML-izveštaji, internI portali ili kontrolisani „Micro-Frontends“. Takođe, kao most za modernizaciju je praktičan, pod uslovom da jasno razdvojite odgovornosti i ne dozvolite da Bridge postane nekontrolisana zadnja vrata za poslovnu logiku.

Ograničenja pristupa:

  • Plattform: Šablon je Windows-centrisan. FMX je multiplatforman, WebView2 nije. Za macOS/iOS/Android biće vam potrebni drugi WebView-ovi ili sloj apstrakcije.
  • Security/Hardening: Čim se učitavaju eksterni sadržaji, morate strože ograničiti navigaciju, dozvoljene domene i odredišta za preuzimanje. To treba biti deo Requirements, ne „kasnije“.
  • Support: UserDataFolder i runtime-zavisnosti (WebView2 Runtime) moraju biti deo vašeg operativnog/rollout koncepta.

Zaključak

Delphi WebView2 FMX nije toliko UI-gadget koliko integraciona komponenta sa sopstvenim lifecycle-om. Ako inicijalizaciju, eventing, UserDataFolder i JS-Bridge strukturirano kapsulišete, WebView2 postaje stabilan građevni blok za digitalna rešenja preduzeća: Web-UI tamo gde ima smisla, i Delphi-logika tamo gde joj je mesto. Ako, pak, nasumično palite skripte, prepuštate puteve slučaju i ne dekoplektujete evente, dobićete upravo onu vrstu „sporadičnih na terenu“ grešaka koja troši vreme i narušava poverenje.

Ako želite da u postojećoj Delphi-aplikaciji WebView2 uredno integrišete ili tehnički ocenite ivicu modernizacije, obratite nam se:

U stručnom kontekstu važnu ulogu igraju i Webview2 Firemonkey i Delphi Fmx Edge Browser kada integracije, tokovi podataka i dalji razvoj moraju delovati koordinisano.

Razgovarajte o projektu ili planu modernizacije sa Net-Base.

Следећи корак

Када тема прерасте у реалан пројекат, архитектуру, постојеће системе и операције треба рано разматрати заједно.

Подржавамо не само у појединачним питањима, већ и када из исечака изворног кода, застарелих тема или идеја за портале треба да настане поуздан корпоративни пројекат.

  • Постојеће стање, циљано стање и технички ризици оцењују се заједно.
  • REST, приступ подацима, портали и роллаут се неће одлагати као накнадне последице.
  • Ви рано видите који пут је економски и оперативно одржив.

Подели објаву

Поделите ову објаву директно

LinkedIn, X, XING, Facebook, WhatsApp и е-пошта су одмах доступни. За Instagram припремамо линк и кратак текст.

Е-пошта

Инстаграм се отвара у новој картици. Линк и кратак текст се претходно копирају у међуспремник.