Net-Base Revistă

14.06.2026

Delphi WebView2 în FMX: inițializare curată, construire punte JS, descărcări și depanare sub control

WebView2 în FireMonkey pare „doar încorporare de browser”, dar în practică eșuează la inițializare, la evenimentele de navigare, la puntea JS↔Delphi, la gestionarea descărcărilor și la depănare. Acest fragment de cod sursă arată un model robust cu responsabilități clar delimitate...

14.06.2026

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.

Delphi
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țional cid (Correlation-ID) și payload sunt ușor de întreținut și testabile.
  • Persistență robustă pentru operare: un controlat UserDataFolder previne 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 Destroy apelați curat remove_*
  • Permiteți InitializeAsync o 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.

Partajează postarea

Distribuiți această postare direct

LinkedIn, X, XING, Facebook, WhatsApp și e-mail sunt disponibile imediat. Pentru Instagram pregătim linkul și textul scurt imediat.

E-mail

Instagram se deschide într-o filă nouă. Linkul și textul scurt se copiază în prealabil în clipboard.