Net-Base Časopis

14.06.2026

Delphi WebView2 u FMX: ispravno inicijalizirati, implementirati JS-Bridge, držati preuzimanja i debugiranje pod kontrolom

WebView2 u FireMonkey zvuči kao 'samo ugraditi preglednik', ali u praksi posrće pri inicijalizaciji, navigacijskim događajima, JS↔Delphi-Bridgeu, rukovanju preuzimanjima i debugiranju. Ovaj fragment izvornog koda prikazuje robustan obrazac s jasno definiranim odgovornostima...

14.06.2026

Od teme magazina do projektne prakse

Povezane stranice usluga i tehnologije za članak

Tko u postojećem poslovnom softveru iznenada želi „samo tako“ ugraditi moderne web-sadržaje, pri radu s WebView2 naići će na Windows. U Delphi WebView2 FMX osnovni problem rijetko je samo prikaz URL‑a, već čista integracija u FireMonkey sučelje (FMX), pouzdano inicijaliziranje (asinkrono i COM‑temeljeno), kao i zamke Edgea vezane uz UserDataFolder-direktorije, preuzimanja, debugiranje i robusnu JS↔Delphi‑komunikaciju.

Ovaj isječak izvornog koda prikazuje obrazac koji preferiram za održive aplikacije: enkapsulirani „Host“‑objekt koji kontrolira životni ciklus WebView2, te definirani most preko WebMessage (JSON), umjesto proizvoljnog „ExecuteScript svugdje“. Cilj nije demo‑kod, već komponenta koja preživi u etabliranim klijentima.

Warum WebView2 in FMX anders ist als „Browser-Component drop“

WebView2 je COM/WinRT‑bliska API s asinkronom inicijalizacijom. FireMonkey apstrahira Windows‑handleove, no za WebView2 ćete na kraju trebati pravo roditeljsko prozorsko okno (HWND) i kontrolirano prosljeđivanje promjena veličine/fokusa. Istovremeno se događaji ne izvršavaju uvijek tamo gdje biste ih očekivali u FMX‑u. Ako ovdje krenete „quick and dirty“, tipično ćete dobiti:

  • sporadične AVs pri zatvaranju forme (Callbacks stižu nakon Destroy)
  • navigacijski događaji iz pogrešnog konteksta dretve
  • nepouzdana persistencija/problemi s cacheom zbog nejasne UserDataFolder‑strategije
  • nema preuzimanja ili „zaglavljeni“ dijalozi preuzimanja
  • debugiranje samo preko sreće umjesto ciljane konfiguracije udaljenog debugiranja

Protivotrov je jasan lifecycle: Create → InitializeAsync → Attach → Navigate → Detach/Dispose – i definirana granica između UI‑ja i browser‑engine‑a.

Source‑Schnipsel: WebView2Host für Delphi WebView2 FMX

Slijedeći kod skicira enkapsuliranu host‑klasu koja (1) kreira konfiguraciju WebView2 okruženja, (2) veže controller‑objekt na HWND, (3) povezuje navigacijske i download‑evente i (4) nudi JSON‑baziranu JS‑bridge preko WebMessageReceived. Kod je namjerno „architekturfähig“: enkapsulira COM‑referencije, sprječava callback‑naknadnike nakon Destroy, i dopušta operativne granice poput odvojenih UserDataFoldera „po korisniku“ ili „po stroju“.

Delphi
unit WebView2Host;

interface

uses
  System.SysUtils, System.Classes, System.IOUtils, System.JSON,
  Winapi.Windows, Winapi.ActiveX,
  FMX.Types,
  WebView2, WebView2_TLB; // ovisno o postavkama: WebView2.pas ili 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 može nedostajati ili biti 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;

  // Odvezivanje događaja prije oslobađanja COM objekata
  UnhookEvents;

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

  inherited;
end;

procedure TWebView2Host.EnsureNotDestroyed;
begin
  if FDestroyed then
    raise EInvalidOperation.Create('WebView2Host je već uništen.');
end;

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

  // U praksi: po aplikaciji i po Windows korisniku, ne u direktoriju programa
  Result := TPath.Combine(TPath.GetHomePath, 'AppDataLocalMyCompanyMyAppWebView2');
  ForceDirectories(Result);
end;

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

  UserData := MakeUserDataFolder;

  // Opcije: ovdje mogu ići dodatni argumenti preglednika, npr. 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;

        // Povezivanje kontrolera s roditeljskim 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;

              // Učiniti inicijalno vidljivim
              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));

  // Napomena: Za robusno uklanjanje veza trebali biste spremiti tokene.
  // U mnogim projektima to je dovoljno ako host živi samo s Formom.
