Net-Base Списание

14.06.2026

Delphi WebView2 в FMX: коректно инициализиране, изграждане на JS-Bridge, управление на изтегляния и отстраняване на грешки

WebView2 в FireMonkey звучи като „просто вграждане на браузър“, но в практиката се проваля при инициализация, навигационни събития, JS↔Delphi-Bridge, обработка на изтегляния и отстраняване на грешки. Този фрагмент от изходния код показва робустен модел с ясно разграничени отговорности...

14.06.2026

От темата в списанието към проектната практика

Подходящи страници за услуги и технологии към публикацията

Когато в съществуващ бизнес софтуер изведнъж искат „просто така“ да вградят модерно уеб съдържание, се стига при Windows до WebView2. В Delphi WebView2 FMX основният проблем рядко е показването на URL, а чистата интеграция в FireMonkey-повърхност (FMX), надеждното инициализиране (асинхронно и базирано на COM), както и капаните на Edge около директории за User-Data, изтеглянията, дебъгването и устойчива JS↔Delphi-комуникация.

Този фрагмент от сорс показва шаблон, който предпочитам за поддържани приложения: капсулиран „Host“-обект, който контролира WebView2-Lifecycle, както и дефиниран мост чрез WebMessage (JSON), вместо произволен „ExecuteScript“ навсякъде. Целта не е демо код, а компонент, който оцелява в развити клиенти.

Защо WebView2 в FMX е различен от „Browser-Component drop“

WebView2 е API, близка до COM/WinRT, с асинхронно инициализиране. FireMonkey абстрахира Windows-Handles, но въпреки това за WebView2 накрая ви е нужно реално Parent-Window (HWND) и контролирано препращане на Resize/Focus. В същото време събитията не винаги се изпълняват там, където ги очаквате в FMX. Ако започнете тук „quick and dirty“, типично ще получите:

  • спорадични AV при затваряне на формата (Callbacks се изпълняват след Destroy)
  • Navigation-събития от грешен контекст на нишка
  • ненадеждна персистентност/проблеми с кеша поради неясна стратегия за UserDataFolder
  • липса на изтегляния или „зависнали“ диалози за изтегляне
  • дебъгване само по късмет вместо чрез целева Remote-Debug конфигурация

Противоотровата е ясен Lifecycle: Create → InitializeAsync → Attach → Navigate → Detach/Dispose – и дефинирана граница между UI и браузър-двигателя.

Фрагмент от изходния код: WebView2Host за Delphi WebView2 FMX

Следният код очертава капсулиран Host-клас, който (1) създава WebView2-Environment конфигурация, (2) привързва Controller-обекта към HWND, (3) свързва Navigation- и Download-събития и (4) предлага JSON-базиран JS-Bridge чрез WebMessageReceived. Кодът е съзнателно „архитектурно-годен“: той капсулира COM-референции, предотвратява последващи callbacks след Destroy и позволява експлоатационни варианти като отделни UserDataFolder „pro User“ или „pro Maschine“.

unit WebView2Host;

interface

uses
System.SysUtils, System.Classes, System.IOUtils, System.JSON,
Winapi.Windows, Winapi.ActiveX,
FMX.Types,
WebView2, WebView2_TLB; // в зависимост от конфигурацията: WebView2.pas или 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 може да липсва или да е 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;

// Разкачете обработчиците на събития, преди да освободите COM обектите
UnhookEvents;

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

inherited;
end;

procedure TWebView2Host.EnsureNotDestroyed;
begin
if FDestroyed then
raise EInvalidOperation.Create(‚WebView2Host вече е унищожен.‘);
end;

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

// Практика: по приложение + по Windows-потребител, не в директорията на програмата
Result := TPath.Combine(TPath.GetHomePath, ‚AppDataLocalMyCompanyMyAppWebView2‘);
ForceDirectories(Result);
end;

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

UserData := MakeUserDataFolder;

// Опции: тук могат да се добавят допълнителни аргументи за браузъра, напр. 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;

// Свързване на контролера към родителския 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;

// Първоначално го направете видим
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));

// Бележка: За по-надеждно откачане запазете токените.
// В много проекти това е достатъчно, ако хостът има същия жизнен цикъл като формата.
end;

