Net-Base Lehti

14.06.2026

Delphi WebView2 in FMX: alusta oikein, rakenna JS-silta, lataukset ja virheenkorjaus hallinnassa

WebView2 FireMonkeyssa kuulostaa "yksinkertaisesti selaimen upottamiselta", mutta käytännössä ongelmia esiintyy initialisoinnissa, navigaatiotapahtumien käsittelyssä, JS↔Delphi-sillan toteutuksessa, latausten hallinnassa ja virheenkorjauksessa. Tämä lähdekoodipätkä esittelee vankan mallin, jossa vastuut on yksiselitteisesti määritelty...

14.06.2026

Lehden aiheesta projektikäytäntöön

Artikkeliin liittyvät palvelu- ja tekniikkasivut

Jos olemassa olevaan yritysohjelmistoon halutaan äkillisesti upottaa moderneja verkkosisältöjä, päädytään Windows kohdassa WebView2:een. In Delphi WebView2 FMX WebView2 FMX perusongelma ei yleensä ole URL:n näyttäminen, vaan siisti upotus FireMonkey-käyttöliittymään (FMX), luotettava alustaminen (asynkroninen ja COM-pohjainen) sekä Edgeen liittyvät sudenkuopat User-Data-hakemistojen, latausten, virheenkorjauksen ja robustin JS↔Delphi-viestinnän osalta.

Tämä lähdekoodikatkelma esittää mallin, jota suosin ylläpidettävissä sovelluksissa: kapseloitu „Host“-objekti, joka kontrolloi WebView2-elinkaarta, sekä määritelty silta WebMessage-viestien (JSON) yli sen sijaan, että käytettäisiin satunnaisia „ExecuteScript“-kutsuja joka paikassa. Tavoitteena ei ole demonstratiokoodi, vaan rakennuspalikka, joka säilyy käytössä kasvaneissa asiakasohjelmissa.

Miksi WebView2 FMX:ssä eroaa „Browser-Component drop“ -menetelmästä

WebView2 on COM/WinRT-lähellä oleva rajapinta, jolla on asynkroninen alustaminen. FireMonkey abstrahoi Windows-käsittimiä, mutta WebView2 vaatii lopulta todellisen parent-windowin (HWND) sekä kontrolloidun koko- ja fokusohjauksen edelleenohjauksen. Samanaikaisesti tapahtumat eivät aina juokse siellä, missä niitä FMX:ssä odotetaan. Jos lähdetään „quick and dirty“ -otteella, tyypillisesti kohtaatte:

  • satunnaisia AV-virheitä lomakkeen sulkemisen yhteydessä (callbacks saapuvat Destroyin jälkeen)
  • navigointitapahtumia väärästä säiekontekstista
  • epäluotettavaa pysyvyyttä/välimuistiongelmia epäselvän User-Data-hakemistostrategian vuoksi
  • ei latauksia tai „jumiutuneita“ latausdialogeja
  • virheenkorjaus vain onnen varassa sen sijaan, että olisi kohdennettu etädebug-konfiguraatio

Vasta-aineena on selkeä elinkaari: Create → InitializeAsync → Attach → Navigate → Detach/Dispose – ja määritelty raja käyttöliittymän ja selainmoottorin välillä.

Lähdekoodikatkelma: WebView2Host Delphi WebView2 FMX:lle

Seuraava koodi hahmottaa kapseloidun Host-luokan, joka (1) luo WebView2-ympäristökonfiguraation, (2) sitoo Controller-objektin HWND:iin, (3) johdottaa navigointi- ja lataustapahtumat ja (4) tarjoaa JSON-pohjaisen JS-sillan WebMessageReceived-kautta. Koodi on tietoisesti „arkkitehtuurivalmis“: se kapseloi COM-viitteet, estää callback-jäämät Destroyin jälkeen ja sallii käyttöön liittyvät reunatilat, kuten erilliset UserDataFolderit käyttäjäkohtaisesti tai konekohtaisesti.

