Net-Base Magazine

14.06.2026

Delphi WebView2 dans FMX : initialisation fiable, implémentation d'une JS-Bridge, maîtrise des téléchargements et du débogage

WebView2 dans FireMonkey donne l'impression de « simplement intégrer un navigateur », mais pose problème en pratique lors de l'initialisation, des événements de navigation, du pont JS↔Delphi, de la gestion des téléchargements et du débogage. Cet extrait de code montre un modèle robuste avec des responsabilités clairement définies...

14.06.2026

Du thème du magazine à la pratique des projets

Pages de services et techniques pertinentes pour l'article

Quiconque souhaite intégrer rapidement des contenus Web modernes dans une solution logicielle d’entreprise existante se retrouve sur Windows avec WebView2. Dans Delphi WebView2 FMX, le problème fondamental n’est que rarement l’affichage d’une URL : il s’agit plutôt de l’intégration propre dans une interface FireMonkey (FMX), de l’initialisation fiable (asynchrone et basée sur COM), ainsi que des pièges propres à Edge autour des répertoires de données utilisateur, des téléchargements, du débogage et d’une communication JS↔Delphi robuste.

Ce fragment de code présente un modèle que je privilégie pour les applications maintenables : un objet « Host » encapsulé qui contrôle le cycle de vie de WebView2, ainsi qu’un pont défini via WebMessage (JSON), au lieu d’un recours généralisé à « ExecuteScript partout ». L’objectif n’est pas du code de démonstration, mais un composant capable de survivre dans des clients existants.

Pourquoi WebView2 dans FMX est différent d’un simple « Browser-Component drop »

WebView2 est une API proche de COM/WinRT avec une initialisation asynchrone. FireMonkey abstrait les handles Windows, néanmoins pour WebView2 vous aurez finalement besoin d’une véritable fenêtre parente (HWND) et d’un renvoi contrôlé des événements de redimensionnement et de focus. Parallèlement, les événements ne s’exécutent pas toujours là où on les attend dans FMX. Si vous procédez ici de manière « quick and dirty », vous obtenez typiquement :

  • violations d’accès (AVs) sporadiques à la fermeture du formulaire (des callbacks arrivent après Destroy)
  • événements de navigation issus d’un contexte de thread incorrect
  • problèmes de persistance/cache peu fiables en raison d’une stratégie UserDataFolder floue
  • absence de téléchargements ou boîtes de dialogue de téléchargement « bloquées »
  • débogage laissé au hasard plutôt qu’à une configuration de débogage à distance ciblée

Le remède est un cycle de vie clair : Create → InitializeAsync → Attach → Navigate → Detach/Dispose – et une frontière définie entre l’UI et le moteur du navigateur.

Extrait de code : WebView2Host pour Delphi WebView2 FMX

Le code suivant esquisse une classe hôte encapsulée qui (1) crée une configuration d’environnement WebView2, (2) lie l’objet controller à un HWND, (3) câble les événements de navigation et de téléchargement et (4) propose un pont JS basé sur JSON via WebMessageReceived. Le code est volontairement « prêt pour l’architecture » : il encapsule les références COM, évite les callbacks retardataires après Destroy et permet des stratégies d’exploitation distinctes — par exemple des UserDataFolder séparés « par utilisateur » ou « par machine ».

Delphi
unit WebView2Host;

interface

uses
  System.SysUtils, System.Classes, System.IOUtils, System.JSON,
  Winapi.Windows, Winapi.ActiveX,
  FMX.Types,
  WebView2, WebView2_TLB; // selon la configuration : WebView2.pas ou 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', '');

    // Le payload peut être absent ou 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;

  // Détacher les événements avant de libérer les objets COM
  UnhookEvents;

  FWebView := nil;
  FController := nil;
  FEnvironment := nil;

  inherited;
end;

procedure TWebView2Host.EnsureNotDestroyed;
begin
  if FDestroyed then
    raise EInvalidOperation.Create('WebView2Host a déjà été détruit.');
end;

function TWebView2Host.MakeUserDataFolder: string;
begin
  if FUserDataFolder <> '' then
    Exit(FUserDataFolder);

  // Pratique : par application + par utilisateur Windows, pas dans le répertoire du programme
  Result := TPath.Combine(TPath.GetHomePath, 'AppDataLocalMyCompanyMyAppWebView2');
  ForceDirectories(Result);
end;

procedure TWebView2Host.InitializeAsync;
var
  UserData: string;
  Opt: ICoreWebView2EnvironmentOptions;
begin
  EnsureNotDestroyed;

  UserData := MakeUserDataFolder;

  // Options : des arguments supplémentaires du navigateur peuvent être ajoutés ici, p.ex. 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;

        // Lier le controller au HWND parent
        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;

              // Le rendre visible initialement
              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));

  // Remarque : pour un détachement robuste, conservez les tokens.
  // Dans de nombreux projets, cela suffit si le host ne vit pas plus longtemps que le Form.
end;

