Od teme v reviji do projektne prakse
Ustrezne strani storitev in tehnični opisi k prispevku
Kdor želi v obstoječo poslovno programsko opremo na hitro vgraditi sodobne spletne vsebine, se pri Windows znajde pri WebView2. V Delphi WebView2 FMX osnovni problem redko predstavlja prikaz URL, temveč čista vgradnja v vmesnik FireMonkey (FMX), zanesljiva inicializacija (asinhrona in COM-podprta), ter pasti Edge v zvezi z User-Data-verižniki, prenosi, odpravljanjem napak in robustno JS↔Delphi-komunikacijo.
Ta izsek iz vira prikazuje vzorec, ki ga raje uporabljam za vzdržljive aplikacije: enkapsuliran »Host«-objekt, ki nadzoruje WebView2-lifecycle, ter definirano mostiček preko WebMessage (JSON), namesto poljubnega »ExecuteScript überall«. Cilj ni demo-koda, temveč gradnik, ki preživi v obstoječih odjemalcih.
Zakaj je WebView2 v FMX drugačen kot „Browser-Component drop“
WebView2 je API, blizu COM/WinRT, z asinhrono inicializacijo. FireMonkey abstraktira Windows-handlle, vendar za WebView2 na koncu potrebujete pravo parent-window (HWND) in nadzorovano posredovanje sprememb velikosti/fokusa. Hkrati dogodki ne tečejo vedno tam, kjer jih v FMX pričakujete. Če tu začnete »quick and dirty«, običajno dobite:
- občasne AV pri zapiranju Form (callbacks se sprožijo po Destroy)
- Navigation-Events iz napačnega konteksta niti
- nezanesljivo persistenco/težave s predpomnilnikom zaradi nejasne UserDataFolder-strategije
- ni prenosov ali »zastali« Download-Dialoge
- Debugging le po sreči namesto namensko nastavljene konfiguracije za oddaljeno odpravljanje napak
Protiukrep je jasen lifecycle: Create → InitializeAsync → Attach → Navigate → Detach/Dispose – in definirana meja med UI in browser-engine.
Izsek kode: WebView2Host für Delphi WebView2 FMX
Naslednja koda orisuje enkapsulirano host-klaso, ki (1) ustvari konfiguracijo WebView2-environment, (2) veže controller-objekt na HWND, (3) poveže Navigation- in Download-Events ter (4) ponudi JSON-baziran JS-Bridge preko WebMessageReceived. Koda je namenoma primerna za arhitekturo: enkapsulira COM-referenc, preprečuje callback-naslednike po Destroy in omogoča obratovalne možnosti, kot so »pro User« ali »pro Maschine«, z ločenimi UserDataFolder.
unit WebView2Host;
interface
uses
System.SysUtils, System.Classes, System.IOUtils, System.JSON,
Winapi.Windows, Winapi.ActiveX,
FMX.Types,
WebView2, WebView2_TLB; // odvisno od namestitve: WebView2.pas ali 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 je lahko manjka ali je 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;
// Odvezovanje dogodkov, preden se sprostijo COM-objekti
UnhookEvents;
FWebView := nil;
FController := nil;
FEnvironment := nil;
inherited;
end;
procedure TWebView2Host.EnsureNotDestroyed;
begin
if FDestroyed then
raise EInvalidOperation.Create('WebView2Host je že uničen.');
end;
function TWebView2Host.MakeUserDataFolder: string;
begin
if FUserDataFolder <> '' then
Exit(FUserDataFolder);
// V praksi: na aplikacijo in za uporabnika Windows, ne v imenik programa
Result := TPath.Combine(TPath.GetHomePath, 'AppDataLocalMyCompanyMyAppWebView2');
ForceDirectories(Result);
end;
procedure TWebView2Host.InitializeAsync;
var
UserData: string;
Opt: ICoreWebView2EnvironmentOptions;
begin
EnsureNotDestroyed;
UserData := MakeUserDataFolder;
// Možnosti: tu lahko dodate dodatne argumente brskalnika, npr. za oddaljeno razhroščevanje
Opt := TCoreWebView2EnvironmentOptions.Create;
// Asinhrono ustvarjanje okolja
OleCheck(CreateCoreWebView2EnvironmentWithOptions(
nil, PWideChar(UserData), Opt,
TCoreWebView2CreateCoreWebView2EnvironmentCompletedHandler.Create(
procedure (errorCode: HRESULT; const createdEnvironment: ICoreWebView2Environment)
begin
if FDestroyed then Exit;
OleCheck(errorCode);
FEnvironment := createdEnvironment;
// Povezava kontrolerja na nadrejeni 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;
// Inicialno narediti vidno
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));
// Opomba: za robustno odstranjevanje povezav dogodkov shranite tokene.
// V mnogih projektih je to dovolj, če je življenjska doba hosta vezana na formo.
end;
procedure TWebView2Host.UnhookEvents;
begin
// Robustna varianta: zapomnite si tokene in pokličite remove_*.
// Tukaj kot komentar, ker se nastavitve uvozne enote in upravljanje tokenov razlikujejo glede na 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));
// V praksi: rezultatna pot datoteke je sprva lahko prazna, odvisno od vira.
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);
// Izbirno: lastna uporabniška vmesnik za prenos in nato nastavimo 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 še ni inicializiran.');
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.
Namen des Ansatzes
- Zapiranje življenjskega cikla: FMX-obrazec pozna le „Initialize/Navigate/Resize“, ne COM-podrobnosti.
- Bridge s pogodbo: JSON-sporočila z
name, opcijskocid(Correlation-ID) inpayloadso lahko vzdrževana in testirana. - Operativno zanesljiva persistenca: kontroliran
UserDataFolderpreprečuje kolizije predpomnilnika, težave z dovoljenji in „deluje na razvijalčevem računalniku, ne v obratovanju“.
JS↔Delphi-Bridge: zakaj je WebMessage bolj stabilen kot ExecuteScript
WebView2 ponuja več poti komunikacije. V praksi je ExecuteScript privlačen, vendar slab za verzioniranje: potiskate nize v interpreter, brez jasnih kanalov za odgovore in brez robustnega preslikavanja napak. PostWebMessageAsString / WebMessageReceived je nasprotno definiran kanal.
Neobičajen primer, ki se pogosto pojavi v poslovnih okoljih: morate iz spletnega frontenda (npr. notranjega portala) sprožiti Delphi-delovni tok (tisk, dostop do naprav, integracija z legacy). Potem potrebujete:
- seznam dovoljenih imen sporočil
- Correlation-ID-je za asinhrone odgovore
- osrednjo točko za validacijo payloadov (npr. obvezna polja, omejitve velikosti)
V gostitelju je to mesto OnWebMessageReceived. Dejanska validacija spada v sloj nad njim (npr. Application-Service), da ohranite ločitev UI-/WebView2-tehnike in poslovne logike (klasična slojna arhitektura: UI → Application → Domain → Infrastruktur).
Prenosi in shranjevanje datotek: kaj v obratovanju pogosto preseneti
Prenosi v WebView2 potekajo preko ICoreWebView2DownloadOperation. Glede na vir je lahko ResultFilePath zgodaj prazen ali šele kasneje nastavljen. Poleg tega veliko podjetij ne želi, da končni uporabniki shranjujejo v nekontrolirane mape.
Preverjeni vzorci:
- Prestrezite DownloadStarting in z
args.put_Handled(1)naj UI prevzame nadzor (lastna pot, konvencija imen, karantenska mapa). - Omejitve velikosti datotek in preverjanje MIME-typov, da preprečite „po pomoti 4 GB log datoteke“.
- Auditing: zapišite metapodatke prenosa (URI, MIME, bajti) v vaše loge, ne vsebine.
Če imate regulirane procese (npr. odobritve, sledljivost), je obravnava prek dogodkov edino mesto, kjer lahko brskalniški svet integrirate v vaše operativne predpise.
Debugging: DevTools, Remote Debug Port in reproducirljiva stanja
Debugging WebView2 pogosto podleti, ker stanj ni mogoče reproducirati. Pomagata dve nastavitvi:
- Vklop/izklop DevTools preko
ICoreWebView2Settings(v kodi:SetDevToolsEnabled) – v releaseu pogosto izklopljeno, v primeru podpore ciljano vključeno. - Stabilni UserDataFolder: Če mora vaša podpora reproducirati napako, je definirana pot neprecenljiva. Mapo lahko varnostno kopirate/zipate (Pozor: varstvo podatkov/PII) in ciljano primerjate stanja.
Neobvezno (odvisno od wrapperja) lahko EnvironmentOptions opremite z dodatnimi argumenti brskalnika, npr. z Remote-Debug-Portom. To ima smisel, kadar morate analizirati aplikacijo na testnem sistemu brez lokalnih razvojnih orodij. Omejitve: v produkcijskih okoljih mora biti to jasno odobreno in dokumentirano, sicer ustvarite nepotrebno ranljivost.
Pasti v Delphi WebView2 FMX: COM, niti in življenjski cikel obrazca
1) Povratni klici po zaprtju
Asinhroni CompletedHandler lahko prispejo potem, ko je obrazec že zapiran. V odlomku prepreči FDestroyed dostop do sproščenih objektov. Bolj robustno je poleg tega:
- Shranjujte tokene za dogodke in v
Destroydosledno pokličiteremove_* - Dovolite
InitializeAsyncsamo enkrat (State-Machine: Created/Initializing/Ready/Disposed)
2) Kontekst niti
Čeprav mnogi handlerji prihajajo »UI‑nah«, se ne zanašajte, da lahko neposredno pišete v FMX‑kontrole. Če v OnWebMessage posodabljate UI, je TThread.Queue(nil, ...) varna možnost. Rad ločim odgovornosti: Host zbere dogodek, Application‑Service odloči, UI se posodablja izključno preko Queue.
3) DPI/Resize und FMX-Layouts
FMX računa v logičnih enotah, WebView2 pa pričakuje pixel‑Rects. V praksi potrebujete jasno mesto, kjer pretvorite Bounds FMX‑kontrol v prave piksle. Odlomek predpostavlja TRect; v vaši formi bi morali iz njega izpeljati WinAPI‑koordinate (npr. preko FMX.Platform.Win in Handle‑APIjev). Če se aplikacija skalira po DPI monitorja, testirajte prehod med monitorji: WebView2 je tu bolj občutljiv kot čiste FMX‑kontrole.
Kdaj se WebView2 v FMX izplača — in kdaj ne
WebView2 se izplača, kadar v razviti Delphi‑klientni aplikaciji ciljno uporabljate spletno tehnologijo: vdelani administrativni pogledi, OAuth/OIDC prijavni tokovi, HTML‑poročila, notranji portali ali kontrolirani »Micro‑Frontends«. Kot most za modernizacijo je praktičen, dokler jasno razmejite odgovornosti in most ne postane nekontroliran zadnji vhod za poslovno logiko.
Omejitve pristopa:
- Plattform: Vzorec je osredotočen na Windows. FMX je večplatformen, WebView2 pa tega ne podpira. Za macOS/iOS/Android potrebujete druge WebViewe ali plast abstrakcije.
- Security/Hardening: Ko se naložijo zunanje vsebine, morate strožje omejiti navigacijo, dovoljene domene in cilje prenosov. To spada v zahteve, ne v »kasneje«.
- Support: UserDataFolder in runtime‑odvisnosti (WebView2 Runtime) morajo biti del vašega koncepta obratovanja/razmestitve.
Zaključek
Delphi WebView2 FMX ni toliko UI‑gadget kot integracijska komponenta z lastnim življenjskim ciklom. Če inicializacijo, Eventing, UserDataFolder in JS‑Bridge strukturirano kapsulirate, bo WebView2 stabilen gradnik za digitalne poslovne rešitve: Web‑UI tam, kjer je smiselno, in Delphi‑logika tam, kjer pripada. Če pa brez nadzora sprožate skripte, poti prepuščate naključju in dogodkov ne razklenete, boste dobili točno tisto vrsto »sporadičnih v polju« napak, ki požira čas in ruši zaupanje.
Če želite WebView2 čisto integrirati v obstoječo Delphi‑aplikacijo ali tehnično ovrednotiti rob modernizacije, se pogovorite z nami:
V strokovnem okolju imata tudi Webview2 Firemonkey in Delphi Fmx Edge Browser pomembno vlogo, kadar morajo integracije, tokovi podatkov in nadaljnji razvoj tesno sodelovati.
Pogovorite se o projektu ali modernizacijskem načrtu z Net-Base.
Naslednji korak
Ko se tema spremeni v dejanski projekt, je treba arhitekturo, obstoječi sistem in obratovanje zgodaj obravnavati skupaj.
Ne podpiramo le pri posameznih vprašanjih, ampak tudi takrat, ko iz izrezkov izvorne kode, legacy-tem ali idej za portale nastane zanesljiv podjetniški projekt.
- Obstoječe stanje, ciljno stanje in tehnična tveganja se ocenjujejo skupaj.
- REST, dostop do podatkov, portali in uvedba niso prestavljeni kot poznejše posledice.
- Zgodaj prepoznate, katera pot je ekonomsko in obratovalno vzdržna.