Net-Base Журнал

14.06.2026

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

WebView2 у FireMonkey звучить як «просто вбудувати браузер», але на практиці дає збої при ініціалізації, обробці подій навігації, мосту JS↔Delphi, обробці завантажень і відлагодженні. Цей фрагмент коду демонструє надійну схему з чітким розподілом відповідальностей.

14.06.2026

Від теми журналу до практики проєкту

Відповідні сторінки послуг і технічні сторінки до публікації

Якщо в існуючому бізнес‑ПЗ раптом потрібно «швидко й просто» вбудувати сучасний веб‑контент, то в контексті Windows йдуть до WebView2. У Delphi WebView2 FMX основна проблема рідко полягає у відображенні URL, швидше — у коректному вбудуванні в інтерфейс FireMonkey (FMX), надійному ініціалізуванні (асинхронно і на базі COM), а також у підводних каменях Edge, що стосуються User‑Data‑каталогів, завантажень, налагодження та надійної JS↔Delphi‑комунікації.

Цей фрагмент коду демонструє патерн, який я віддаю перевагу для підтримуваних застосунків: інкапсульований «Host»‑об’єкт, що контролює життєвий цикл WebView2, і чітко визначений міст через WebMessage (JSON) замість довільних «ExecuteScript всюди». Мета — не демо‑код, а будівельний блок, що виживає в дорослих клієнтах.

Чому WebView2 у FMX відрізняється від «Browser‑Component drop»

WebView2 — це API близьке до COM/WinRT з асинхронною ініціалізацією. FireMonkey абстрагує Windows‑дескриптори, але в підсумку для WebView2 потрібне реальне батьківське вікно (HWND) і контрольована переадресація зміни розмірів/фокусу. Одночасно події іноді спрацьовують не там, де ви очікуєте в FMX. Якщо почати «quick and dirty», зазвичай отримаєте:

  • епізодичні AVs при закритті форми (Callbacks надходять після Destroy)
  • події навігації в невірному контексті потоку
  • ненадійну персистентність/проблеми кешу через невизначену стратегію UserDataFolder
  • неможливість завантажень або «завислі» діалоги завантаження
  • налагодження — більше пощастить, ніж цілеспрямована конфігурація віддаленого дебагу

Протидія — чіткий життєвий цикл: Create → InitializeAsync → Attach → Navigate → Detach/Dispose — і визначена межа між UI та рушієм браузера.

Source‑Schnipsel: WebView2Host für Delphi WebView2 FMX

Наведений нижче код окреслює інкапсульований клас Host, який (1) створює конфігурацію WebView2‑Environment, (2) прив’язує Controller‑об’єкт до HWND, (3) підключає події навігації і завантажень та (4) пропонує JSON‑базований JS‑міст через WebMessageReceived. Код свідомо «придатний для архітектури»: він інкапсулює COM‑референси, запобігає «слідам» колбеків після Destroy і дозволяє експлуатаційні варіанти, наприклад розділені UserDataFolder для рівня користувача або для машини.

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;

// Обробник подій
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;

// Асинхронне створення середовища
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_*.
// Тут у вигляді коментаря, оскільки налаштування імпортної юніти та управління токенами залежать від обгортки.
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);

// Опційно: власний інтерфейс завантаження, тоді встановити 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.
  • Міст з контрактом: JSON-повідомлення з name, опціонально cid (Correlation-ID) та payload підлягають супроводу та тестуванню.
  • Надійна для експлуатації персистентність: контрольований UserDataFolder запобігає колізіям кешу, проблемам з правами доступу та ситуаціям «працює на машині розробника, але не в експлуатації».

JS↔Delphi-Bridge: чому WebMessage стабільніший за ExecuteScript

WebView2 пропонує кілька шляхів для комунікації. На практиці ExecuteScript спокушає, але його важко версіонувати: ви штовхаєте рядки в інтерпретатор без чітких каналів відповіді і без надійного відображення помилок. PostWebMessageAsString / WebMessageReceived натомість — визначений канал.

Крайній випадок, який часто виникає в корпоративному середовищі: потрібно зі Web‑фронтенду (наприклад, внутрішнього порталу) запустити Delphi‑воркфлоу (друк, доступ до пристроїв, інтеграція з Legacy). Тоді вам потрібні:

  • біла список імен повідомлень
  • 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: якщо вашій службі підтримки потрібно відтворити помилку, визначений шлях безцінний. Ви можете заархівувати/запакувати папку (Увага: захист даних/PII) і цілеспрямовано порівнювати стани.

Опціонально (залежно від wrapper‑а) ви можете передати EnvironmentOptions з додатковими аргументами браузера, напр., Remote‑Debug‑Port. Це має сенс, коли потрібно аналізувати застосунок на тестовій системі без локальних інструментів розробника. Межі: у продуктивних середовищах це потрібно коректно дозволити та задокументувати, інакше ви створите непотрібну векторну поверхню атаки.

