Net-Base Magazin

14.06.2026

Delphi WebView2 in FMX: sauber initialisieren, JS-Bridge bauen, Downloads und Debugging im Griff

WebView2 in FireMonkey klingt nach „einfach Browser einbetten“, kippt in der Praxis aber bei Initialisierung, Navigation-Events, JS↔Delphi-Bridge, Download-Handling und Debugging. Dieser Source-Schnipsel zeigt ein robustes Muster mit eindeutigen Zuständigkeiten...

14.06.2026

Vom Magazinthema zur Projektpraxis

Passende Leistungs- und Technikseiten zum Beitrag

Wer in einer bestehenden Business-Software plötzlich „mal eben“ moderne Web-Inhalte einbetten will, landet auf Windows bei WebView2. In Delphi WebView2 FMX ist das Grundproblem selten das Anzeigen einer URL, sondern die saubere Einbettung in eine FireMonkey-Oberfläche (FMX), das zuverlässige Initialisieren (asynchron und COM-basiert), sowie die Edge-Fallstricke rund um User-Data-Verzeichnisse, Downloads, Debugging und eine robuste JS↔Delphi-Kommunikation.

Dieser Source-Schnipsel zeigt ein Muster, das ich für wartbare Anwendungen bevorzuge: ein gekapseltes „Host“-Objekt, das den WebView2-Lifecycle kontrolliert, sowie eine definierte Bridge über WebMessage (JSON), statt beliebigem „ExecuteScript überall“. Ziel ist kein Demo-Code, sondern ein Baustein, der in gewachsenen Clients überlebt.

Warum WebView2 in FMX anders ist als „Browser-Component drop“

WebView2 ist eine COM/WinRT-nahe API mit asynchroner Initialisierung. FireMonkey abstrahiert Windows-Handles, trotzdem benötigen Sie für WebView2 am Ende ein echtes Parent-Window (HWND) und kontrollierte Resize-/Focus-Weiterleitung. Gleichzeitig laufen Ereignisse nicht immer dort auf, wo man sie in FMX erwartet. Wenn Sie hier „quick and dirty“ starten, bekommen Sie typischerweise:

  • sporadische AVs beim Form-Schließen (Callbacks treffen nach Destroy ein)
  • Navigation-Events aus einem falschen Thread-Kontext
  • unzuverlässige Persistenz/Cache-Probleme wegen unklarer UserDataFolder-Strategie
  • keine Downloads oder „hängende“ Download-Dialoge
  • Debugging nur über Glück statt gezielter Remote-Debug-Konfiguration

Das Gegenmittel ist ein klarer Lifecycle: Create → InitializeAsync → Attach → Navigate → Detach/Dispose – und eine definierte Grenze zwischen UI und Browser-Engine.

Source-Schnipsel: WebView2Host für Delphi WebView2 FMX

Der folgende Code skizziert eine gekapselte Host-Klasse, die (1) eine WebView2-Environment-Konfiguration erstellt, (2) das Controller-Objekt an ein HWND bindet, (3) Navigation und Download-Events verdrahtet und (4) eine JSON-basierte JS-Bridge über WebMessageReceived anbietet. Der Code ist bewusst „architekturfähig“: er kapselt COM-Referenzen, verhindert Callback-Nachläufer nach Destroy, und erlaubt Betriebskanten wie „pro User“ oder „pro Maschine“ getrennte UserDataFolder.

Delphi
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 ist bereits zerstört.');
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 ist noch nicht initialisiert.');

  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.

Zweck des Ansatzes

  • Lifecycle-Kapselung: Die FMX-Form kennt nur „Initialize/Navigate/Resize“, nicht COM-Details.
  • Bridge mit Vertrag: JSON-Nachrichten mit name, optional cid (Correlation-ID) und payload sind wartbar und testbar.
  • Betriebssichere Persistenz: ein kontrolliertes UserDataFolder verhindert Cache-Kollisionen, Rechteprobleme und „läuft auf Entwicklerrechner, nicht im Betrieb“.