procedure TWebView2Host.UnhookEvents;
begin
// По-надежден вариант: запазете токените и извикайте remove_*.
// Тук само като коментар, тъй като настройката на import-юнита и управлението на токените варират в зависимост от 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));

// На практика: ResultFileName първоначално е празно, в зависимост от източника.
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);

// По избор: собствен UI за изтегляне, тогава задайте 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 все още не е инициализиран.‘);

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.

Цел на подхода

  • Капсулиране на жизнения цикъл: FMX-формата познава само „Initialize/Navigate/Resize“, а не COM-подробности.
  • Bridge с договор: JSON-съобщения с name, опционално cid (Correlation-ID) и payload са поддържими и тестируеми.
  • Оперативно надеждна персистентност: контролиран UserDataFolder предотвратява конфликти в кеша, проблеми с правата и ситуацията „работи на машината на разработчика, но не в продукция“.

JS↔Delphi-мост: защо WebMessage е по-стабилен от ExecuteScript

WebView2 предлага няколко канала за комуникация. На практика ExecuteScript е изкушаващ, но трудно подлежащ на версиониране: вкарвате низове в интерпретатор без ясни канали за отговор и без надеждно картографиране на грешки. PostWebMessageAsString / WebMessageReceived е дефиниран канал.

Граничен случай, който често се появява в корпоративни среди: трябва от уеб-фронтенд (напр. вътрешен портал) да стартирате Delphi работен поток (печат, достъп до устройства, интеграция със стари системи). Тогава ви трябват:

  • бял списък с имена на съобщения
  • Correlation-IDs за асинхронни отговори
  • централно място, което валидира payload-ите (напр. задължителни полета, ограничения за размер)

В хоста това е мястото OnWebMessageReceived. Самата валидация принадлежи на слоя над него (напр. Application-Service), за да държите отделени UI-/WebView2-технологията и бизнес логиката (класическа слойна архитектура: UI → Application → Domain → Infrastruktur).

Изтегляния и файлово съхранение: какво често изненадва в продукция

Изтеглянията в WebView2 се управляват чрез ICoreWebView2DownloadOperation. В зависимост от източника ResultFilePath може да е празен в началото или да бъде зададен по-късно. Освен това много компании не желаят крайните потребители да записват в неконтролирани папки.

Добри практики:

  • Хващане на DownloadStarting и чрез args.put_Handled(1) поемане на управлението от вашия UI (собствен път, конвенция за имена, карантинна папка).
  • Ограничения за размер на файловете и проверки на MIME типа, за да се избегне „по погрешка 4 GB лог файл“.
  • Аудит: записвайте метаданните на изтеглянето (URI, MIME, байтове) в логовете си, не съдържанието.

Ако имате регулирани процеси (напр. одобрения, проследимост), обработката през събитията е единственото място, където можете да интегрирате браузърния свят в оперативните си правила.

Отстраняване на грешки: DevTools, Remote Debug Port и възпроизводими състояния

Отстраняването на грешки в WebView2 често се проваля, защото състоянията не са възпроизводими. Две настройки помагат:

  • Активиране/деактивиране на DevTools чрез ICoreWebView2Settings (в кода: SetDevToolsEnabled) – в релийза често изключени, при случай за поддръжка включвайте целенасочено.
  • Стабилен UserDataFolder: ако вашата поддръжка трябва да възпроизведе грешка, дефиниран път е безценен. Можете да архивирате/zip-нете папката (Внимание: защита на данни/PII) и целенасочено да сравнявате състояния.

По избор (в зависимост от wrapper-а) можете да конфигурирате EnvironmentOptions с допълнителни аргументи за браузъра, напр. Remote-Debug-Port. Това е полезно, когато трябва да анализирате приложение на тестова система без локални инструменти за разработка. Ограничения: в продуктивни среди това трябва да бъде правилно активирано и документирано, в противен случай създавате ненужна повърхност за атаки.

Подводни камъни в Delphi WebView2 FMX: COM, нишки и жизнен цикъл на формата

1) Callback‑и след затваряне

