No žurnāla tēmas līdz projektu praksei
Atbilstošas pakalpojumu un tehniskās lapas rakstam
Kad esošā biznesa programmatūrā pēkšņi vēlas „ātri un vienkārši“ iegult modernu tīmekļa saturu, ceļš uz Windows ved caur WebView2. Savukārt Delphi WebView2 FMX WebView2 FMX gadījumā pamatproblēma reti ir URL attēlošana, bet gan tīra iegulšana FireMonkey lietotāja saskarnē (FMX), uzticama inicializācija (asinkrona un COM bāzēta), kā arī Edge slazdi saistībā ar User-Data katalogiem, lejupielādēm, atkļūdošanu un robustu JS↔Delphi komunikāciju.
Šis koda fragments demonstrē modeli, ko es priekšroku dāvāju uzturējamām lietojumprogrammām: kapsulēts „Host“ objekts, kas kontrolē WebView2 dzīves ciklu, un definēta tilta implementācija, izmantojot WebMessage (JSON), nevis nejauša „ExecuteScript všur“ izmantošana. Mērķis nav demonstrācijas kods, bet komponentes gabals, kas izdzīvo izaugotā klienta lietotnē.
Kāpēc WebView2 FMX atšķiras no „Browser-Component drop“
WebView2 ir COM/WinRT tuvā API ar asinkronu inicializāciju. FireMonkey abstraktē Windows-handle, tomēr WebView2 gadījumā jums galu galā nepieciešams reāls Parent-Window (HWND) un kontrolēta Resize-/Focus-virzīšana. Vienlaikus notikumi ne vienmēr norisinās tur, kur FMX to sagaida. Ja šeit sākat „quick and dirty“, parasti iegūstat:
- sporādiskas AV kļūdas formu aizvēršanas laikā (Callbacks pienāk pēc Destroy)
- navigācijas notikumi no nepareiza thread konteksta
- neuzticama persistēšana/keša problēmas sakarā ar neskaidru UserDataFolder stratēģiju
- nav lejupielāžu vai „iestrēguši“ lejupielādes dialogi
- atkļūdošana pa veiksmi, nevis ar mērķētu Remote-Debug konfigurāciju
Pretlīdzeklis ir skaidrs dzīves cikls: Create → InitializeAsync → Attach → Navigate → Detach/Dispose – un definēta robeža starp UI un pārlūka dzinēju.
Source-Schnipsel: WebView2Host für Delphi WebView2 FMX
Nākamais kods iezīmē kapsulētu host-klasi, kas (1) izveido WebView2-Environment konfigurāciju, (2) sasaista Controller objektu ar HWND, (3) vada Navigation un Download notikumus un (4) piedāvā JSON-bāzētu JS-tiltu caur WebMessageReceived. Kods ir apzināti „architekturfähig“: tas kapsulē COM atsauces, novērš callback pēcnācējus pēc Destroy un atļauj ekspluatācijas šķautnes, piemēram, atsevišķus UserDataFolder risinājumus „pro User“ vai „pro Maschine“.unit WebView2Host;
interface
uses
System.SysUtils, System.Classes, System.IOUtils, System.JSON,
Winapi.Windows, Winapi.ActiveX,
FMX.Types,
WebView2, WebView2_TLB; // atkarībā no konfigurācijas: WebView2.pas vai 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 var nebūt vai var būt 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;
// Atvienot notikumus pirms COM objektu atbrīvošanas
UnhookEvents;
FWebView := nil;
FController := nil;
FEnvironment := nil;
inherited;
end;
procedure TWebView2Host.EnsureNotDestroyed;
begin
if FDestroyed then
raise EInvalidOperation.Create(‚WebView2Host jau ir iznīcināts.‘);
end;
function TWebView2Host.MakeUserDataFolder: string;
begin
if FUserDataFolder <> “ then
Exit(FUserDataFolder);
// Prakse: pa lietotnei un pa Windows lietotājam, ne programmmapē
Result := TPath.Combine(TPath.GetHomePath, ‚AppDataLocalMyCompanyMyAppWebView2‘);
ForceDirectories(Result);
end;
procedure TWebView2Host.InitializeAsync;
var
UserData: string;
Opt: ICoreWebView2EnvironmentOptions;
begin
EnsureNotDestroyed;
UserData := MakeUserDataFolder;
// Iespējas: šeit var pievienot papildu pārlūka argumentus, piem., 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;
// Controller an Parent HWND binden
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;
// Initial sichtbar machen
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));
// Piezīme: lai droši atvienotu notikumus, saglabājiet tokenus.
// Daudzos projektos tas ir pietiekami, ja host objektam ir tāds pats dzīves cikls kā formai.
end;
procedure TWebView2Host.UnhookEvents;
begin
// Robustā versija: saglabāt tokenus un izsaukt remove_*.
// Šeit komentārā, jo importvienības konfigurācija un tokenu pārvaldība var atšķirties atkarībā no wrappera.
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));
// Praksē: ResultFilePath sākotnēji var būt tukšs, atkarībā no avota.
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);
// Neobligāti: sava lejuplādes UI, tad iestatiet 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 vēl nav inicializēts.‘);
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.
Pieejas mērķis
- Dzīves cikla kapsulēšana: FMX forma pazīst tikai „Initialize/Navigate/Resize“, ne COM detaļas.
- Tilts ar definētu līgumu: JSON ziņas ar
name, pēc izvēlescid(Correlation-ID) unpayloadir uzturamas un testējamas. - Darbības droša persistēšana: kontrolēts
UserDataFoldernovērš keša sadursmes, piekļuves tiesību problēmas un situācijas „strādā uz izstrādātāja datora, nevis ražošanā“.
JS↔Delphi-Bridge: kāpēc WebMessage ir stabilāks nekā ExecuteScript
WebView2 nodrošina vairākas komunikācijas iespējas. Praktiski ExecuteScript ir pievilcīgs, bet grūti versijot: jūs iedodat interpreteram virknes, bez skaidriem atbildes kanāliem un bez robustas kļūdu kartēšanas. PostWebMessageAsString / WebMessageReceived ir savukārt definēts kanāls.
Bieži uzņēmumu vidē sastopams gadījums: jums jāuzsāk no Web-frontend (piem., iekšējā portāla) Delphi darba plūsma (drukāšana, ierīču piekļuve, Legacy-Integrācija). Tad jums nepieciešams:
- baltais saraksts ar ziņu nosaukumiem
- Correlation-IDs asīnhronām atbildēm
- centrāla vieta, kas validē payloads (piem., obligātie lauki, izmēru ierobežojumi)
Hostā tā ir vieta OnWebMessageReceived. Pašai validācijai jābūt augstākā līmeņa slānī (piem., Application-Service), lai UI/WebView2 tehnika un biznesa loģika būtu atdalītas (klasiskā slāņu arhitektūra: UI → Application → Domain → Infrastruktur).
Lejupielādes un failu glabāšana: kas darbībā bieži pārsteidz
Lejupielādes WebView2 notiek caur ICoreWebView2DownloadOperation. Atkarībā no avota ResultFilePath var būt sākotnēji tukšs vai tikt iestatīts vēlāk. Turklāt daudzas organizācijas nevēlas, lai gala lietotāji saglabā failus nekontrolētās mapēs.
Pārbaudīti modeļi:
- Noķert DownloadStarting un ar
args.put_Handled(1)nodrošināt UI pašu pārvaldību (iekšējais ceļš, nosaukumu konvencija, karantīnas mape). - Faila izmēru ierobežojumi un MIME tipa pārbaudes, lai izvairītos no „nejaušas 4 GB žurnālfailas“.
- Auditēšana: ierakstiet lejupielādes metadatus (URI, MIME, baiti) savā žurnālvedībā, ne saturu.
Ja jums ir reglamentēti procesi (piem., apstiprinājumi, izsekojamība), notikumu apstrāde ir vienīgā vieta, kur varat integrēt pārlūka pasauli savos darbības noteikumos.
Atkļūdošana: DevTools, Remote Debug Port un reproducējami stāvokļi
WebView2 atkļūdošana bieži neizdodas tāpēc, ka stāvokļi nav reproducējami. Divi regulējami punkti palīdz:
- Aktivēt/deaktivēt DevTools caur
ICoreWebView2Settings(koda līmenī:SetDevToolsEnabled) – Release versijā parasti izslēgts, atbalsta gadījumā ieslēdzams mērķtiecīgi. - Stabils UserDataFolder: ja atbalsts vēlas reproducēt kļūdu, definēts ceļš ir ļoti vērtīgs. Jūs varat mapīti saglabāt/saspiest (Uzmanību: datu aizsardzība/PII) un mērķtiecīgi salīdzināt stāvokļus.
Pēc izvēles (atkarībā no Wrapper) varat EnvironmentOptions aprīkot ar papildu pārlūkargumentiem, piem., Remote-Debug-Port. Tas ir lietderīgi, ja jāanalizē lietojumprogramma testēšanas sistēmā bez vietējiem izstrādātāju rīkiem. Ierobežojumi: ražošanas vidē tas jāatļauj un jādokumentē skaidri, pretējā gadījumā tiek radīta nevajadzīga uzbrukuma virsma.
Klupšanas akmeņi in Delphi WebView2 FMX: COM, Threads und Form-Lifecycle
1) Callbacks nach dem Schließen
Asinhronie CompletedHandler var pienākt pēc tam, kad forma jau tiek aizvērta. Snippetā FDestroyed novērš piekļuvi atbrīvotiem objektiem. Robusktāk ir papildus:
- Glabāt tokenus notikumiem un
Destroylaikā korekti izsauktremove_* - Atļaut
InitializeAsynctikai vienu reizi (State-Machine: Created/Initializing/Ready/Disposed)
2) Thread-Kontext
Daudzi handleri nāk it kā «UI-tuvumā», tomēr neuzticieties, ka varēsiet tieši rakstīt FMX kontrolēs. Ja atjaunināt UI no OnWebMessage, TThread.Queue(nil, ...) ir drošā izvēle. Es parasti atdala: Host savāc notikumu, Application-Service izšķir loģiku, UI tiek atjaunināta izslēgti caur Queue.
3) DPI/Resize und FMX-Layouts
FMX rēķina loģiskajās vienībās, WebView2 gaida Pixel-Rects. Praktiski nepieciešama skaidra vieta, kur no FMX-Controls iegūt bounds un pārrēķināt tos uz reāliem pikseļiem. Snippet pieņem TRect; jūsu formā no tā vajadzētu atvasināt WinAPI-koordinātas (piem., izmantojot FMX.Platform.Win un Handle-APIs). Ja lietotne mērogojas pēc monitora DPI, testējiet pāreju starp monitoriem: WebView2 šeit ir jutīgāks nekā tīri FMX-kontroles.
Wann sich WebView2 in FMX lohnt – und wann nicht
WebView2 atmaksājas, ja vēlaties mērķtiecīgi izmantot web-tehnoloģijas esošā Delphi-klienta lietotnē: iebūvēti admin‑skati, OAuth/OIDC pieteikšanās plūsmas, HTML‑atskaites, iekšējie portāli vai kontrolēti “Micro-Frontends”. Kā modernizācijas tilts tas arī ir praktisks, ja pienācīgi nodalāt atbildības un neļaujat tiltam kļūt par nekontrolētu aizmugures durvīm biznesa loģikai.
Pieejas ierobežojumi:
- Plattform: Šis modelis ir Windows-centrēts. FMX ir multiplatformu spējīgs, WebView2 — nē. Par macOS/iOS/Android jums vajadzēs citas WebViews vai abstrakcijas slāni.
- Security/Hardening: Tiklīdz tiek ielādēts ārējs saturs, jāierobežo navigācija, atļautās domēnas un lejupielādes mērķi stingrāk. Tas jāiekļauj prasību specifikācijā, nevis “vēlāk”.
- Support: UserDataFolder un runtime‑atkarības (WebView2 Runtime) jāiekļauj jūsu ekspluatācijas/rollout koncepcijā.
Fazit
Delphi WebView2 FMX nav vien UI‑gadget, bet integrācijas komponents ar savu lifecycle. Ja inicializāciju, eventing, UserDataFolder un JS‑Bridge strukturēti kapsulējat, WebView2 kļūs par stabilu būvbloku digitālām uzņēmuma risinājumiem: web‑UI tur, kur tam ir jēga, un Delphi‑loģika tur, kur tai jābūt. Ja savukārt nekontrolēti izsauksiet skriptus, atstājat ceļus nejaušībai un neatslēdzienēsiet eventus, iegūsiet tieši to veidu “sporādiski lauka” kļūdu, kas ēd laiku un grauj uzticību.
Ja vēlaties tīri integrēt WebView2 esošā Delphi‑lietotnē vai tehniski novērtēt modernizācijas malas, sazinieties ar mums:
Faktniskajā kontekstā arī Webview2 Firemonkey un Delphi Fmx Edge Browser spēlē nozīmīgu lomu, ja integrācijas, datu plūsmas un tālākā attīstība ir jāvirzās kopā.
Projekt oder Modernisierungsvorhaben mit Net-Base besprechen.
Nākamais solis
Ja no tēmas rodas reāls projekts, arhitektūra, esošais stāvoklis un ekspluatācija būtu jāizskata kopā jau agri.
Mēs atbalstām ne tikai atsevišķu jautājumu risināšanā, bet arī tad, kad no avota koda fragmentiem, mantojuma sistēmu jautājumiem vai portāla idejām jāizveido stabils uzņēmuma līmeņa projekts.
- Esošais stāvoklis, mērķa stāvoklis un tehniskie riski tiek kopīgi vērtēti.
- REST, datu piekļuve, portāli un izvēršana netiek atlikti kā vēlākas sekas.
- Jūs savlaicīgi redzat, kurš ceļš ir ekonomiski un darbības ziņā dzīvotspējīgs.