end;

procedure TWebView2Host.UnhookEvents;
begin
  // Robusna varijanta: zapamtite tokene i pozovite remove_*.
  // Ovdje kao komentar, jer se setup import-jedinice i upravljanje tokenima razlikuju ovisno o wrapperu.
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));

  // U praksi: ResultFileName inicijalno prazan, ovisno o izvoru.
  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);

    // Opcionalno: vlastiti UI za preuzimanje, tada postavite 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 još nije inicijaliziran.');

  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.

Svrha pristupa

  • Kapsuliranje životnog ciklusa: FMX-forma poznaje samo „Initialize/Navigate/Resize“, ne COM-detalje.
  • Bridge s ugovornim sučeljem: JSON-poruke s name, opcionalno cid (Correlation-ID) i payload su održive i moguće za testiranje.
  • Operativno sigurna perzistencija: kontrolirani UserDataFolder sprječava kolizije cache-a, probleme s dozvolama i situacije „radi na računalu developera, ne u proizvodnji“.

JS↔Delphi-Bridge: zašto je WebMessage stabilniji od ExecuteScript

WebView2 nudi više kanala komunikacije. U praksi je ExecuteScript primamljiv, ali teško ga je verzionirati: ubacujete stringove u interpreter bez jasnih kanala za odgovore i bez robusnog mapiranja pogrešaka. PostWebMessageAsString / WebMessageReceived je za razliku od toga definirani kanal.

Rubni slučaj koji se često pojavljuje u korporativnim okruženjima: morate iz web‑frontenda (npr. internog portala) pokrenuti Delphi-workflow (ispis, pristup uređajima, integracija sa legacy sustavima). Tada trebate:

  • bijelu listu imena poruka
  • Correlation-ID-ove za asinkrone odgovore
  • središnje mjesto koje validira payloadove (npr. obavezna polja, ograničenja veličine)

U hostu je to mjesto OnWebMessageReceived. Stvarna validacija treba biti u sloju iznad (npr. Application-Service), kako biste držali UI/WebView2-tehnologiju odvojeno od poslovne logike (klasična slojevita arhitektura: UI → Application → Domain → Infrastruktur).

Preuzimanja i pohrana datoteka: što često iznenadi u produkciji

Preuzimanja u WebView2 koriste ICoreWebView2DownloadOperation. Ovisno o izvoru, ResultFilePath može biti prazan na početku ili biti postavljen tek kasnije. Osim toga, mnoge tvrtke ne žele da krajnji korisnici spremaju u nekontrolirane mape.

Provjereni obrasci:

  • Presresti DownloadStarting i pozivom args.put_Handled(1) preuzeti rukovanje u UI-ju (vlastita putanja, konvencija imenovanja, karantenski direktorij).
  • Ograničenja veličine datoteka i provjere MIME‑tipa, kako biste izbjegli slučajno preuzimanje 4 GB log datoteke.
  • Auditing: zapisati metapodatke preuzimanja (URI, MIME, broj bajtova) u logove, ne sadržaj.

Ako imate regulirane procese (npr. odobrenja, mogućnost revizije), rukovanje putem događaja je jedino mjesto na kojem možete integrirati svijet preglednika u svoje operativne procedure.

Debugiranje: DevTools, Remote Debug Port i reproducibilna stanja

Debugiranje WebView2 često zapinje jer se stanja ne mogu reproducirati. Dva podešavanja pomažu:

  • Aktivacija/deaktivacija DevTools preko ICoreWebView2Settings (u kodu: SetDevToolsEnabled) – u releaseu često isključeno, u slučaju podrške ciljano uključeno.
  • Stabilan UserDataFolder: ako vaš support treba reproducirati grešku, definirana putanja vrijedi zlata. Možete direktorij sigurnosno kopirati/zipati (Pažnja: zaštita podataka/PII) i ciljano usporediti stanja.

Opcionalno (ovisno o wrapperu) možete EnvironmentOptions opremiti dodatnim argumentima za preglednik, npr. Remote-Debug-Port. To ima smisla kada morate analizirati aplikaciju na testnom sustavu bez lokalnih developerskih alata. Ograničenja: u produkcijskim okruženjima to mora biti uredno odobreno i dokumentirano, inače otvarate nepotreban napadni vektor.

Zamke u Delphi WebView2 FMX: COM, niti i životni ciklus forme

1) Povratni pozivi nakon zatvaranja

