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 околу директориумите за кориснички податоци, преземањата, отстранување грешки и робусната JS↔Delphi комуникација.

Овој исечок од изворен код го прикажува образецот што го претпочитам за одржливи апликации: капсулиран „Host“-објект што го контролира животниот циклус на WebView2, како и дефиниран мост преку WebMessage (JSON), наместо произволно „ExecuteScript“ насекаде. Целта не е демо-код, туку градежен елемент што опстанува во постоечки клиенти.

Зошто WebView2 во FMX е поинаков отколку „Browser-Component drop“

WebView2 е API блиска до COM/WinRT со асинхрона инициализација. FireMonkey ги апстрахира Windows-handles, сепак за WebView2 на крај ви е потребно вистинско Parent-Window (HWND) и контролирано пренасочување на прилагодување на големина и фокус. Истовремено, настаните не секогаш се извршуваат таму каде што се очекува во FMX. Ако тука почнете „quick and dirty“, типично ќе добиете:

  • спорадични AVs при затворање на формата (Callbacks се повикуваат по Destroy)
  • Navigation-евенти од погрешен нитски контекст
  • неподоверлива перзистенција/проблеми со кешот поради нејасна UserDataFolder-стратегија
  • нема преземања или „заглавени“ дијалози за преземање
  • Debugging само по среќа наместо со целосно конфигурирана Remote-Debug поставка

Противотров е јасен lifecycle: Create → InitializeAsync → Attach → Navigate → Detach/Dispose – и дефинирана граница помеѓу UI и browser-engine.

Исечок од изворен код: WebView2Host за Delphi WebView2 FMX

Следниот код го нацртува капсулираното Host-класата, која (1) создава WebView2-Environment-конфигурација, (2) го поврзува Controller-објектот со HWND, (3) поврзува Navigation и Download-настани и (4) нуди JSON-базирана JS-Bridge преку WebMessageReceived. Кодот е намерно „архитектурно-погоден“: ги капсулира COM-референците, спречува callback-наследници по Destroy, и дозволува оперативни граници како „по корисник“ или „по машина“ со одделни UserDataFolder.

Delphi
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;

  // Options: тука може да се додадат дополнителни аргументи за прелистувачот, н.пр. 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;

              // Почетно прикажување
              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-Bridge: зошто WebMessage е постабилен отколку ExecuteScript

WebView2 нуди неколку патеки за комуникација. Во практика ExecuteScript е примамлив, но тешко се верзионира: вметнувате стрингови во интерпретатор без јасни канали за одговор и без робусно мапирање на грешки. PostWebMessageAsString / WebMessageReceived е, пак, дефиниран канал.

Рандм случај што често се појавува во ентерпрајз средини: треба од веб-frontend (на пр. интерен портал) да стартувате Delphi-workflow (печат, пристап до уреди, legacy-интеграција). Тогаш ви требаат:

  • бела листа на имиња на пораки
  • Correlation-IDs за асинхрони одговори
  • централна точка што ги валидира payload-ите (на пр. задолжителни полиња, лимити на големина)

Во хостот тоа е местото OnWebMessageReceived. Самата валидација припаѓа во надлежен горен слој (на пр. Application-Service), за да ја раздвоите UI-/WebView2-техниката од бизнис-логиката (класична слоистa архитектура: UI → Application → Domain → Infrastruktur).

Downloads und Dateiablage: was im Betrieb oft überrascht

Downloads во WebView2 се реализираат преку ICoreWebView2DownloadOperation. Во зависност од изворот ResultFilePath може да биде празен рано или да се постави подоцна. Покрај тоа, многу компании не сакаат крајни корисници да зачувуваат во неконтролирани фолдери.

Препорачани шаблони:

  • Intercept на DownloadStarting и со args.put_Handled(1) преземете вие самите UI-логиката (сопствен пат, конвенција за имиња, карантин-фолдер).
  • Граници на големина на датотеки и проверки на MIME-type за да избегнете „случајно 4 GB лог-датотека“.
  • Аудитирање: запишете метаподатоци за download (URI, MIME, байти) во вашето логирање, не содржината.

Ако имате регулирани процеси (на пр. одобрувања, проверливост), обработката преку овие настани е единственото место каде што светот на прелистувачите можете да го вградите во вашите оперативни правила.

Debugging: DevTools, Remote Debug Port und reproduzierbare Zustände

