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 ».
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, éventuellementcid(Correlation-ID) etpayloadsont maintenables et testables. - Persistance fiable en exploitation : un
UserDataFoldercontrô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
Destroysauberremove_*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.