Net-Base Revistë

14.06.2026

Delphi WebView2 në FMX: inicializim i pastër, ndërtim i JS-Bridge, shkarkimet dhe debugimi nën kontroll

WebView2 në FireMonkey tingëllon si „thjesht të integrosh një shfletues“, por në praktikë dështon gjatë inicializimit, ngjarjeve të navigimit, urës JS↔Delphi, menaxhimit të shkarkimeve dhe debugimit. Ky fragment burimkodi tregon një model të qëndrueshëm me përgjegjësi të qarta...

14.06.2026

Nga tema e revistës në praktikën e projektit

Faqe shërbimi dhe teknike të përshtatshme për artikullin

Kur në një Business-Software ekzistuese dikush papritmas dëshiron të integrjë shpejt përmbajtje moderne web, zakonisht tek Windows përfundohet te WebView2. Në Delphi WebView2 FMX problemi themelor rrallë është thjesht shfaqja e një URL-je, por embeddimi i pastër në një ndërfaqe FireMonkey (FMX), inicializimi i besueshëm (asinkron dhe i bazuar në COM), si dhe kurthet e Edge rreth dosjeve të User-Data, shkarkimeve, debugging-ut dhe komunikimit të qëndrueshëm JS↔Delphi.

Ky copëz source tregon një model që preferoj për aplikacione të mirëmbajtshme: një objekt i kapsulluar „Host“ që kontrollon lifecycle-in e WebView2, dhe një bridge të përcaktuar mbi WebMessage (JSON), në vend të „ExecuteScript überall“ të paqëllimshëm. Qëllimi nuk është kod demo, por një bllok ndërtimi që mbijeton në klientë të rritur.

Pse WebView2 në FMX është ndryshe nga „Browser-Component drop“

WebView2 është një API e afërt me COM/WinRT me inicializim asinkron. FireMonkey abstrahon Windows-Handles, megjithatë për WebView2 në fund ju duhët një Parent-Window të vërtetë (HWND) dhe një përcjellje e kontrolluar e resize-/focus. Njëkohësisht, ngjarjet nuk gjithmonë përsëriten aty ku priten në FMX. Nëse nisni „quick and dirty“ këtu, tipikisht do të merrni:

  • AV të sporadike gjatë mbylljes së formës (callback-et mbërrijnë pas Destroy)
  • ngjarje navigimi që vijnë nga një kontekst i gabuar i thread-it
  • probleme të paqëndrueshme me persistencën/cache për shkak të strategjisë së paqartë për UserDataFolder
  • asnjë shkarkim ose dialogë shkarkimi të „bllokuar“
  • debugging vetëm me fat në vend të një konfigurimi të synuar për Remote-Debug

Kundërmasa është një lifecycle i qartë: Create → InitializeAsync → Attach → Navigate → Detach/Dispose – dhe një kufi i përcaktuar midis UI dhe Browser-Engine.

Source-Schnipsel: WebView2Host für Delphi WebView2 FMX

Kodi vijues skicon një klasë Host të kapsulluar, që (1) krijon një konfigurim të WebView2-Environment, (2) lidh objektin Controller me një HWND, (3) lidh ngjarjet e Navigation dhe Download, dhe (4) ofron një JS-Bridge bazuar në JSON përmes WebMessageReceived. Kodi është qëllimisht „i përshtatshëm për arkitekturë“: kapsullon referencat COM, parandalon callback-et që mbërrijnë pas Destroy, dhe lejon skenare operimi si „pro User“ ose „pro Maschine“ me UserDataFolder të ndara.

Delphi
unit WebView2Host;

interface

uses
  System.SysUtils, System.Classes, System.IOUtils, System.JSON,
  Winapi.Windows, Winapi.ActiveX,
  FMX.Types,
  WebView2, WebView2_TLB; // në varësi të konfigurimit: WebView2.pas ose 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 mund të mungojë ose të jetë 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;

  // Heqni lidhjet e eventeve para lëshimit të objekteve COM
  UnhookEvents;

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

  inherited;
end;

procedure TWebView2Host.EnsureNotDestroyed;
begin
  if FDestroyed then
    raise EInvalidOperation.Create('WebView2Host është tashmë i shkatërruar.');
end;

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

  // Praktikë: për çdo app + për çdo përdorues Windows, jo në dosjen e programit
  Result := TPath.Combine(TPath.GetHomePath, 'AppDataLocalMyCompanyMyAppWebView2');
  ForceDirectories(Result);
end;

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

  UserData := MakeUserDataFolder;

  // Opsione: këtu mund të shtoni argumente shtesë për shfletuesin, p.sh. 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;

        // Lidh controller me HWND të prindit
        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;

              // Initial sichtbar machen
              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));

  // Shënim: Për heqje më të qëndrueshme, ruani token-et.
  // Në shumë projekte kjo është e mjaftueshme nëse host-i jeton vetëm me formën.
end;