Delphi
unit WebView2Host;

interface

uses
  System.SysUtils, System.Classes, System.IOUtils, System.JSON,
  Winapi.Windows, Winapi.ActiveX,
  FMX.Types,
  WebView2, WebView2_TLB; // asennuksesta riippuen: WebView2.pas tai 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 voi puuttua tai olla 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;

  // Irrota tapahtumakäsittelijät ennen kuin COM-oliot vapautetaan
  UnhookEvents;

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

  inherited;
end;

procedure TWebView2Host.EnsureNotDestroyed;
begin
  if FDestroyed then
    raise EInvalidOperation.Create('WebView2Host on jo tuhottu.');
end;

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

  // Käytännössä: sovelluskohtainen + pro Windows-käyttäjäkohtainen, ei ohjelmakansioon
  Result := TPath.Combine(TPath.GetHomePath, 'AppDataLocalMyCompanyMyAppWebView2');
  ForceDirectories(Result);
end;

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

  UserData := MakeUserDataFolder;

  // Asetukset: tänne voi lisätä lisäargumentteja selaimelle, esim. etävianmääritys
  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;

        // Sitouta kontrolleri Parent HWND:iin
        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;

              // Aseta aluksi näkyväksi
              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));

  // Huom: Jos haluat irrottaa tapahtumakäsittelijät luotettavasti, tallenna tokenit.
  // Monissa projekteissa tämä riittää, kun host elää lomakkeen kanssa.
end;

procedure TWebView2Host.UnhookEvents;
begin
  // Robustiversio: tallenna tokenit ja kutsu vastaavia remove_*-metodeja.
  // Tässä kommenttina, koska import-unitin asetus ja tokenien hallinta vaihtelee wrapperin mukaan.
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));

  // Käytännössä: ResultFileName on aluksi tyhjä, riippuen lähteestä.
  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);

    // Valinnainen: oma lataus-UI, jolloin aseta 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 ei ole vielä alustettu.');

  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.

Lähestymistavan tarkoitus

  • Lifecycle-kapselointi: FMX-lomake tuntee vain „Initialize/Navigate/Resize“, ei COM-yksityiskohtia.
  • Sopimukseen perustuva silta: JSON-viestit, joissa on name, valinnainen cid (Correlation-ID) ja payload, ovat ylläpidettäviä ja testattavia.
  • Käyttövarma pysyvyys: hallittu UserDataFolder estää välimuistikollisioita, käyttöoikeusongelmia ja tilanteet joissa sovellus „ajetaan kehittäjän koneella, ei tuotannossa“.

JS↔Delphi-Bridge: miksi WebMessage on vakaampi kuin ExecuteScript

WebView2 tarjoaa useita kommunikaatiotapoja. Käytännössä ExecuteScript on houkutteleva, mutta vaikea versionhallita: työnnätte merkkijonoja tulkitsijaan ilman selkeää vastauskanavaa ja ilman robustia virheiden kartoitusta. PostWebMessageAsString / WebMessageReceived on sen sijaan määritelty kanava.

Erityistapaus, joka yritysympäristöissä usein esiintyy: sinun täytyy aloittaa Delphi-työnkulku web-käyttöliittymästä (esim. sisäinen portaali) (tulostus, laitekäyttö, legacy-integraatio). Silloin tarvitset:

  • valkoisen listan (whitelist) sallituille viestinimille
  • korrelaatio-ID:t asynkronisia vastauksia varten
  • keskitetyn paikan, joka validoi payloadit (esim. pakolliset kentät, kokorajoitukset)

Isäntäprosessissa tämä on kohta OnWebMessageReceived. Varsinainen validointi kuuluu sitä ylemmälle tasolle (esim. Application-Service), jotta UI-/WebView2-tekniikka ja liiketoimintalogiikka pidetään erillään (klassinen kerrosarkkitehtuuri: UI → Application → Domain → Infrastruktur).