Asinkroni CompletedHandleri mogu stizati nakon što se forma već zatvara. U primjeru FDestroyed sprječava pristup oslobođenim objektima. Robusnije je dodatno:

  • Pohraniti tokene za događaje i u Destroy uredno pozvati remove_*
  • Dopustiti InitializeAsync samo jednom (stroj stanja: Created/Initializing/Ready/Disposed)

2) Kontekst niti

Mnogi handleri dolaze „blizu UI“, ali ne oslanjajte se na to da možete izravno pisati u FMX-kontrolama. Ako u OnWebMessage ažurirate UI, TThread.Queue(nil, ...) je sigurna varijanta. Volim razdvojiti odgovornosti: host prikuplja događaj, aplikacijski servis odlučuje, a UI se ažurira isključivo putem Queue.

3) DPI/Resize und FMX-Layouts

FMX računa u logičkim jedinicama, WebView2 očekuje pravokutnike u pikselima. U praksi trebate jasno mjesto na kojem prevodite Bounds FMX-kontrola u stvarne piksele. Primjer očekuje TRect; u vašoj formi trebali biste iz njega izvesti WinAPI-koordinate (npr. preko FMX.Platform.Win i Handle-API-ja). Ako se aplikacija skaluje prema DPI monitora, testirajte prelazak između monitora: WebView2 je ovdje osjetljiviji od čistih FMX-kontrola.

Kada se WebView2 u FMX isplati – a kada ne

WebView2 se isplati kad u zreloj Delphi-klijentskoj aplikaciji želite ciljano koristiti web-tehnologiju: ugrađeni admin-prikazi, OAuth/OIDC login-tokovi, HTML-izvještaji, interni portali ili kontrolirani „Micro-Frontends“. Također je praktičan kao most za modernizaciju, sve dok jasno razgraničite odgovornosti i ne dopustite da most postane nekontrolirana stražnja vrata za poslovnu logiku.

Ograničenja pristupa:

  • Platforma: Uzorak je Windows-centriran. FMX je multiplatforman, WebView2 nije. Za macOS/iOS/Android trebate druge WebView-e ili sloj apstrakcije.
  • Security/Hardening: Kad se učitavaju vanjski sadržaji, morate strože ograničiti navigaciju, dopuštene domene i ciljeve preuzimanja. To treba biti dio zahtjeva, ne „kasnije“.
  • Support: UserDataFolder i runtime-ovisnosti (WebView2 Runtime) moraju biti dio vašeg koncepta za operacije/rollout.

Zaključak

Delphi WebView2 FMX nije toliko UI-gadget koliko komponenta integracije s vlastitim životnim ciklusom. Ako inicijalizaciju, eventing, UserDataFolder i JS-Bridge strukturirano uokvirite, WebView2 će postati stabilan građevni blok za digitalna rješenja u poduzeću: Web-UI tamo gdje ima smisla, i Delphi-logika tamo gdje pripada. Ako pak nekontrolirano pokrećete skripte, ostavljate puteve na milost slučaja i ne razdvajate događaje, dobivate upravo onu vrstu „sporadičnih u polju“ pogrešaka koja troši vrijeme i narušava povjerenje.

Ako želite u postojećoj Delphi-aplikaciji uredno integrirati WebView2 ili tehnički procijeniti rub modernizacije, razgovarajte s nama:

U stručnom okruženju važnu ulogu igraju i Webview2 Firemonkey i Delphi Fmx Edge Browser, kad je potrebno da se integracije, tokovi podataka i daljnji razvoj uredno usklade.

Raspravite projekt ili plan modernizacije s Net-Base.

Sljedeći korak

Kad se tema pretvori u stvarni projekt, arhitektura, postojeći sustav i operativni rad trebaju se rano sagledati zajedno.

Podržavamo vas ne samo u pojedinačnim pitanjima, već i kada iz isječaka izvornog koda, naslijeđenih sustava ili ideja za portale treba nastati pouzdan poslovni projekt.

  • Postojeće stanje, ciljna slika i tehnički rizici procjenjuju se zajedno.
  • REST, pristup podacima, portali i Rollout neće biti odgođeni kao kasne posljedice.
  • Vidite rano koji je put ekonomski i operativno održiv.

Podijeli objavu

Izravno proslijedite ovu objavu

LinkedIn, X, XING, Facebook, WhatsApp i e-mail su odmah dostupni. Za Instagram pripremamo link i kratak tekst.

E-pošta

Instagram se otvara u novoj kartici. Link i kratki tekst se prethodno kopiraju u međuspremnik.