Од теме часописа до пројектне праксе
Одговарајуће странице услуга и техничке странице за чланак
Ко год у постојећи бизнис‑софтвер изненада жели „укратко“ уткати модерни веб‑садржај, на 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, опционocid(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
Destroyuredno pozvatiremove_* - Dozvoliti
InitializeAsyncsamo 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, приступ подацима, портали и роллаут се неће одлагати као накнадне последице.
- Ви рано видите који пут је економски и оперативно одржив.