Debugging на WebView2 често пропаѓа бидејќи состојбите не се репродуцираат. Две прилагодувања помагаат:

  • Вклучување/исклучување на DevTools преку ICoreWebView2Settings (во код: SetDevToolsEnabled) – во релиз често исклучено, во случај на поддршка целно вклучено.
  • Стабилен UserDataFolder: кога вашиот support треба да репродуцира грешка, дефиниран пат е злато. Можете да го архивирате/zip-нете фолдерот (внимание: заштита на податоци/PII) и да ги споредувате состојбите намерно.

Опционално (во зависност од wrapper) можете да додадете аргументи на прелистувачот преку EnvironmentOptions, на пр. remote-debug-port. Тоа е корисно кога треба да анализирате апликација на тест систем без локални развојни алатки. Ограничувања: во продукција тоа мора да биде чисто одобрено и документирано, инаку создавате непотребна површина за напади.

Stolperfallen in Delphi WebView2 FMX: COM, Threads und Form-Lifecycle

1) Повратни повици по затворање

Асинхронните CompletedHandler може да пристигнат откако формата веќе се затвора. Во примерот FDestroyed го спречува пристапот до ослободените објекти. Поустойчиво е дополнително:

  • Зачувајте токени за настани и во Destroy чисто повикајте remove_*
  • Дозволете InitializeAsync само еднаш (состојбена машина: Created/Initializing/Ready/Disposed)

2) Контекст на нишка

Многу обработувачи доаѓаат „блиску до UI“, но не сметајте на тоа дека можете директно да пишувате во FMX-контроли. Ако во OnWebMessage ажурирате UI, 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-клиентска апликација сакате целенасочено да вклучите веб-технологија: вградени админ-погледи, OAuth/OIDC лог-ин флоуa, HTML-извештаи, интерни портали или контролирани „Micro-Frontends“. Како мост за модернизација е практичен, сè додека јасно ги исечете одговорностите и мостот не стане неконтролирана задна врата за бизнис-логика.

Граници на пристапот:

  • Платформа: Шемата е центрирана околу Windows. FMX е мултиплатформска, WebView2 не е. За macOS/iOS/Android ви требаат други WebView-ови или слој за апстракција.
  • Security/Hardening: Откако ќе се вчитаат екстерни содржини, треба построго да ги ограничите навигацијата, дозволените домени и местата за преземање. Тоа припаѓа во барањата, не „подоцна“.
  • Поддршка: UserDataFolder и Runtime-зависности (WebView2 Runtime) мора да бидат дел од вашиот оперативен/rollout концепт.

Заклучок

Delphi WebView2 FMX е помалку UI-геџет и повеќе интеграциска компонента со свој животен циклус. Ако ја капсулирате инициализацијата, ракувањето со настани, UserDataFolder и JS-Bridge структурирано, WebView2 ќе стане стабилен градежен елемент за дигитални корпоративни решенија: Web-UI таму каде што има смисла, и Delphi-логика таму каде ѝ е местото. Ако пак неконтролирано пуштате скрипти, ги оставате патиштата на случајноста и не ги одвојувате настаните, ќе добиете токму таа категорија „спорадични на терен“ грешки што ги троши ресурсите и ја разнишува довербата.

Ако сакате да го интегрирате WebView2 чисто во постоечка Delphi-апликација или технички да оцените работен опсег за модернизација, разговарајте со нас:

Во стручното опкружување, Webview2 Firemonkey и Delphi Fmx Edge Browser исто така играат важна улога кога интеграциите, протокот на податоци и натамошниот развој треба да се усогласат.

Разговарајте за проект или намера за модернизација со Net-Base.

Следен чекор

Кога темата ќе прерасне во реален проект, архитектурата, постоечката средина и експлоатацијата треба рано да се разгледаат заедно.

Не поддржуваме само при поединечни прашања, туку и кога од исечоци од изворен код, legacy-теми или идеи за портали треба да прерасне во робустен корпоративен проект.

  • Постоечката состојба, целната слика и техничките ризици се проценуваат заедно.
  • REST, пристапот до податоци, порталите и Rollout не се одложуваат како подоцнежни последици.
  • Уште рано идентификувате кој пат е економски и оперативно одржлив.

Сподели објава

Споделете го овој пост директно.

LinkedIn, X, XING, Facebook, WhatsApp и е-пошта се веднаш достапни. За Instagram директно подготвуваме линк и краток текст.

Е-пошта

Instagram се отвора во нов таб. Линкот и краткиот текст претходно се копираат во меѓуспремникот.