procedure TWebView2Host.UnhookEvents;
begin
  // Varianti i qëndrueshëm: mbani token-et dhe thërrisni remove_*.
  // Këtu si koment, sepse setup-i i import-unit dhe menaxhimi i token-eve ndryshojnë sipas wrapper-it.
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ë praktikë: ResultFileName shpeshherë është bosh në fillim, në varësi të burimit.
  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);

    // Optional: eigene Download-UI, dann Handled setzen
    // 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 nuk është ende i inicializuar.');

  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.

Qëllimi i qasjes

  • Kapsulimi i ciklit të jetës: Forma FMX njeh vetëm „Initialize/Navigate/Resize“, jo detajet e COM.
  • Ura me kontratë: Mesazhe JSON me name, opsional cid (Correlation-ID) dhe payload janë të mirëmbajtshme dhe të testueshme.
  • Persistencë e sigurt për operim: një kontroll i UserDataFolder parandalon kolizionet e cache, problemet me të drejtat dhe „funksionon në kompjuterin e zhvilluesit, jo në mjedisin e prodhimit“.

JS↔Delphi-Bridge: pse WebMessage është më i qëndrueshëm se ExecuteScript

WebView2 ofron disa rrugë komunikimi. Në praktikë ExecuteScript është joshës, por i vështirë për versionim: ju dërgoni string-e në një interpretues, pa kanale të qarta përgjigjeje dhe pa një mappim të fortë të gabimeve. PostWebMessageAsString / WebMessageReceived është përkundrazi një kanal i definuar.

Skenar që shpesh paraqitet në mjedise ndërmarrjesh: duhet të nisni nga një Web-Frontend (p.sh. portal i brendshëm) një Delphi-workflow (shtypje, akses pajisjesh, integrim me legacy). Atëherë ju nevojiten:

  • një listë të lejuar të emrave të mesazheve
  • Correlation-IDs për përgjigje asincrone
  • një vend qendror që validon payloads (p.sh. fusha të detyrueshme, kufizime madhësie)

Në host kjo është pika OnWebMessageReceived. Validimi i vërtetë i takon një shtrese më lart (p.sh. Application-Service), në mënyrë që të mbani teknikën UI/WebView2 dhe logjikën e biznesit të ndara (arkitekturë klasike e shtresave: UI → Application → Domain → Infrastruktur).

Downloads und Dateiablage: was im Betrieb oft überrascht

Shkarkimet në WebView2 menaxhohen përmes ICoreWebView2DownloadOperation. Sipas burimit ResultFilePath mund të jetë bosh herët ose të vendoset më vonë. Për më tepër, shumë kompani nuk duan që përdoruesit final të ruajnë në dosje të pakontrolluara.

Modele të provuara:

  • Kapja e DownloadStarting dhe marrja e kontrollit të UI me args.put_Handled(1) (rrugë e vet, konvencion emërtimi, dosje karantine).
  • Kufijtë e madhësisë së skedarëve dhe kontrollet e llojit MIME, për të shmangur „pa dashje një logfile 4 GB“.
  • Auditim: shkruani metadatat e shkarkimit (URI, MIME, bytes) në logimin tuaj, jo përmbajtjen.

Nëse keni procese të rregulluara (p.sh. miratime, gjurmueshmëri), trajtimi përmes ngjarjeve është pika e vetme ku mund të integroni botën e shfletuesit në rregullat tuaja të operimit.

Debugging: DevTools, Remote Debug Port und reproduzierbare Zustände

Debugging i WebView2 shpesh dështojnë sepse gjendjet nuk janë të riprodhueshme. Dy pika rregulluese ndihmojnë:

  • Aktivimi/deaktivimi i DevTools përmes ICoreWebView2Settings (në kod: SetDevToolsEnabled) – në release shpesh i fikur, në rast supporti aktivizohet në mënyrë të synuar.
  • UserDataFolder i qëndrueshëm: Nëse supporti juaj duhet të riprodhojë një gabim, një rrugë e përcaktuar ka vlerë të madhe. Mund të siguroni/zip-oni dosjen (Kujdes: Datenschutz/PII) dhe të krahasoni gjendjet e synuara.

Opsionalisht (sipas wrapper) mund të konfiguroni EnvironmentOptions me argumente shtesë të shfletuesit, p.sh. një Remote-Debug-Port. Kjo është e dobishme kur duhet të analizoni një aplikacion në një sistem testimi pa mjetet lokale të zhvilluesit. Kufizimet: Në mjedise prodhimi kjo duhet të aktivizohet dhe dokumentohet rregullisht, përndryshe krijoni një sipërfaqe sulmi të panevojshme.

Capçanat në Delphi WebView2 FMX: COM, Threads dhe Form-Lifecycle

1) Callbacks pas mbylljes

Handler-ët asinkronë CompletedHandler mund të arrijnë pasi forma tashmë është duke u mbyllur. Në shembullin e kodit FDestroyed parandalon qasjen në objekte të liruar. Më i qëndrueshëm është gjithashtu:

  • Ruani token-at për ngjarjet dhe në Destroy thërrisni në mënyrë të pastër remove_*
  • Lejoni InitializeAsync vetëm një herë (sistemi i gjendjeve: Created/Initializing/Ready/Disposed)