Lataukset ja tiedostojen säilytys: mikä tuotannossa usein yllättää

WebView2-lataukset kulkevat ICoreWebView2DownloadOperation-rajapinnan kautta. Lähteestä riippuen ResultFilePath voi olla aluksi tyhjä tai täyttyä vasta myöhemmin. Lisäksi monet yritykset eivät halua, että loppukäyttäjät tallentavat tiedostoja hallitsemattomiin kansioihin.

Vakiintuneet mallit:

  • DownloadStarting-tapahtuman kaappaaminen ja UI:n hoitaminen itse kutsumalla esimerkiksi args.put_Handled(1) (oma polku, nimeämiskäytäntö, karanteenikansio).
  • Tiedoston kokoon liittyvät rajat ja MIME-tyypin tarkistukset, jotta ei vahingossa tallenneta vaikkapa 4 GB lokitiedostoa.
  • Auditointi: kirjatkaa latauksen metatiedot (URI, MIME, tavumäärä) lokiin, älkää sisältöä.

Jos prosessinne ovat säänneltyjä (esim. hyväksynnät, jäljitettävyys), tapahtumakäsittelyn kautta tehtävä hallinta on ainoa paikka, jossa selainmaailma voidaan integroida operatiivisiin sääntöihinne.

Debuggaus: DevTools, Remote Debug Port ja toistettavat tilat

WebView2-debuggaus menee usein pieleen siksi, että tiloja ei saada toistettua. Kaksi säätökohtaa auttavat:

  • DevToolsin voi ottaa päälle/pois ICoreWebView2Settings-rajapinnan kautta (koodissa: SetDevToolsEnabled) – tuotantoversiossa usein pois, tukitapauksessa tarvittaessa päälle.
  • Vakaa UserDataFolder: kun tukitiimin pitää toistaa virhe, määritelty polku on kullanarvoinen. Kansion voi arkistoida/zipata (huomioi tietosuoja/PII) ja tiloja verrata kohdistetusti.

Valinnaisesti (riippuen wrapperista) voit antaa EnvironmentOptionsille lisäselaimen argumentteja, esimerkiksi remote debug -portin. Tämä on hyödyllistä, jos sinun pitää analysoida sovellusta testijärjestelmässä ilman paikallisia kehitystyökaluja. Rajoitukset: tuotantoympäristössä tämä on dokumentoitava ja hyväksyttävä huolellisesti, muuten luot tarpeettoman hyökkäyspinnan.

Stolperfallen in Delphi WebView2 FMX: COM, Threads und Form-Lifecycle

1) Callbacks sulkemisen jälkeen

Asynkroniset CompletedHandler-kutsut voivat saapua vasta, kun lomake on jo sulkemassa. Esimerkkikoodissa FDestroyed estää pääsyn jo vapautettuihin objekteihin. Vakaampi käytäntö on lisäksi:

  • Tallenna tapahtumien tokenit ja kutsu Destroy:ssa siististi remove_*
  • Salli InitializeAsync vain kerran (state-machine: Created/Initializing/Ready/Disposed)

2) Thread-Kontext

Monet handlerit ovat „UI-lähestyviä“, mutta älä luota siihen, että voit kirjoittaa suoraan FMX-komponentteihin. Kun päivität käyttöliittymää OnWebMessage:ssa, on TThread.Queue(nil, ...) turvallinen vaihtoehto. Erotan mielelläni vastuut: Host kerää tapahtuman, Application-Service tekee päätöksen, ja UI päivitetään yksinomaan Queue:n kautta.

3) DPI/Resize und FMX-Layouts

FMX laskee loogisissa yksiköissä, kun taas WebView2 odottaa pikselikoordinaatteja. Käytännössä tarvitsette selkeän paikan, jossa muunnetaan FMX-komponenttien bounds todellisiksi pikseleiksi. Esimerkkikoodi olettaa TRect-arvon; lomakkeessanne tulisi johtaa siitä WinAPI-koordinaatit (esim. FMX.Platform.Winin ja Handle-APIen kautta). Jos sovellus skaalaa monitorin DPI:n mukaan, testatkaa monitorien välistä vaihtoa: WebView2 on tässä herkempi kuin pelkät FMX-komponentit.

