От темы в журнале к проектной практике
Соответствующие страницы услуг и технологий к статье
Если в существующее бизнес‑приложение внезапно нужно «быстро» внедрить современные веб‑контенты, то по Windows вы натолкнётесь на WebView2. В Delphi WebView2 FMX основная проблема редко заключается в отображении URL; гораздо важнее корректная встраиваемость в интерфейс FireMonkey (FMX), надёжная инициализация (асинхронная и основанная на COM), а также подводные камни Edge, связанные с каталогами пользовательских данных, загрузками, отладкой и устойчивой JS↔Delphi‑коммуникацией.
Этот фрагмент исходного кода демонстрирует шаблон, который я предпочитаю для сопровождаемого ПО: инкапсулированный «Host»-объект, контролирующий жизненный цикл WebView2, и определённый мост через WebMessage (JSON) вместо произвольного «ExecuteScript везде». Цель — не демонстрационный код, а строительный блок, который переживёт эволюцию существующих клиентов.
Почему WebView2 в FMX отличается от «Browser-Component drop»
WebView2 — это API, близкая к COM/WinRT, с асинхронной инициализацией. FireMonkey абстрагирует Windows-дескрипторы, тем не менее для WebView2 в конце концов требуется настоящее родительское окно (HWND) и контролируемая переадресация изменений размера/фокуса. При этом события не всегда выполняются там, где их ожидают в FMX. Если начать здесь «quick and dirty», вы обычно получите:
- спорадические AV при закрытии формы (обратные вызовы приходят после Destroy)
- события навигации из неверного контекста потока
- ненадёжную персистентность/проблемы с кэшем из‑за неясной стратегии UserDataFolder
- отсутствие загрузок или «зависающие» диалоги загрузки
- отладку по счастливому стечению обстоятельств вместо настроенной конфигурации удалённой отладки
Противоядие — чёткий жизненный цикл: Create → InitializeAsync → Attach → Navigate → Detach/Dispose — и определённая граница между UI и движком браузера.
Source‑Schnipsel: WebView2Host für Delphi WebView2 FMX
Ниже приведён код, набрасывающий инкапсулированный класс хоста, который (1) создаёт конфигурацию окружения WebView2, (2) привязывает объект контроллера к HWND, (3) проводит проводку событий навигации и загрузки и (4) предоставляет основанный на JSON JS‑мост через WebMessageReceived. Код сознательно «architekturfähig»: он инкапсулирует COM‑ссылки, предотвращает появление последующих колбэков после 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; // je nach Setup: WebView2.pas oder 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 kann fehlen oder null sein
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;
// Events lösen, bevor COM-Objekte freigegeben werden
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);
// Praxis: pro App + pro Windows-User, nicht in Programmverzeichnis
Result := TPath.Combine(TPath.GetHomePath, 'AppDataLocalMyCompanyMyAppWebView2');
ForceDirectories(Result);
end;
procedure TWebView2Host.InitializeAsync;
var
UserData: string;
Opt: ICoreWebView2EnvironmentOptions;
begin
EnsureNotDestroyed;
UserData := MakeUserDataFolder;
// Options: hier können zusätzliche Browser-Argumente rein, z.B. 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));
// Hinweis: Für robustes Unhooking sollten Sie die Tokens speichern.
// In vielen Projekten ist das ausreichend, wenn der Host nur mit dem Form lebt.
end;
procedure TWebView2Host.UnhookEvents;
begin
// Robust-Variante: Tokens merken und remove_* aufrufen.
// Hier als Kommentar, weil das Import-Unit-Setup und Token-Verwaltung je nach Wrapper variiert.
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));
// In der Praxis: ResultFileName initial leer, je nach Quelle.
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);
// Optional: eigene Download-UI, dann Handled setzen
// 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.
- Мост с контрактом: JSON-сообщения с
name, опциональнымcid(Correlation-ID) иpayloadподдерживаемы и тестируемы. - Операционно безопасная персистентность: контролируемый
UserDataFolderпредотвращает конфликты кэша, проблемы с правами и ситуацию «работает на машине разработчика, но не в эксплуатации».
JS↔Delphi-Bridge: почему WebMessage стабильнее, чем ExecuteScript
WebView2 предоставляет несколько каналов для коммуникации. На практике ExecuteScript выглядит привлекательно, но плохо версионируется: вы отправляете строки в интерпретатор без явных каналов ответа и без надежного сопоставления ошибок. PostWebMessageAsString / WebMessageReceived — это определённый канал.
Краевой случай, часто встречающийся в корпоративной среде: нужно из веб‑фронтенда (например, внутреннего портала) запустить workflow Delphi (печать, доступ к устройствам, интеграция с легаси). Тогда вам потребуются:
- белый список имён сообщений
- Correlation‑ID для асинхронных ответов
- центральное место валидации 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: если служба поддержки должна воспроизвести ошибку, предопределённый путь очень ценен. Папку можно заархивировать/сохранить (внимание: GDPR/PII) и сравнивать состояния целенаправленно.
Опционально (в зависимости от обёртки) вы можете добавить в EnvironmentOptions дополнительные аргументы браузера, например порт для remote‑debug. Это полезно, когда нужно проанализировать приложение на тестовой системе без локальных инструментов разработчика. Ограничения: в продуктивных средах это должно быть явно разрешено и задокументировано, иначе вы создаёте лишнюю поверхность атаки.
Подводные камни в Delphi WebView2 FMX: COM, потоки и жизненный цикл формы
1) Обратные вызовы после закрытия
Асинхронные CompletedHandler могут сработать после того, как форма уже закрывается. В примере FDestroyed предотвращает доступ к освобождённым объектам. Более надёжным будет дополнительно:
- Сохранять токены для событий и в
Destroyкорректно вызыватьremove_* - Разрешать
InitializeAsyncтолько один раз (машина состояний: Created/Initializing/Ready/Disposed)
2) Контекст потока
Многие обработчики приходят «близко к UI», но не полагайтесь на то, что вы можете напрямую писать в FMX-контролы. Если вы в OnWebMessage обновляете UI, безопасный вариант — TThread.Queue(nil, ...). Я предпочитаю разделение: Host собирает событие, Application-Service принимает решение, UI обновляется исключительно через очередь.
3) DPI/Изменение размера и FMX-Layouts
FMX оперирует логическими единицами, WebView2 ожидает Pixel-Rects. На практике нужна чёткая точка, где вы переводите bounds FMX-контролов в реальные пиксели. В примере используется TRect; в вашей форме вы должны из него вывести WinAPI-координаты (например, через FMX.Platform.Win и Handle-API). Если приложение масштабируется по DPI монитора, протестируйте переключение между мониторами: WebView2 здесь чувствительнее, чем чистые FMX-контролы.
Когда WebView2 в FMX имеет смысл — а когда нет
WebView2 имеет смысл, если в развитом Delphi-клиентском приложении вы хотите целенаправленно использовать веб-технологии: встроенные административные представления, OAuth/OIDC-потоки входа, HTML-отчёты, внутренние порталы или контролируемые «Micro-Frontends». Также это практичный мост при модернизации, пока вы чётко разграничиваете зоны ответственности и не превращаете мост в неконтролируемую заднюю дверь для бизнес-логики.
Ограничения подхода:
- Платформа: Шаблон ориентирован на Windows. FMX — кроссплатформенный, WebView2 — нет. Для macOS/iOS/Android потребуется другой WebView или слой абстракции.
- Security/Hardening: Как только загружаются внешние ресурсы, необходимо жёстко ограничивать навигацию, разрешённые домены и цели загрузки. Это должно быть отражено в требованиях, а не «потом».
- Support: UserDataFolder и зависимости рантайма (WebView2 Runtime) должны быть частью вашей концепции эксплуатации/раскатки.
Вывод
Delphi WebView2 FMX — это не UI-игрушка, а интеграционная компонента с собственным жизненным циклом. Если вы структурированно инкапсулируете инициализацию, Eventing, UserDataFolder и JS-Bridge, WebView2 станет стабильным элементом цифровых корпоративных решений: веб-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.