2) Konteksti i thread-it

Shumë handler dalin „afër UI-së“, por mos u mbështetni që mund të shkruani direkt në komponentët FMX. Nëse përditësoni UI në OnWebMessage, TThread.Queue(nil, ...) është varianti i sigurt. Unë zakonisht i ndaj përgjegjësitë: Host-i mbledh ngjarjen, Application-Service vendos, UI përditësohet ekskluzivisht përmes Queue.

3) DPI/Resize dhe FMX-Layouts

FMX llogarit në njësi logjike, WebView2 pret Pixel-Rects. Në praktikë ju duhet një vend të qartë ku përktheni bounds e FMX-Controls në piksela të vërtetë. Shembulli pranon një TRect; në formen tuaj duhet të nxirrni prej tij koordinatat WinAPI (p.sh. përmes FMX.Platform.Win dhe Handle-API-ve). Nëse aplikacioni shkallëzohet sipas DPI të monitorit, testoni kalimin midis monitorëve: WebView2 është këtu më i ndjeshëm se komponentët e pastër FMX.

Kur ia vlen WebView2 në FMX — dhe kur jo

WebView2 ia vlen kur dëshironi të përdorni teknologji web në mënyrë të synuar brenda një aplikacioni klient të rritur Delphi: pamje admin të të integruara, flukset e hyrjes OAuth/OIDC, raporte HTML, porta të brendshme ose Micro-Frontends të kontrolluara. Gjithashtu si urë modernizimi është praktik, për sa kohë ndani qartë përgjegjësitë dhe ura (bridge) nuk bëhet një derë e pasme e pakontrolluar për logjikën e biznesit.

Kufizimet e qasjes:

  • Platforma: Modeli është i përqendruar te Windows. FMX është multiplatformë, WebView2 nuk është. Për macOS/iOS/Android ju duhen WebView të tjera ose një shtresë abstraksioni.
  • Security/Hardening: Sapo të ngarkohen përmbajtje të jashtme, duhet të kufizoni më fort navigimin, domain-et e lejuara dhe destinacionet e shkarkimit. Kjo duhet të jetë pjesë e kërkesave, jo „më vonë“.
  • Support: UserDataFolder dhe varësitë e runtime (WebView2 Runtime) duhet të jenë pjesë e konceptit tuaj të operimit/rollout.

Përfundim

Delphi WebView2 FMX është më pak një gadget UI dhe më shumë një komponent integrimi me lifecycle të vetin. Nëse kapsuloni në mënyrë të strukturuar inicializimin, eventing-un, UserDataFolder dhe JS-Bridge, WebView2 do të bëhet një bllok i qëndrueshëm për zgjidhjet dixhitale të ndërmarrjeve: Web-UI aty ku ka kuptim, dhe logjika Delphi aty ku i takon. Përndryshe, nëse ekzekutoni skripte pa kontroll, lini rrugët rastësisht dhe nuk shkëputni eventet, do të përballeni me llojin e gabimeve „sporadike në terren“ që konsumojnë kohë dhe hanë besueshmërinë.

Nëse dëshironi të integroni WebView2 në mënyrë të pastër në një aplikacion ekzistues Delphi ose të vlerësoni teknikisht një kufi modernizimi, flisni me ne:

Në kontekstin profesional edhe Webview2 Firemonkey dhe Delphi Fmx Edge Browser luajnë një rol të rëndësishëm, kur integrimet, rrjedhat e të dhënave dhe zhvillimi i mëtejshëm duhet të punojnë së bashku në mënyrë të pastër.

Diskutoni projektin ose nismën e modernizimit me Net-Base.

Hapi tjetër

Kur nga një temë bëhet një projekt real, arkitektura, sistemi ekzistues dhe operimi duhet të merren në konsideratë së bashku që në fazat e hershme.

Ne nuk mbështesim vetëm në çështje të veçanta, por edhe kur nga fragmente të kodit burimor, temat legacy ose idetë për portale duhet të zhvillohen në një projekt korporativ të qëndrueshëm.

  • Gjendja ekzistuese, imazhi i synuar dhe rreziqet teknike vlerësohen së bashku.
  • REST, akses në të dhëna, portalet dhe Rollout nuk shtyhen si pasoja të mëvonshme.
  • Ju e shihni herët se cila rrugë është e qëndrueshme ekonomikisht dhe operativisht.

Ndaje postimin

Shpërndaj këtë postim drejtpërdrejt

LinkedIn, X, XING, Facebook, WhatsApp dhe E‑Mail janë menjëherë të disponueshme. Për Instagram po përgatitim menjëherë lidhjen dhe tekstin e shkurtër.

Postë elektronike

Instagram hapet në një skedë të re. Linku dhe teksti i shkurtër kopjohen më parë në memorjen e kopjimit.