Milloin WebView2 FMX:ssä kannattaa – ja milloin ei

WebView2 kannattaa, jos haluat hyödyntää web‑tekniikkaa kohdennetusti olemassa olevassa Delphi-asiakasohjelmassa: upotetut admin‑näkymät, OAuth/OIDC‑kirjautumisflowt, HTML‑raportit, sisäiset portaalit tai hallitut „Micro-Frontends“. Myös modernisointisillan roolissa se on käytännöllinen, kun vastuut on selkeästi rajattu eikä silta muutu hallitsemattomaksi takaoveksi liiketoimintalogiikalle.

Lähestymistavan rajoitukset:

  • Plattform: Malli on Windows-keskeinen. FMX on monialustainen, WebView2 ei. macOS/iOS/Android‑ympäristöihin tarvitsette muita WebView‑ratkaisuja tai abstraktiokerroksen.
  • Security/Hardening: Kun ulkoista sisältöä ladataan, on rajoitettava navigointia, sallittuja domaineja ja latauskohteita tiukasti. Tämä kuuluu vaatimusmäärittelyihin, ei „myöhemmin“.
  • Support: UserDataFolder ja runtime-riippuvuudet (WebView2 Runtime) on otettava osaksi käyttö-/rollout‑konseptia.

Yhteenveto

Delphi WebView2 FMX on vähemmän UI‑gadget kuin integraatiokomponentti, jolla on oma elinkaari. Jos kapsellette alustamisen, tapahtumankäsittelyn, UserDataFolderin ja JS‑Bridgen rakenteellisesti, WebView2:sta tulee vakaa osa digitaalisten yritysratkaisujen arkkitehtuuria: web‑UI sinne, missä se on perusteltua, ja Delphi‑logiikka sinne, missä sen kuuluu olla. Jos taas suoritatte skriptejä hallitsemattomasti, jätätte polut sattuman varaan ettekä irrota eventtejä, ilmenee juuri sellaisia „sporadisch im Feld“ -tyyppisiä virheitä, jotka syövät aikaa ja heikentävät luottamusta.

Jos haluatte integroida WebView2:n siististi olemassa olevaan Delphi-sovellukseen tai arvioida modernisointireunaa teknisesti, ottakaa yhteyttä meihin:

Ammatillisessa kontekstissa myös Webview2 Firemonkey ja Delphi Fmx Edge Browser näyttelevät tärkeää roolia, kun integraatioiden, datavirtojen ja jatkokehityksen on toimittava hallitusti yhdessä.

Keskustele projektista tai modernisointihankkeesta Net-Base kanssa.

Seuraava vaihe

Kun aiheesta tulee todellinen projekti, arkkitehtuuri, nykyinen järjestelmäkanta ja käyttö tulisi varhaisessa vaiheessa tarkastella yhdessä.

Emme tue pelkästään yksittäiskysymyksissä, vaan myös silloin, kun lähdekoodipalasista, legacy-aiheista tai portaali-ideoista halutaan muodostaa luotettava yrityshanke.

  • Nykytila, tavoitetila ja tekniset riskit arvioidaan yhdessä.
  • REST, datan käyttö, portaalit ja käyttöönotto eivät jätetä myöhempien seurausten varaan.
  • Näette ajoissa, mikä ratkaisu on taloudellisesti ja toiminnallisesti kestävä.

Jaa artikkeli

Jaa tämä viesti suoraan

LinkedIn, X, XING, Facebook, WhatsApp ja sähköposti ovat heti käytettävissä. Instagramia varten valmistelemme linkin ja lyhyen tekstin.

Sähköposti

Instagram avautuu uuteen välilehteen. Linkki ja lyhyt teksti kopioidaan ensin leikepöydälle.