JS↔Delphi-Bridge: warum WebMessage stabiler ist als ExecuteScript

WebView2 bietet mehrere Wege der Kommunikation. In der Praxis ist ExecuteScript verführerisch, aber schlecht zu versionieren: Sie schieben Strings in einen Interpreter, ohne klare Antwort-Kanäle und ohne robustes Error-Mapping. PostWebMessageAsString / WebMessageReceived ist dagegen ein definierter Kanal.

Randfall, der in Unternehmensumgebungen häufig auftaucht: Sie müssen aus einem Web-Frontend (z. B. internes Portal) einen Delphi-Workflow starten (Druck, Gerätezugriff, Legacy-Integration). Dann brauchen Sie:

  • eine Whitelist von Message-Namen
  • Correlation-IDs für asynchrone Antworten
  • eine zentrale Stelle, die Payloads validiert (z. B. Pflichtfelder, Größenlimits)

Im Host ist das die Stelle OnWebMessageReceived. Die eigentliche Validierung gehört in eine darüber liegende Schicht (z. B. Application-Service), damit Sie UI-/WebView2-Technik und Business-Logik getrennt halten (klassische Layer-Architektur: UI → Application → Domain → Infrastruktur).

Downloads und Dateiablage: was im Betrieb oft überrascht

Downloads laufen in WebView2 über ICoreWebView2DownloadOperation. Je nach Quelle kann ResultFilePath früh leer sein oder erst später gesetzt werden. Zudem wollen viele Unternehmen nicht, dass Endanwender in unkontrollierte Ordner speichern.

Bewährte Muster:

  • DownloadStarting abfangen und per args.put_Handled(1) die UI selbst übernehmen (eigener Pfad, Namenskonvention, Quarantäne-Ordner).
  • Dateigrößen-Grenzen und MIME-Type-Checks, um „versehentlich 4 GB Logfile“ zu vermeiden.
  • Auditing: Download-Metadaten (URI, MIME, Bytes) in Ihr Logging schreiben, nicht den Inhalt.

Wenn Sie regulierte Prozesse haben (z. B. Freigaben, Nachvollziehbarkeit), ist das Handling über die Events die einzige Stelle, an der Sie die Browser-Welt in Ihre Betriebsregeln integrieren können.

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

WebView2-Debugging kippt häufig daran, dass Zustände nicht reproduzierbar sind. Zwei Stellschrauben helfen:

  • DevTools aktivieren/deaktivieren über ICoreWebView2Settings (im Code: SetDevToolsEnabled) – im Release oft aus, im Support-Fall gezielt an.
  • Stabiles UserDataFolder: Wenn Ihr Support einen Fehler nachstellen soll, ist ein definierter Pfad Gold wert. Sie können den Ordner sichern/zippen (Achtung: Datenschutz/PII) und Zustände gezielt vergleichen.

Optional (je nach Wrapper) können Sie EnvironmentOptions mit zusätzlichen Browser-Argumenten versehen, z. B. einen Remote-Debug-Port. Das ist sinnvoll, wenn Sie eine Anwendung auf einem Testsystem ohne lokale Entwickler-Tools analysieren müssen. Grenzen: In produktiven Umgebungen muss das sauber freigeschaltet und dokumentiert sein, sonst schaffen Sie eine unnötige Angriffsfläche.

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

1) Callbacks nach dem Schließen

Die asynchronen CompletedHandler können eintreffen, nachdem die Form schon schließt. Im Snippet verhindert FDestroyed den Zugriff auf freigegebene Objekte. Robuster ist zusätzlich:

  • Tokens für Events speichern und in Destroy sauber remove_* aufrufen
  • InitializeAsync nur einmal zulassen (State-Machine: Created/Initializing/Ready/Disposed)

2) Thread-Kontext