Асинхронните CompletedHandler могат да пристигнат, след като формата вече се затваря. В примера FDestroyed предотвратява достъпа до освободени обекти. Допълнително по‑надеждно е:

  • Съхранявайте токени за събития и в Destroy коректно извиквайте remove_*
  • Позволявайте InitializeAsync само веднъж (State‑Machine: Created/Initializing/Ready/Disposed)

2) Контекст на нишката

Много обработвачи са „UI‑близки“, но не разчитайте, че можете да пишете директно в FMX‑контроли. Ако обновявате UI в OnWebMessage, TThread.Queue(nil, ...) е безопасният вариант. Аз предпочитам разделение: Host събира събитието, Application‑Service взема решението, UI се обновява единствено чрез Queue.

3) DPI/Resize и FMX‑оформления

FMX работи с логически единици, WebView2 очаква Pixel‑Rects. На практика ви трябва ясно място, където превеждате Bounds от FMX‑контроли в реални пиксели. Примерът приема TRect; във формата си трябва да извеждате WinAPI‑координатите (напр. чрез FMX.Platform.Win и Handle‑API). Ако приложението се скалира спрямо DPI на монитора, тествайте смяната между монитори: WebView2 е тук по‑чувствителен от чисто FMX‑контроли.

Кога WebView2 в FMX си заслужава — и кога не

WebView2 има смисъл, когато в развита Delphi клиентска апликация искате целенасочено да използвате уеб технологии: вградени Admin‑Views, OAuth/OIDC логин‑потоци, HTML‑репорти, вътрешни портали или контролирани „Micro‑Frontends“. Като мост за модернизация също е практичен, стига да изрежете отговорностите чисто и мостът да не се превърне в неконтролирана задна врата за бизнес‑логика.

Граници на подхода:

  • Plattform: Шаблонът е Windows‑центриран. FMX е многоплатформен, WebView2 — не. За macOS/iOS/Android ще ви трябват други WebViews или абстракционен слой.
  • Security/Hardening: Щом се зареждат външни съдържания, трябва по‑строго да ограничите навигацията, позволените домейни и целите за сваляне. Това трябва да е част от изискванията, не „по‑късно“.
  • Support: UserDataFolder и runtime зависимости (WebView2 Runtime) трябва да са част от вашата концепция за експлоатация/разгръщане.

Заключение

Delphi WebView2 FMX е по‑малко UI‑джаджа и повече интеграционен компонент с собствен lifecycle. Ако капсулирате инициализацията, eventing‑а, UserDataFolder и JS‑Bridge структуриранo, WebView2 става стабилен градивен елемент за цифрови корпоративни решения: Web‑UI там, където има смисъл, и Delphi‑логика там, където ѝ е мястото. Ако обаче пускате скриптове неконтролирано, оставяте пътищата на случайността и не разплетете събитията, ще получите точно онзи тип спорадични грешки в полева експлоатация, които изяждат време и подкопават доверието.

Ако искате да интегрирате WebView2 чисто в съществуващо Delphi приложение или да оцените технически ръба на модернизацията, говорете с нас:

В професионален контекст Webview2 Firemonkey и Delphi Fmx Edge Browser също играят важна роля, когато интеграциите, потокът от данни и по‑нататъшното развитие трябва да се координират коректно.

Обсъдете проект или план за модернизация с Net-Base.

Следваща стъпка

Когато темата прерасне в реален проект, архитектурата, съществуващото състояние и експлоатацията трябва да бъдат разгледани съвместно още в ранна фаза.

Подпомагаме не само при отделни въпроси, но и когато от фрагменти от изходен код, проблеми с наследени системи или идеи за портал трябва да бъде реализиран надежден корпоративен проект.

  • Сегашното състояние, целевото състояние и техническите рискове се оценяват съвместно.
  • REST, достъпът до данни, порталите и разгръщането не се отлагат като по-късни последици.
  • Виждате рано кой път е икономически и експлоатационно жизнеспособен.

Сподели публикацията

Споделете тази публикация директно

LinkedIn, X, XING, Facebook, WhatsApp и имейл са незабавно достъпни. За Instagram ще подготвим връзка и кратък текст.

Електронна поща

Instagram се отваря в нов раздел. Връзката и краткият текст се копират предварително в клипборда.