procedure TWebView2Host.UnhookEvents;
begin
  // Variante robuste : mémoriser les tokens et appeler remove_*. 
  // Ici en commentaire, car la configuration de l'unité d'importation et la gestion des tokens varient selon le 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));

  // En pratique : ResultFileName initialement vide, selon la source.
  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);

    // Optionnel : interface de téléchargement personnalisée, puis définir 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 n\'est pas encore initialisé.');

  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.

But de l’approche

  • Encapsulation du cycle de vie : le formulaire FMX ne connaît que «Initialize/Navigate/Resize», pas les détails COM.
  • Passerelle contractuelle : des messages JSON avec name, éventuellement cid (Correlation-ID) et payload sont maintenables et testables.
  • Persistance fiable en exploitation : un UserDataFolder contrôlé évite les collisions de cache, les problèmes de droits et le cas «fonctionne sur la machine du développeur, pas en production».

JS↔Delphi-Bridge : pourquoi WebMessage est plus stable que ExecuteScript

WebView2 offre plusieurs voies de communication. En pratique, ExecuteScript est séduisant, mais difficile à versionner : on pousse des chaînes dans un interpréteur, sans canaux de réponse clairs et sans mappage d’erreurs robuste. PostWebMessageAsString / WebMessageReceived est en revanche un canal défini.

Cas limite fréquent en environnement d’entreprise : vous devez déclencher depuis un front web (p. ex. un portail interne) un workflow Delphi (impression, accès aux périphériques, intégration legacy). Vous devez alors :

  • une whitelist de noms de message
  • des Correlation-IDs pour les réponses asynchrones
  • un point central qui valide les payloads (p. ex. champs obligatoires, limites de taille)

Dans l’hôte, c’est le point OnWebMessageReceived. La validation proprement dite doit appartenir à une couche supérieure (p. ex. Application-Service), afin de séparer la technique UI/WebView2 et la logique métier (architecture en couches classique : UI → Application → Domain → Infrastructure).

Téléchargements et stockage de fichiers : ce qui surprend souvent en exploitation

Les téléchargements dans WebView2 passent par ICoreWebView2DownloadOperation. Selon la source, ResultFilePath peut être vide au départ ou rempli plus tard. De plus, beaucoup d’entreprises ne veulent pas que les utilisateurs finaux enregistrent dans des dossiers non contrôlés.

Bonnes pratiques :

  • Intercepter DownloadStarting et prendre en charge l’UI via args.put_Handled(1) (chemin propre, convention de nommage, dossier de quarantaine).
  • Limites de taille de fichier et vérifications du type MIME, pour éviter « un fichier de log de 4 Go par erreur ».
  • Audit : écrire les métadonnées du téléchargement (URI, MIME, octets) dans votre logging, pas le contenu.

Si vous avez des processus régulés (p. ex. validations, traçabilité), le traitement via les événements est le seul point où intégrer le monde du navigateur dans vos règles opérationnelles.

Débogage : DevTools, port de débogage distant et états reproductibles

Le débogage WebView2 échoue souvent parce que les états ne sont pas reproductibles. Deux leviers aident :

  • Activer/désactiver DevTools via ICoreWebView2Settings (dans le code : SetDevToolsEnabled) – souvent désactivés en release, activés ponctuellement en cas de support.
  • UserDataFolder stable : si votre support doit reproduire un bug, un chemin défini vaut de l’or. Vous pouvez sauvegarder/zipper le dossier (Attention : protection des données/PII) et comparer les états de façon ciblée.

Optionnel (selon le wrapper), vous pouvez fournir des EnvironmentOptions avec des arguments supplémentaires pour le navigateur, p. ex. un port de debug distant. Utile pour analyser une application sur un système de test sans outils développeur locaux. Limites : en production, cela doit être correctement autorisé et documenté, sinon vous ouvrez une surface d’attaque inutile.

Pièges dans Delphi WebView2 FMX : COM, threads et cycle de vie du formulaire

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.

Étape suivante

Lorsque ce sujet devient un projet concret, l'architecture, l'existant et l'exploitation doivent être examinés ensemble dès le départ.

Nous n'intervenons pas seulement sur des questions ponctuelles, mais aussi lorsque des fragments de code source, des problématiques liées aux systèmes legacy ou des concepts de portail doivent se transformer en un projet d'entreprise robuste.

  • L'état des lieux, l'état cible et les risques techniques sont évalués conjointement.
  • REST, l'accès aux données, les portails et le déploiement ne sont pas repoussés en tant que conséquences ultérieures.
  • Vous identifiez tôt quelle voie est viable sur le plan économique et opérationnel.

Partager l'article

Partager directement cette publication

LinkedIn, X, XING, Facebook, WhatsApp et e-mail sont immédiatement disponibles. Pour Instagram, nous préparons directement le lien et un court texte.

Courriel

Instagram s'ouvre dans un nouvel onglet. Le lien et le court texte sont préalablement copiés dans le presse-papiers.