От темата в списанието към проектната практика
Подходящи страници за услуги и технологии към публикацията
Когато в съществуващ бизнес софтуер изведнъж искат „просто така“ да вградят модерно уеб съдържание, се стига при 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 също играят важна роля, когато интеграциите, потокът от данни и по‑нататъшното развитие трябва да се координират коректно.
Nächster Schritt
Wenn aus dem Thema ein reales Projekt wird, sollten Architektur, Bestand und Betrieb früh zusammen betrachtet werden.
Подпомагаме не само при отделни въпроси, но и когато от фрагменти от изходен код, проблеми с наследени системи или идеи за портал трябва да бъде реализиран надежден корпоративен проект.
- Сегашното състояние, целевото състояние и техническите рискове се оценяват съвместно.
- REST, Datenzugriff, Portale und Rollout werden nicht als Spätfolgen verschoben.
- Sie sehen früh, welcher Weg wirtschaftlich und betrieblich tragfähig ist.