Nuo žurnalo temos iki projekto įgyvendinimo
Tinkami puslapiai apie paslaugas ir techninę informaciją šiam įrašui
Jei esamoje verslo programoje staiga reikia „greitai“ integruoti modernų žiniatinklio turinį, naudojant WebView2 dažnai susiduriama su Windows. In Delphi WebView2 FMX pagrindinė problema retai būna URL rodymas — svarbiau švarus įterpimas į FireMonkey sąsają (FMX), patikimas inicializavimas (asinchroninis ir COM pagrindu) bei Edge spąstai, susiję su User-Data katalogais, atsisiuntimais, derinimu ir tvirta JS↔Delphi komunikacija.
Šis Source‑schnipsel rodo modelį, kurį aš teikiu pirmenybę prižiūrimoms programoms: inkapsuliuotą „Host“ objektą, kontroliuojantį WebView2 gyvavimo ciklą, ir apibrėžtą tiltą per WebMessage (JSON), vietoje bet kur vykdomo „ExecuteScript“. Tikslas nėra demo‑kodas, o komponentas, kuris išgyvena augančiuose kliento sprendimuose.
Kodėl WebView2 FMX skiriasi nuo „Browser-Component drop“
WebView2 yra COM/WinRT artima API su asinchroniniu inicializavimu. FireMonkey abstraktina Windows‑handles, vis dėlto galų gale WebView2 reikalauja tikro Parent‑Window (HWND) ir kontroliuojamo dydžio/fokusavimo persiuntimo. Tuo pačiu įvykiai ne visada vyksta ten, kur juos tikimasi FMX. Jei čia pradėsite „quick and dirty“, typiškai gausite:
- sporadiškas AV prie formos uždarymo (Callbacks atvyksta po Destroy)
- navigacijos įvykiai iš neteisingo gijos (Thread) konteksto
- nepatikima persistencija/talpyklos problemos dėl neaiškios UserDataFolder strategijos
- neveikiantys atsisiuntimai arba „užstrigę“ atsisiuntimų dialogai
- derinimas tik sėkmės dėka vietoje tikslios nuotolinio derinimo konfigūracijos
Atsvara yra aiškus gyvavimo ciklas: Create → InitializeAsync → Attach → Navigate → Detach/Dispose – ir apibrėžta riba tarp UI ir naršyklės variklio.
Source‑Schnipsel: WebView2Host für Delphi WebView2 FMX
Žemiau pateiktas kodas eskizuoja inkapsuliuotą Host klasę, kuri (1) sukuria WebView2 aplinkos konfigūraciją, (2) pririša Controller objektą prie HWND, (3) sujungia navigacijos ir atsisiuntimų įvykius ir (4) siūlo JSON pagrindu veikiančią JS‑Bridge per WebMessageReceived. Kodo dizainas sąmoningai „architektūriškas“: jis inkapsuliuoja COM‑referencijas, užkerta kelią callback likučiams po Destroy ir leidžia eksploatacijos scenarijus, pavyzdžiui atskirus UserDataFolder „pro User“ arba „pro Maschine“.
unit WebView2Host;
interface
uses
System.SysUtils, System.Classes, System.IOUtils, System.JSON,
Winapi.Windows, Winapi.ActiveX,
FMX.Types,
WebView2, WebView2_TLB; // priklausomai nuo nustatymų: WebView2.pas arba 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 gali nebūti arba būti 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;
// Atjungti įvykius prieš išlaisvinant COM objektus
UnhookEvents;
FWebView := nil;
FController := nil;
FEnvironment := nil;
inherited;
end;
procedure TWebView2Host.EnsureNotDestroyed;
begin
if FDestroyed then
raise EInvalidOperation.Create(‚WebView2Host jau sunaikintas.‘);
end;
function TWebView2Host.MakeUserDataFolder: string;
begin
if FUserDataFolder <> “ then
Exit(FUserDataFolder);
// Praktikoje: po programą ir po Windows vartotoją, ne programos kataloge
Result := TPath.Combine(TPath.GetHomePath, ‚AppDataLocalMyCompanyMyAppWebView2‘);
ForceDirectories(Result);
end;
procedure TWebView2Host.InitializeAsync;
var
UserData: string;
Opt: ICoreWebView2EnvironmentOptions;
begin
EnsureNotDestroyed;
UserData := MakeUserDataFolder;
// Parinktys: čia galima pridėti papildomus naršyklės argumentus, pvz., 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;
// Prijungti valdiklį prie tėvinio 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;
// Nustatyti pradinį matomumą
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));
// Pastaba: Norint patikimai atjungti, reikėtų išsaugoti tokenus.
// Daugelio projektų atveju tai pakanka, jei hostas egzistuoja tik kartu su forma.
end;
procedure TWebView2Host.UnhookEvents;
begin
// Patikimesnė versija: išsaugoti tokenus ir kviesti remove_*.
// Čia palikta kaip komentaras, nes importo vieneto nustatymai ir tokenų valdymas skiriasi priklausomai nuo wrapperio.
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));
// Praktikoje: ResultFileName pradžioje tuščias, priklausomai nuo šaltinio.
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);
// Nebūtina: savas atsisiuntimo UI, tada nustatyti 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 dar nėra inicializuotas.‘);
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.
Požiūrio paskirtis
- Gyvavimo ciklo kapsuliavimas: FMX forma žino tik „Initialize/Navigate/Resize“, o ne COM detales.
- Tiltas su sutartimi: JSON pranešimai su
name, neprivalomucid(Correlation-ID) irpayloadyra prižiūrimi ir testuojami. - Eksploatacijai saugi persitencija: kontroliuojamas
UserDataFolderužkerta kelią talpyklos konfliktams, teisių problemoms ir situacijoms, kai „veikia tik ant kūrėjo kompiuterio, bet ne eksploatacijoje“.
JS↔Delphi-Bridge: kodėl WebMessage yra stabilesnė nei ExecuteScript
WebView2 siūlo kelis komunikacijos būdus. Praktikoje ExecuteScript vilioja, bet yra sunkiai versijonuojamas: jūs įterpiate eilutes į interpretatorių be aiškių atsakymo kanalų ir be patikimo klaidų susiejimo (error-mapping). PostWebMessageAsString / WebMessageReceived yra apibrėžtas kanalas.
Kraštinis atvejis, dažnai pasitaikantis įmonių aplinkoje: jums reikia iš web-frontendo (pvz., vidinio portalo) paleisti Delphi darbo eigą (spausdinimas, įrenginių prieiga, paveldėtos sistemos integracija). Tuomet jums reikia:
- baltasis sąrašas pranešimų pavadinimams
- Correlation-IDs asinchroniniams atsakymams
- centrinė vieta, kuri validuoja payload’us (pvz., privalomi laukai, dydžio ribos)
Hoste tai yra vieta OnWebMessageReceived. Tikrasis validavimas turi būti aukštesniame sluoksnyje (pvz., Application-Service), kad UI/WebView2 technika ir verslo logika būtų atskirtos (klasikinė sluoksnių architektūra: UI → Application → Domain → Infrastruktur).
Atsisiuntimai ir failų saugykla: kas eksploatacijoje dažnai nustebina
Atsisiuntimai WebView2 vykdomi per ICoreWebView2DownloadOperation. Priklausomai nuo šaltinio ResultFilePath gali būti tuščias iš karto arba nustatomas vėliau. Be to, daug įmonių nenori, kad galutiniai vartotojai išsaugotų į nekontroliuojamus aplankus.
Patikrinti modeliai:
- Perimti DownloadStarting įvykį ir per
args.put_Handled(1)perduoti UI atsakomybę (savo kelias, vardų konvencija, karantino aplankas). - Failo dydžio ribos ir MIME tipo patikrinimai, kad būtų išvengta „netyčinio 4 GB žurnalo“.
- Auditavimas: atsisiuntimo metaduomenis (URI, MIME, baitai) rašyti į jūsų žurnalus, ne turinį.
Jei turite reguliuojamus procesus (pvz., patvirtinimus, atsekamumą), įvykių lygmeniu tvarkymas yra vienintelė vieta, kur galite integruoti naršyklės pasaulį į savo eksploatacijos taisykles.
Derinimas: DevTools, Remote Debug Port ir reproducuojamos būsenos
WebView2 derinimas dažnai strigsta dėl to, kad būsenos nėra reproducuojamos. Dvi reguliavimo priemonės padeda:
- DevTools įjungimas/išjungimas per
ICoreWebView2Settings(kodas:SetDevToolsEnabled) – release versijose dažnai išjungta, pagalbos atveju įjungiama tikslingai. - Stabilus UserDataFolder: jei jūsų palaikymas turi atkartoti klaidą, apibrėžtas kelias yra labai vertingas. Galite aplanką išsaugoti/zip’inti (dėmesio: duomenų apsauga/PII) ir palyginti būsenas tikslingai.
Papildomai (priklausomai nuo wrapper’io) galite priskirti EnvironmentOptions papildomais naršyklės argumentais, pvz., nuotolinio derinimo portu. Tai prasminga, kai turite analizuoti programą testinėje sistemoje be vietinių kūrimo įrankių. Ribos: produkcinėse aplinkose tai turi būti aiškiai įjungta ir dokumentuota, priešingu atveju sukuriate nereikalingą atakos paviršių.
Klupimo akmenys Delphi WebView2 FMX: COM, siūlai ir formos gyvavimo ciklas
1) Callback’ai po uždarymo
Asinchroniniai CompletedHandler gali atvykti po to, kai forma jau uždaroma. Pavyzdyje FDestroyed užkerta kelią prieigai prie išlaisvintų objektų. Patikimiau yra papildomai:
- Išsaugokite eventų tokenus ir metoduose
Destroytvarkingai iškvieskiteremove_* - Leiskite
InitializeAsynctik vieną kartą (būsenų mašina: Created/Initializing/Ready/Disposed)
2) Thread-Kontext
Daugelis handlerių yra „UI-nah“, tačiau nepasikliaukite, kad galite tiesiogiai rašyti į FMX valdiklius. Jei atnaujinate UI per OnWebMessage, saugesnis variantas yra TThread.Queue(nil, ...). Aš mėgstu atskirti: host renka įvykį, Application-Service priima sprendimą, UI atnaujinama vien tik per Queue.
3) DPI/Resize und FMX-Layouts
FMX skaičiuoja loginėmis vienetais, WebView2 tikisi pikselių rect’ų. Praktikoje jums reikia aiškios vietos, kur iš FMX valdiklių Bounds paverčiate tikrais pikseliais. Snippet’as priima TRect; jūsų formoje turėtumėte iš jo išvesti WinAPI koordinates (pvz. per FMX.Platform.Win ir Handle-APIs). Jei programa skalėja pagal monitoriaus DPI, išbandykite perėjimą tarp monitorių: WebView2 čia jautresnė nei gryni FMX valdikliai.
Kada WebView2 FMX pasiteisina – ir kada ne
WebView2 apsimoka, jei norite augančioje Delphi-kliento programoje tiksliai naudoti web-technologijas: integruotos administravimo peržiūros, OAuth/OIDC prisijungimo srautai, HTML ataskaitos, vidiniai portalai arba kontroliuojami „Micro-Frontends“. Taip pat kaip modernizacijos tiltas jis yra praktiškas, kol aiškiai apibrėžiate atsakomybes ir tiltas netampa nekontroliuojama verslo logikos galine durele.
Šio požiūrio ribos:
- Plattform: Šis modelis yra Windows-centrinis. FMX yra daugplatforminis, WebView2 tuo nėra. Dėl macOS/iOS/Android reikės kitų WebViews arba abstrakcijos sluoksnio.
- Security/Hardening: Kai tik kraunamas išorinis turinys, privalote griežčiau riboti navigaciją, leistinas domenų sąrašas ir atsisiuntimų tikslus. Tai turi būti įtraukta į reikalavimus, ne „vėliau“.
- Support: UserDataFolder ir runtime priklausomybės (WebView2 Runtime) turi būti jūsų eksploatacijos/rollout koncepcijos dalis.
Išvada
Delphi WebView2 FMX nėra tik UI-gadžetas, o integracijos komponentas su savitu lifecycle. Jei struktūriškai kapsuliuosite inicializaciją, eventing, UserDataFolder ir JS-Bridge, WebView2 taps stabiliu komponentu skaitmeninėms įmonių sprendimams: Web-UI ten, kur jis prasmingas, ir Delphi-logika ten, kur jai vieta. Jei nekontroliuojamai paleidžiate skriptus, paliekate takus atsitiktinumui ir neatjungiame įvykių, gausite būtent tokio tipo „sporadiškas lauke“ klaidas, kurios išeikvoja laiką ir kenkia pasitikėjimui.
Jei norite tvarkingai integruoti WebView2 į esamą Delphi-programą arba techniškai įvertinti modernizacijos ribą, susisiekite su mumis:
Profesiniame kontekste taip pat svarbų vaidmenį atlieka Webview2 Firemonkey ir Delphi Fmx Edge Browser, kai integracijos, duomenų srautai ir tolesnė plėtra turi veikti darniai.
Kitas žingsnis
Kai tema virsta realiu projektu, architektūra, esami sprendimai ir eksploatavimas turėtų būti nagrinėjami kartu nuo pat pradžių.
Mes padedame ne tik pavienėse užklausose, bet ir tuomet, kai iš šaltinio kodo fragmentų, paveldėtų temų ar portalo idėjų turi tapti patikimas įmonės projektas.
- Esama padėtis, tikslinis vaizdas ir techninės rizikos vertinami kartu.
- REST, duomenų prieiga, portalai ir rollout nebus perkelti į vėlesnį etapą kaip vėlyvos pasekmės.
- Jūs anksti matote, kuris kelias yra ekonomiškai ir operaciniškai tvarus.