Viele Handler kommen zwar „UI-nah“, aber verlassen Sie sich nicht darauf, dass Sie direkt in FMX-Controls schreiben können. Wenn Sie in OnWebMessage UI aktualisieren, ist TThread.Queue(nil, ...) die sichere Variante. Ich trenne gern: Host sammelt Ereignis, Application-Service entscheidet, UI wird ausschließlich per Queue aktualisiert.

3) DPI/Resize und FMX-Layouts

FMX rechnet in logischen Einheiten, WebView2 erwartet Pixel-Rects. In der Praxis brauchen Sie eine klare Stelle, an der Sie aus FMX-Controls Bounds in echte Pixel übersetzen. Das Snippet nimmt ein TRect an; in Ihrer Form sollten Sie daraus die WinAPI-Koordinaten ableiten (z. B. über FMX.Platform.Win und Handle-APIs). Wenn die App per Monitor-DPI skaliert, testen Sie den Wechsel zwischen Monitoren: WebView2 ist hier empfindlicher als reine FMX-Controls.

Wann sich WebView2 in FMX lohnt – und wann nicht

WebView2 lohnt sich, wenn Sie in einer gewachsenen Delphi-Client-Anwendung Web-Technik gezielt einsetzen wollen: eingebettete Admin-Views, OAuth/OIDC-Login-Flows, HTML-Reports, interne Portale oder kontrollierte „Micro-Frontends“. Auch als Modernisierungsbrücke ist es praktikabel, solange Sie die Zuständigkeiten sauber schneiden und die Bridge nicht zur unkontrollierten Hintertür für Business-Logik machen.

Grenzen des Ansatzes:

  • Plattform: Das Muster ist Windows-zentriert. FMX ist multiplattformfähig, WebView2 ist es nicht. Für macOS/iOS/Android brauchen Sie andere WebViews oder eine Abstraktionsschicht.
  • Security/Hardening: Sobald externe Inhalte geladen werden, müssen Sie Navigation, erlaubte Domains und Download-Ziele härter einschränken. Das gehört in Requirements, nicht „später“.
  • Support: UserDataFolder und Runtime-Abhängigkeiten (WebView2 Runtime) müssen Teil Ihres Betriebs-/Rollout-Konzepts sein.

Fazit

Delphi WebView2 FMX ist weniger ein UI-Gadget als eine Integrationskomponente mit eigenem Lifecycle. Wenn Sie Initialisierung, Eventing, UserDataFolder und JS-Bridge strukturiert kapseln, wird WebView2 ein stabiler Baustein für digitale Unternehmenslösungen: Web-UI dort, wo es Sinn ergibt, und Delphi-Logik dort, wo sie hingehört. Wenn Sie dagegen unkontrolliert Skripte feuern, Pfade dem Zufall überlassen und Events nicht entkoppeln, bekommen Sie genau die Sorte „sporadisch im Feld“-Fehler, die Zeit frisst und Vertrauen kostet.

Wenn Sie bei einer bestehenden Delphi-Anwendung WebView2 sauber integrieren oder eine Modernisierungskante technisch bewerten wollen, sprechen Sie mit uns:

Im fachlichen Umfeld spielen auch Webview2 Firemonkey und Delphi Fmx Edge Browser eine wichtige Rolle, wenn Integrationen, Datenflüsse und Weiterentwicklung sauber zusammenspielen müssen.

Projekt oder Modernisierungsvorhaben mit Net-Base besprechen.

Nächster Schritt

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

Wir unterstuetzen nicht nur bei Einzelfragen, sondern auch dann, wenn aus Source-Schnipseln, Legacy-Themen oder Portalideen ein belastbares Unternehmensprojekt werden soll.

  • Bestand, Zielbild und technische Risiken werden zusammen bewertet.
  • REST, Datenzugriff, Portale und Rollout werden nicht als Spaetfolgen verschoben.
  • Sie sehen frueh, welcher Weg wirtschaftlich und betrieblich tragfähig ist.

Beitrag teilen

Diesen Beitrag direkt weitergeben

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

E-Mail

Instagram oeffnet in einem neuen Tab. Link und Kurztext werden vorher in die Zwischenablage kopiert.