De la tema din revistă la practica în proiecte
Pagini relevante de servicii și pagini tehnice pentru articol
Cine, într-o aplicație business existentă, dorește „pe repede înainte” să încorporeze conținut web modern, se lovește pe Windows de WebView2. În Delphi WebView2 FMX problema de bază rar ține de afișarea unei URL, ci de integrarea curată într-o interfață FireMonkey (FMX), de inițializarea fiabilă (asincronă și bazată pe COM), precum și de capcanele Edge legate de directoarele User-Data, descărcări, depanare și de o comunicare JS↔Delphi robustă.
Acest fragment de cod arată un model pe care îl prefer pentru aplicații ușor de întreținut: un obiect „Host” încapsulat care controlează ciclul de viață al WebView2, precum și o punte definită prin WebMessage (JSON), în locul execuțiilor „ExecuteScript” oriunde. Scopul nu este cod demonstrativ, ci o componentă care supraviețuiește în clienți existenți.
De ce WebView2 în FMX este diferit față de „Browser-Component drop”
WebView2 este o API apropiată COM/WinRT cu inițializare asincronă. FireMonkey abstrage handle-urile Windows, totuși, pentru WebView2 veți avea în final nevoie de o fereastră părinte reală (HWND) și de o redirecționare controlată a redimensionării/focalizării. În același timp, evenimentele nu rulează întotdeauna acolo unde v-ați aștepta în FMX. Dacă porniți aici „quick and dirty”, veți obține de obicei:
- AV-uri sporadice la închiderea formularului (callback-urile sosesc după Destroy)
- evenimente de navigare care vin dintr-un context de thread greșit
- probleme nesigure de persistență/cache din cauza unei strategii neclare pentru UserDataFolder
- fără descărcări sau dialoguri de descărcare „blocate”
- depurare doar la noroc în loc de o configurare de Remote-Debug țintită
Remediul este un ciclu de viață clar: Create → InitializeAsync → Attach → Navigate → Detach/Dispose – și o frontieră definită între UI și motorul browserului.
Source-Schnipsel: WebView2Host für Delphi WebView2 FMX
Codul următor schițează o clasă host încapsulată, care (1) creează o configurație a WebView2-Environment, (2) leagă obiectul Controller de un HWND, (3) conectează evenimentele de navigare și descărcare și (4) oferă o JS-Bridge bazată pe JSON prin WebMessageReceived. Codul este intenționat „potrivit din perspectiva arhitecturii”: incapsulează referințele COM, previne callback-urile reziduale după Destroy și permite scenarii de operare precum „pro User” sau „pro Maschine” cu UserDataFolder separate.
unit WebView2Host;
interface
uses
System.SysUtils, System.Classes, System.IOUtils, System.JSON,
Winapi.Windows, Winapi.ActiveX,
FMX.Types,
WebView2, WebView2_TLB; // în funcție de configurare: WebView2.pas sau 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 poate lipsi sau să fie 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;
// Deconectați evenimentele înainte de a elibera obiectele COM
UnhookEvents;
FWebView := nil;
FController := nil;
FEnvironment := nil;
inherited;
end;
procedure TWebView2Host.EnsureNotDestroyed;
begin
if FDestroyed then
raise EInvalidOperation.Create('WebView2Host a fost deja distrus.');
end;
function TWebView2Host.MakeUserDataFolder: string;
begin
if FUserDataFolder <> '' then
Exit(FUserDataFolder);
// Practică: per aplicație + per utilizator Windows, nu în directorul programului
Result := TPath.Combine(TPath.GetHomePath, 'AppDataLocalMyCompanyMyAppWebView2');
ForceDirectories(Result);
end;
procedure TWebView2Host.InitializeAsync;
var
UserData: string;
Opt: ICoreWebView2EnvironmentOptions;
begin
EnsureNotDestroyed;
UserData := MakeUserDataFolder;
// Opțiuni: aici pot fi adăugate argumente suplimentare pentru browser, ex. 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;
// Atașați controllerul la HWND părinte
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;
// Faceți-l vizibil inițial
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));
// Atenție: pentru deconectare robustă ar trebui să păstrați token-urile.
// În multe proiecte acesta este suficient dacă durata de viață a hostului este aceeași cu cea a formularului.
end;
procedure TWebView2Host.UnhookEvents;
begin
// Variantă robustă: rețineți token-urile și apelați remove_*.
// Lăsat ca comentariu aici, deoarece setup-ul unității de import și gestionarea token-urilor variază în funcție de 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));
// În practică: ResultFileName inițial gol, în funcție de sursă.
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);
// Opțional: UI de descărcare proprie, apoi setați 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 nu a fost încă inițializat.');
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.
Scopul abordării
- Încapsularea ciclului de viață: Formularul FMX cunoaște doar „Initialize/Navigate/Resize“, nu detalii COM.
- Punte cu contract: Mesaje JSON cu
name, opționalcid(Correlation-ID) șipayloadsunt ușor de întreținut și testabile. - Persistență robustă pentru operare: un controlat
UserDataFolderprevine coliziunile de cache, problemele de permisiuni și „funcționează pe mașina dezvoltatorului, nu în producție“.
JS↔Delphi-Bridge: de ce WebMessage este mai stabil decât ExecuteScript
WebView2 oferă mai multe căi de comunicare. În practică, ExecuteScript este tentant, dar dificil de versionat: trimiteți stringuri într-un interpret fără canale clare de răspuns și fără un mapare robustă a erorilor. PostWebMessageAsString / WebMessageReceived este în schimb un canal definit.
Un caz particular, frecvent în mediile enterprise: trebuie să porniți un workflow Delphi dintr-un frontend web (de ex. portal intern) (tipic: imprimare, acces dispozitive, integrare legacy). Atunci aveți nevoie de:
- o listă albă (whitelist) de nume de mesaj
- Correlation-IDs pentru răspunsuri asincrone
- un punct central care validează payload-urile (de ex. câmpuri obligatorii, limite de dimensiune)
În host acesta este punctul OnWebMessageReceived. Validarea efectivă aparține unei straturi superioare (de ex. serviciu de aplicație), astfel încât să mențineți separat tehnica UI/WebView2 și logica de business (arhitectură clasică pe straturi: UI → Aplicație → Domeniu → Infrastructură).
Descărcări și stocare de fișiere: ce surprinde frecvent în operare
Descărcările în WebView2 rulează prin ICoreWebView2DownloadOperation. În funcție de sursă, ResultFilePath poate fi gol de la început sau setat doar ulterior. În plus, multe companii nu doresc ca utilizatorii finali să salveze în foldere necontrolate.
Practici dovedite:
- Interceptați DownloadStarting și preluați UI prin
args.put_Handled(1)(cale proprie, convenție de nume, folder de carantină). - Limite de dimensiune a fișierelor și verificări de tip MIME, pentru a evita „din greșeală un fișier de jurnal de 4 GB“.
- Auditare: scrieți metadatele descărcării (URI, MIME, octeți) în log, nu conținutul.
Dacă aveți procese reglementate (de ex. aprobări, trasabilitate), manipularea prin evenimente este singurul loc în care puteți integra lumea browserului în regulile dvs. de operare.
Depanare: DevTools, port de debug la distanță și stări reproductibile
Depanarea WebView2 eșuează adesea pentru că stările nu sunt reproductibile. Două reglaje ajută:
- Activați/dezactivați DevTools prin
ICoreWebView2Settings(în cod:SetDevToolsEnabled) – de obicei dezactivate în release, activate punctual în caz de suport. - UserDataFolder stabil: dacă suportul trebuie să reproducă un bug, o cale definită valorează mult. Puteți arhiva/zipa folderul (Atenție: protecția datelor/PII) și compara stările în mod țintit.
Opțional (în funcție de wrapper) puteți furniza EnvironmentOptions cu argumente suplimentare pentru browser, de ex. un port de debug la distanță. Are sens când trebuie să analizați o aplicație pe un sistem de test fără uneltele locale ale dezvoltatorului. Limite: în medii productive trebuie activat și documentat clar, altfel creați o suprafață de atac inutilă.
Capcane în Delphi WebView2 FMX: COM, fire de execuție și ciclul de viață al formularului
1) Callback-uri după închiderea formularului
Manevrele asincrone CompletedHandler pot ajunge după ce formularul s-a închis deja. În snippet, FDestroyed previne accesul la obiecte eliberate. Mai robust este, în plus:
- Stocați tokenii pentru evenimente și în
Destroyapelați curatremove_* - Permiteți
InitializeAsynco singură dată (State-Machine: Created/Initializing/Ready/Disposed)
2) Thread-Kontext
Mulți handleri vin „aproape de UI”, dar nu vă bazați pe posibilitatea de a scrie direct în controalele FMX. Dacă actualizați UI în OnWebMessage, TThread.Queue(nil, ...) este varianta sigură. Îmi place să separ: host-ul colectează evenimentul, serviciul aplicației decide, UI este actualizată exclusiv prin Queue.
3) DPI/Resize und FMX-Layouts
FMX lucrează în unități logice, WebView2 așteaptă Pixel-Rects. În practică aveți nevoie de un punct clar în care traduceți bound-urile din controalele FMX în pixeli reali. Snippet-ul primește un TRect; în formularul dvs. ar trebui să derivați din acesta coordonatele WinAPI (de ex. prin FMX.Platform.Win și API-urile Handle). Dacă aplicația se scalează în funcție de DPI-ul monitorului, testați comutarea între monitoare: WebView2 este aici mai sensibil decât controalele FMX simple.
Când merită WebView2 în FMX — și când nu
WebView2 merită dacă doriți să utilizați tehnologie web în mod țintit într-o aplicație client Delphi existentă: vizualizări admin încorporate, fluxuri de autentificare OAuth/OIDC, rapoarte HTML, portaluri interne sau „micro-frontends” controlate. Ca punte de modernizare este, de asemenea, practic, atâta timp cât separați clar responsabilitățile și nu transformați bridge-ul într-o ușă din spate necontrolată pentru logica de business.
Limitele abordării:
- Platformă: Modelul este centrat pe Windows. FMX este multiplatformă, WebView2 nu este. Pentru macOS/iOS/Android aveți nevoie de alte WebView-uri sau de un strat de abstracție.
- Securitate/Hardening: De îndată ce se încarcă conținut extern, trebuie să restricționați mai strict navigația, domeniile permise și țintele de download. Asta trebuie inclus în cerințe, nu „mai târziu”.
- Suport: UserDataFolder și dependențele runtime (WebView2 Runtime) trebuie să facă parte din conceptul dvs. de operare/rollout.
Concluzie
Delphi WebView2 FMX este mai puțin un gadget UI și mai degrabă o componentă de integrare cu propriul lifecycle. Dacă încapsulați structurat inițializarea, eventing-ul, UserDataFolder și JS-Bridge, WebView2 devine un element stabil pentru soluții enterprise digitale: Web-UI acolo unde are sens, și logica Delphi acolo unde îi este locul. Dacă, în schimb, rulați scripturi necontrolat, lăsați căile la voia întâmplării și nu decuplați evenimentele, veți obține exact genul de erori „sporadice în teren” care consumă timp și subminează încrederea.
Dacă doriți să integrați curat WebView2 într-o aplicație Delphi existentă sau să evaluați tehnic o margine de modernizare, discutați cu noi:
În contextul profesional, Webview2 Firemonkey și Delphi Fmx Edge Browser joacă, de asemenea, un rol important atunci când integrările, fluxurile de date și dezvoltarea ulterioară trebuie să funcționeze coerent.
Discutați un proiect sau un plan de modernizare cu Net-Base.
Următorul pas
Când o temă devine un proiect real, arhitectura, infrastructura existentă și operarea trebuie analizate împreună de la început.
Nu oferim sprijin doar pentru întrebări punctuale, ci și atunci când fragmente de cod sursă, probleme legacy sau idei de portal trebuie transformate într-un proiect robust la nivel de companie.
- Situația curentă, starea țintă și riscurile tehnice sunt evaluate împreună.
- REST, accesul la date, portalurile și Rollout nu sunt amânate ca consecințe ulterioare.
- Veți vedea din timp ce cale este viabilă din punct de vedere economic și operațional.