Від теми журналу до практики проєкту
Відповідні сторінки послуг і технічні сторінки до публікації
Якщо в існуючому бізнес‑ПЗ раптом потрібно «швидко й просто» вбудувати сучасний веб‑контент, то в контексті 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, коли інтеграції, потоки даних і подальший розвиток мають працювати злагоджено.
Наступний крок
Якщо тема перетворюється на реальний проєкт, архітектуру, наявну інфраструктуру та експлуатацію слід розглядати разом на ранньому етапі.
Ми підтримуємо не лише в окремих питаннях, а й тоді, коли з уривків вихідного коду, питань, пов’язаних із legacy, або ідей порталу має вирости надійний корпоративний проєкт.
- Поточний стан, цільова архітектура та технічні ризики оцінюються спільно.
- REST, доступ до даних, портали та розгортання не відкладаються на пізніші етапи.
- Ви завчасно визначаєте, який підхід є економічно та операційно життєздатним.