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.
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, valinnainencid(Correlation-ID) japayload, ovat ylläpidettäviä ja testattavia. - Käyttövarma pysyvyys: hallittu
UserDataFolderestää 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 siististiremove_* - Salli
InitializeAsyncvain 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ä.