Net-Base Revija

14.06.2026

Delphi WebView2 v FMX: pravilna inicializacija, gradnja JS-Bridge, obvladovanje prenosov in razhroščevanja

WebView2 v FireMonkey zveni kot „preprosto vgraditi brskalnik“, a v praksi odpove pri inicializaciji, dogodkih navigacije, JS↔Delphi-Bridge, upravljanju prenosov in odpravljanju napak. Ta izsek izvorne kode prikazuje robusten vzorec z jasnimi odgovornostmi...

14.06.2026

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.

Delphi
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, opcijsko cid (Correlation-ID) in payload so lahko vzdrževana in testirana.
  • Operativno zanesljiva persistenca: kontroliran UserDataFolder prepreč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 Destroy dosledno pokličite remove_*
  • Dovolite InitializeAsync samo 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.

Deli objavo

Deli ta prispevek neposredno

LinkedIn, X, XING, Facebook, WhatsApp in e-pošta so takoj na voljo. Za Instagram bomo neposredno pripravili povezavo in kratek opis.

E-pošta

Instagram se odpre v novem zavihku. Povezava in kratek opis se pred tem kopirata v odložišče.