Підводні камені в Delphi WebView2 FMX: COM, потоки та життєвий цикл форми

1) Зворотні виклики після закриття

Асинхронні CompletedHandler можуть надійти після того, як форма вже закривається. У прикладі FDestroyed перешкоджає доступу до звільнених об’єктів. Додатково більш робастним є:

  • Зберігайте токени для подій і в Destroy коректно викликайте remove_*
  • Дозволяйте виклик InitializeAsync лише один раз (машина станів: Created/Initializing/Ready/Disposed)

2) Контекст потоку

Багато обробників справді приходять «UI‑близько», але не покладайтеся на те, що можна писати безпосередньо в FMX‑контроли. Якщо ви оновлюєте UI в OnWebMessage, безпечний варіант — TThread.Queue(nil, ...). Я зазвичай розділяю обов’язки: хост збирає подію, Application‑Service приймає рішення, UI оновлюється виключно через Queue.

3) DPI/зміна розміру та FMX‑макети

FMX оперує логічними одиницями, WebView2 очікує піксельні прямокутники. На практиці потрібна чітка точка, де ви перетворюєте Bounds FMX‑контролів у реальні пікселі. У прикладі береться TRect; у вашій формі з нього слід вивести координати WinAPI (напр., через FMX.Platform.Win та Handle‑APIs). Якщо додаток масштабується за Monitor‑DPI, протестуйте перемикання між моніторами: у цьому WebView2 чутливіший, ніж чисті FMX‑контроли.

Коли WebView2 в FMX виправданий — а коли ні

WebView2 має сенс, якщо ви в існуючому Delphi‑клієнтському застосунку цілеспрямовано використовуєте веб‑технології: вбудовані Admin‑Views, OAuth/OIDC‑Login‑Flows, HTML‑Reports, внутрішні портали або контрольовані «Micro‑Frontends». Як міст для модернізації це також практичний підхід, доки ви чітко розмежовуєте відповідальності і не даєте мосту перетворитися на неконтрольовану «задню двері» для бізнес‑логіки.

Обмеження підходу:

  • Платформа: Шаблон орієнтований на Windows. FMX — кросплатформений, WebView2 — ні. Для macOS/iOS/Android потрібні інші WebView або шар абстракції.
  • Security/Hardening: Як тільки завантажуються зовнішні вмісти, необхідно жорсткіше обмежити навігацію, дозволені домени та цілі завантаження. Це має бути закладено в вимогах, не «пізніше».
  • Підтримка: UserDataFolder і runtime‑залежності (WebView2 Runtime) повинні бути частиною вашої концепції експлуатації/розгортання.

Висновок

Delphi WebView2 FMX — це радше не UI‑ґаджет, а інтеграційний компонент з власним життєвим циклом. Якщо ви структуровано інкапсулюєте ініціалізацію, обробку подій, UserDataFolder і JS‑Bridge, WebView2 стане стабільним елементом цифрових корпоративних рішень: Web‑UI там, де це має сенс, і Delphi‑логіка там, де їй місце. Якщо ж ви безконтрольно запускаєте скрипти, залишаєте шляхи на волю випадку і не розділяєте обробку подій, отримаєте саме ті «епізодичні в полі» помилки, які відбирають час і підривають довіру.

Якщо ви хочете чисто інтегрувати WebView2 в існуючий Delphi‑застосунок або технічно оцінити крок модернізації, поговоріть з нами:

У практичному контексті також важливу роль відіграють Webview2 Firemonkey і Delphi Fmx Edge Browser, коли інтеграції, потоки даних і подальший розвиток мають працювати злагоджено.

Обговорити проєкт або завдання з модернізації з Net-Base.

Наступний крок

Якщо тема перетворюється на реальний проєкт, архітектуру, наявну інфраструктуру та експлуатацію слід розглядати разом на ранньому етапі.

Ми підтримуємо не лише в окремих питаннях, а й тоді, коли з уривків вихідного коду, питань, пов’язаних із legacy, або ідей порталу має вирости надійний корпоративний проєкт.

  • Поточний стан, цільова архітектура та технічні ризики оцінюються спільно.
  • REST, доступ до даних, портали та розгортання не відкладаються на пізніші етапи.
  • Ви завчасно визначаєте, який підхід є економічно та операційно життєздатним.

Поділитися дописом

Поділитися цим дописом безпосередньо

LinkedIn, X, XING, Facebook, WhatsApp та електронна пошта доступні негайно. Для Instagram ми готуємо посилання та короткий текст безпосередньо.

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

Instagram відкривається в новій вкладці. Посилання та короткий текст попередньо копіюються у буфер обміну.