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.

Nächster Schritt

Wenn aus dem Thema ein reales Projekt wird, sollten Architektur, Bestand und Betrieb früh zusammen betrachtet werden.

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

  • Поточний стан, цільова архітектура та технічні ризики оцінюються спільно.
  • REST, Datenzugriff, Portale und Rollout werden nicht als Spätfolgen verschoben.
  • Sie sehen früh, welcher Weg wirtschaftlich und betrieblich tragfähig ist.

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

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

LinkedIn, X, XING, Facebook, WhatsApp und E-Mail sind sofort verfügbar. Für Instagram bereiten wir Link und Kurztext direkt vor.

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

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